mail4agent_messenger_shell/
nick.rs1use crate::ShellError;
9
10pub fn nick_from_display_name(display_name: &str) -> Result<String, ShellError> {
22 let mut out = String::new();
23 let mut pending_dash = false;
24 for ch in display_name.chars() {
25 match latin_piece(ch) {
26 Some("") => {}
27 Some(piece) => {
28 if pending_dash && !out.is_empty() {
29 out.push('-');
30 }
31 pending_dash = false;
32 out.push_str(piece);
33 }
34 None => pending_dash = true,
35 }
36 }
37 let nick: String = out.chars().take(32).collect();
38 let nick = nick.trim_end_matches('-').to_string();
39 if !is_nick_token(&nick) {
40 return Err(ShellError::Nick);
41 }
42 Ok(nick)
43}
44
45pub fn routine_folder_id(name: &str) -> Option<String> {
61 let mut slug = String::new();
62 let mut pending_dash = false;
63 for ch in name.to_lowercase().chars() {
64 if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
65 if pending_dash && !slug.is_empty() {
66 slug.push('-');
67 }
68 pending_dash = false;
69 slug.push(ch);
70 } else {
71 pending_dash = true;
72 }
73 }
74 let slug: String = slug.chars().take(48).collect();
76 if slug.is_empty() {
77 None
78 } else {
79 Some(slug)
80 }
81}
82
83pub fn lookup_nick(raw: &str) -> Result<String, ShellError> {
88 let trimmed = raw.trim();
89 if is_nick_token(trimmed) {
90 return Ok(trimmed.to_string());
91 }
92 nick_from_display_name(trimmed)
93}
94
95fn is_nick_token(nick: &str) -> bool {
96 !nick.is_empty()
97 && nick.len() <= 32
98 && nick
99 .chars()
100 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
101}
102
103fn latin_piece(ch: char) -> Option<&'static str> {
104 if ch.is_ascii_alphanumeric() {
105 return Some(ascii_lower(ch));
106 }
107 let lower = ch.to_lowercase().next()?;
108 Some(match lower {
109 'а' => "a",
110 'б' => "b",
111 'в' => "v",
112 'г' => "g",
113 'д' => "d",
114 'е' => "e",
115 'ё' => "yo",
116 'ж' => "zh",
117 'з' => "z",
118 'и' => "i",
119 'й' => "i",
120 'к' => "k",
121 'л' => "l",
122 'м' => "m",
123 'н' => "n",
124 'о' => "o",
125 'п' => "p",
126 'р' => "r",
127 'с' => "s",
128 'т' => "t",
129 'у' => "u",
130 'ф' => "f",
131 'х' => "kh",
132 'ц' => "ts",
133 'ч' => "ch",
134 'ш' => "sh",
135 'щ' => "shch",
136 'ъ' | 'ь' => "",
137 'ы' => "y",
138 'э' => "e",
139 'ю' => "yu",
140 'я' => "ya",
141 'є' => "ye",
143 'і' => "i",
144 'ї' => "yi",
145 'ґ' => "g",
146 'ў' => "u",
147 _ => return None,
148 })
149}
150
151fn ascii_lower(ch: char) -> &'static str {
152 match ch {
153 'A' | 'a' => "a",
154 'B' | 'b' => "b",
155 'C' | 'c' => "c",
156 'D' | 'd' => "d",
157 'E' | 'e' => "e",
158 'F' | 'f' => "f",
159 'G' | 'g' => "g",
160 'H' | 'h' => "h",
161 'I' | 'i' => "i",
162 'J' | 'j' => "j",
163 'K' | 'k' => "k",
164 'L' | 'l' => "l",
165 'M' | 'm' => "m",
166 'N' | 'n' => "n",
167 'O' | 'o' => "o",
168 'P' | 'p' => "p",
169 'Q' | 'q' => "q",
170 'R' | 'r' => "r",
171 'S' | 's' => "s",
172 'T' | 't' => "t",
173 'U' | 'u' => "u",
174 'V' | 'v' => "v",
175 'W' | 'w' => "w",
176 'X' | 'x' => "x",
177 'Y' | 'y' => "y",
178 'Z' | 'z' => "z",
179 '0' => "0",
180 '1' => "1",
181 '2' => "2",
182 '3' => "3",
183 '4' => "4",
184 '5' => "5",
185 '6' => "6",
186 '7' => "7",
187 '8' => "8",
188 '9' => "9",
189 _ => "",
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn display_names_become_nicks() {
199 assert_eq!(nick_from_display_name("Alice").unwrap(), "alice");
200 assert_eq!(nick_from_display_name(" Alice ").unwrap(), "alice");
201 assert_eq!(
202 nick_from_display_name("Привет мир").unwrap(),
203 "privet-mir"
204 );
205 assert_eq!(
206 nick_from_display_name("Свой браузер").unwrap(),
207 "svoi-brauzer"
208 );
209 assert_eq!(
210 nick_from_display_name("foo+bar").unwrap(),
211 "foo-bar"
212 );
213 assert_eq!(nick_from_display_name("Sample Bot").unwrap(), "sample-bot");
214 assert_eq!(
215 nick_from_display_name("Объём, данные").unwrap(),
216 "obyom-dannye"
217 );
218 assert_eq!(
219 nick_from_display_name(&("x".repeat(31) + " y")).unwrap(),
220 "x".repeat(31)
221 );
222 assert_ne!(
223 nick_from_display_name("Привет мир").unwrap(),
224 "nachshtab"
225 );
226 assert_eq!(nick_from_display_name("Foo Bar!!").unwrap(), "foo-bar");
227 assert_eq!(nick_from_display_name("__a__b__").unwrap(), "a-b");
228 assert_eq!(
229 nick_from_display_name(&"A".repeat(40)).unwrap(),
230 "a".repeat(32)
231 );
232 assert!(nick_from_display_name("...").is_err());
233 assert_eq!(lookup_nick("Привет мир").unwrap(), "privet-mir");
234 assert_eq!(lookup_nick("chief_bob").unwrap(), "chief_bob");
235 assert_eq!(lookup_nick("Alice").unwrap(), "Alice");
236 }
237
238 #[test]
239 fn routine_folder_matches_the_host_slug() {
240 assert_eq!(routine_folder_id("alice").as_deref(), Some("alice"));
242 assert_eq!(
243 routine_folder_id("chief_bob").as_deref(),
244 Some("chief-bob")
245 );
246 assert_eq!(
247 routine_folder_id("sample_bot").as_deref(),
248 Some("sample-bot")
249 );
250 assert_eq!(
251 routine_folder_id("foobar").as_deref(),
252 Some("foobar")
253 );
254 assert_eq!(
255 routine_folder_id("foo+bar").as_deref(),
256 Some("foo-bar")
257 );
258 assert_eq!(routine_folder_id("Alice").as_deref(), Some("alice"));
259 assert_eq!(routine_folder_id("__a__b__").as_deref(), Some("a-b"));
260 assert_eq!(routine_folder_id("Привет"), None);
261 assert_eq!(routine_folder_id("___"), None);
262 assert_eq!(
263 routine_folder_id(&"x".repeat(60)).map(|s| s.len()),
264 Some(48)
265 );
266 assert_eq!(
267 routine_folder_id(&format!("{}_b", "a".repeat(47))).as_deref(),
268 Some(format!("{}-", "a".repeat(47)).as_str())
269 );
270 let chain = |name: &str| routine_folder_id(&nick_from_display_name(name).unwrap());
272 for name in [
273 "Alice",
274 "Привет мир",
275 "foo+bar",
276 "Sample Bot",
277 "carol",
278 ] {
279 let nick = nick_from_display_name(name).unwrap();
280 assert_eq!(routine_folder_id(&nick).as_deref(), Some(nick.as_str()));
281 }
282 assert_eq!(
283 chain("Привет мир").as_deref(),
284 Some("privet-mir")
285 );
286 assert_eq!(chain("foo+bar").as_deref(), Some("foo-bar"));
287 assert_eq!(chain("Sample Bot").as_deref(), Some("sample-bot"));
288 assert_eq!(chain("carol").as_deref(), Some("carol"));
289 }
290}