1use alloc::string::String;
9use core::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
15pub enum Casemapping {
16 Ascii,
18 #[default]
20 Rfc1459,
21 Rfc1459Strict,
23}
24
25impl Casemapping {
26 pub fn parse(token: &str) -> Self {
29 match token {
30 "ascii" => Self::Ascii,
31 "rfc1459-strict" => Self::Rfc1459Strict,
32 _ => Self::Rfc1459,
33 }
34 }
35
36 pub fn fold_char(self, c: char) -> char {
38 match c {
39 'A'..='Z' => c.to_ascii_lowercase(),
40 '[' | ']' | '\\' if self != Self::Ascii => match c {
41 '[' => '{',
42 ']' => '}',
43 _ => '|',
44 },
45 '~' if self == Self::Rfc1459 => '^',
46 _ => c,
47 }
48 }
49
50 pub fn fold(self, name: &str) -> CaseFolded {
52 CaseFolded(name.chars().map(|c| self.fold_char(c)).collect())
53 }
54
55 pub fn eq(self, a: &str, b: &str) -> bool {
57 let mut a = a.chars();
58 let mut b = b.chars();
59 loop {
60 match (a.next(), b.next()) {
61 (None, None) => return true,
62 (Some(x), Some(y)) if self.fold_char(x) == self.fold_char(y) => {}
63 _ => return false,
64 }
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
79#[derive(Default)]
80pub struct CaseFolded(String);
81
82impl CaseFolded {
83 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87
88 pub fn into_string(self) -> String {
90 self.0
91 }
92}
93
94impl AsRef<str> for CaseFolded {
95 fn as_ref(&self) -> &str {
96 &self.0
97 }
98}
99
100impl fmt::Display for CaseFolded {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(&self.0)
103 }
104}
105
106impl core::borrow::Borrow<str> for CaseFolded {
107 fn borrow(&self) -> &str {
108 &self.0
109 }
110}
111
112impl From<CaseFolded> for String {
113 fn from(folded: CaseFolded) -> Self {
114 folded.0
115 }
116}
117
118impl From<&str> for CaseFolded {
120 fn from(name: &str) -> Self {
121 Casemapping::default().fold(name)
122 }
123}
124
125impl CaseFolded {
126 pub fn already_folded(value: impl Into<String>) -> Self {
128 Self(value.into())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn ascii_folds_only_letters() {
138 let map = Casemapping::Ascii;
139 assert_eq!(map.fold("NiCk").as_str(), "nick");
140 assert_eq!(map.fold("[a]").as_str(), "[a]");
141 }
142
143 #[test]
144 fn rfc1459_folds_the_bracket_alphabet() {
145 let map = Casemapping::Rfc1459;
146 assert_eq!(map.fold(r"[Nick]\~").as_str(), "{nick}|^");
147 }
148
149 #[test]
150 fn strict_leaves_tilde_alone() {
151 assert_eq!(Casemapping::Rfc1459Strict.fold("~a").as_str(), "~a");
152 assert_eq!(Casemapping::Rfc1459.fold("~a").as_str(), "^a");
153 }
154
155 #[test]
156 fn eq_matches_fold() {
157 let map = Casemapping::Rfc1459;
158 assert!(map.eq("[nick]", "{NICK}"));
159 assert!(!map.eq("nick", "nick2"));
160 assert!(!Casemapping::Ascii.eq("[nick]", "{nick}"));
161 }
162
163 #[test]
164 fn parses_the_isupport_token() {
165 assert_eq!(Casemapping::parse("ascii"), Casemapping::Ascii);
166 assert_eq!(
167 Casemapping::parse("rfc1459-strict"),
168 Casemapping::Rfc1459Strict
169 );
170 assert_eq!(Casemapping::parse("rfc1459"), Casemapping::Rfc1459);
171 assert_eq!(Casemapping::parse("something-else"), Casemapping::Rfc1459);
172 }
173}