1use crate::common;
2use lazy_static::lazy_static;
3use precis_core::profile::stabilize;
4use precis_core::profile::{PrecisFastInvocation, Profile, Rules};
5use precis_core::Error;
6use precis_core::{FreeformClass, StringClass};
7use std::borrow::Cow;
8
9fn find_disallowed_space(label: &str) -> Option<usize> {
20 let mut begin = true;
21 let mut prev_space = false;
22 let mut last_c: Option<char> = None;
23 let mut offset = 0;
24
25 for (index, c) in label.chars().enumerate() {
26 offset = index;
27 if !common::is_space_separator(c) {
28 last_c = Some(c);
29 prev_space = false;
30 begin = false;
31 continue;
32 }
33
34 if begin {
35 return Some(index);
37 }
38
39 if prev_space {
40 return Some(index);
42 }
43
44 if c == common::SPACE {
45 prev_space = true;
46 last_c = Some(c);
47 } else {
48 return Some(index);
50 }
51 }
52
53 if let Some(common::SPACE) = last_c {
54 Some(offset)
56 } else {
57 None
60 }
61}
62
63fn trim_spaces<'a, T>(s: T) -> Result<Cow<'a, str>, Error>
79where
80 T: Into<Cow<'a, str>>,
81{
82 let s = s.into();
83 match find_disallowed_space(&s) {
84 None => Ok(s),
85 Some(pos) => {
86 let mut res = String::from(&s[..pos]);
87 res.reserve(s.len() - res.len());
88 let mut begin = true;
89 let mut prev_space = false;
90 for c in s[pos..].chars() {
91 if !common::is_space_separator(c) {
92 res.push(c);
93 prev_space = false;
94 begin = false;
95 continue;
96 }
97
98 if begin {
99 continue;
101 }
102
103 if !prev_space {
104 res.push(common::SPACE);
105 }
106
107 prev_space = true;
108 }
109 if let Some(c) = res.pop() {
111 if c != common::SPACE {
112 res.push(c);
113 }
114 }
115 Ok(res.into())
116 }
117 }
118}
119
120#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
147pub struct Nickname(FreeformClass);
148
149impl Nickname {
150 pub fn new() -> Self {
152 Self(FreeformClass::default())
153 }
154
155 fn apply_prepare_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
156 where
157 T: Into<Cow<'a, str>>,
158 {
159 let s = s.into();
160 let s = (!s.is_empty()).then_some(s).ok_or(Error::Invalid)?;
161 self.0.allows(&s)?;
162 Ok(s)
163 }
164
165 fn apply_enforce_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
166 where
167 T: Into<Cow<'a, str>>,
168 {
169 let s = self.apply_prepare_rules(s)?;
170 let s = self.additional_mapping_rule(s)?;
171 let s = self.normalization_rule(s)?;
172 (!s.is_empty()).then_some(s).ok_or(Error::Invalid)
173 }
174
175 fn apply_compare_rules<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
176 where
177 T: Into<Cow<'a, str>>,
178 {
179 let s = self.apply_prepare_rules(s)?;
180 let s = self.additional_mapping_rule(s)?;
181 let s = self.case_mapping_rule(s)?;
182 self.normalization_rule(s)
183 }
184}
185
186impl Profile for Nickname {
187 fn prepare<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
188 where
189 S: Into<Cow<'a, str>>,
190 {
191 self.apply_prepare_rules(s)
192 }
193
194 fn enforce<'a, S>(&self, s: S) -> Result<Cow<'a, str>, Error>
195 where
196 S: Into<Cow<'a, str>>,
197 {
198 stabilize(s, |s| self.apply_enforce_rules(s))
199 }
200
201 fn compare<A, B>(&self, s1: A, s2: B) -> Result<bool, Error>
202 where
203 A: AsRef<str>,
204 B: AsRef<str>,
205 {
206 Ok(stabilize(s1.as_ref(), |s| self.apply_compare_rules(s))?
207 == stabilize(s2.as_ref(), |s| self.apply_compare_rules(s))?)
208 }
209}
210
211impl Rules for Nickname {
212 fn additional_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
213 where
214 T: Into<Cow<'a, str>>,
215 {
216 trim_spaces(s)
217 }
218
219 fn case_mapping_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
220 where
221 T: Into<Cow<'a, str>>,
222 {
223 common::case_mapping_rule(s)
224 }
225
226 fn normalization_rule<'a, T>(&self, s: T) -> Result<Cow<'a, str>, Error>
227 where
228 T: Into<Cow<'a, str>>,
229 {
230 common::normalization_form_nfkc(s)
231 }
232}
233
234fn get_nickname_profile() -> &'static Nickname {
235 lazy_static! {
236 static ref NICKNAME: Nickname = Nickname::default();
237 }
238 &NICKNAME
239}
240
241impl PrecisFastInvocation for Nickname {
242 fn prepare<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
243 where
244 S: Into<Cow<'a, str>>,
245 {
246 get_nickname_profile().prepare(s)
247 }
248
249 fn enforce<'a, S>(s: S) -> Result<Cow<'a, str>, Error>
250 where
251 S: Into<Cow<'a, str>>,
252 {
253 get_nickname_profile().enforce(s)
254 }
255
256 fn compare<A, B>(s1: A, s2: B) -> Result<bool, Error>
257 where
258 A: AsRef<str>,
259 B: AsRef<str>,
260 {
261 get_nickname_profile().compare(s1, s2)
262 }
263}
264
265#[cfg(test)]
266mod test_nicknames {
267 use crate::nicknames::*;
268
269 #[test]
270 fn test_find_disallowed_space() {
271 assert_eq!(find_disallowed_space(""), None);
272 assert_eq!(find_disallowed_space("test"), None);
273 assert_eq!(find_disallowed_space("test "), Some(4));
274 assert_eq!(find_disallowed_space("test good"), None);
275
276 assert_eq!(find_disallowed_space(" test"), Some(0));
278 assert_eq!(find_disallowed_space("t est"), Some(2));
279
280 assert_eq!(find_disallowed_space(" test"), Some(0));
282
283 assert_eq!(find_disallowed_space("\u{00a0}test"), Some(0));
285 assert_eq!(find_disallowed_space("te\u{00a0}st"), Some(2));
286 assert_eq!(find_disallowed_space("test\u{00a0}"), Some(4));
287 }
288
289 #[test]
290 fn test_trim_spaces() {
291 assert_eq!(trim_spaces(" "), Ok(Cow::from("")));
293 assert_eq!(trim_spaces(" test"), Ok(Cow::from("test")));
294 assert_eq!(trim_spaces("test "), Ok(Cow::from("test")));
295
296 assert_eq!(trim_spaces("hello world"), Ok(Cow::from("hello world")));
297
298 assert_eq!(trim_spaces(""), Ok(Cow::from("")));
299 assert_eq!(trim_spaces(" test"), Ok(Cow::from("test")));
300 assert_eq!(trim_spaces("test "), Ok(Cow::from("test")));
301 assert_eq!(
302 trim_spaces(" hello world "),
303 Ok(Cow::from("hello world"))
304 );
305
306 assert_eq!(trim_spaces("\u{205f}test\u{205f}"), Ok(Cow::from("test")));
308 assert_eq!(
309 trim_spaces("\u{205f}\u{205f}hello\u{205f}\u{205f}world\u{205f}\u{205f}"),
310 Ok(Cow::from("hello world"))
311 );
312
313 assert_eq!(trim_spaces(" \u{205f}test\u{205f} "), Ok(Cow::from("test")));
315 assert_eq!(
316 trim_spaces("\u{205f} hello \u{205f} world \u{205f} "),
317 Ok(Cow::from("hello world"))
318 );
319 }
320
321 #[test]
322 fn nick_name_profile() {
323 let profile = Nickname::new();
324
325 let res = profile.prepare("Foo Bar");
326 assert_eq!(res, Ok(Cow::from("Foo Bar")));
327
328 let res = profile.enforce("Foo Bar");
329 assert_eq!(res, Ok(Cow::from("Foo Bar")));
330
331 let res = profile.compare("Foo Bar", "foo bar");
332 assert_eq!(res, Ok(true));
333 }
334}