anni_common/
validator.rs

1use anni_artist::ArtistList;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::de::Error;
5use serde::{Deserialize, Deserializer};
6use std::str::FromStr;
7
8pub struct Validator(&'static str, fn(&str) -> ValidateResult);
9
10impl Validator {
11    #[inline]
12    pub fn name(&self) -> &'static str {
13        self.0
14    }
15
16    #[inline]
17    pub fn validate(&self, input: &str) -> ValidateResult {
18        self.1(input)
19    }
20}
21
22impl FromStr for Validator {
23    type Err = ();
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s {
27            "number" => Ok(Self("number", number_validator)),
28            "trim" => Ok(Self("trim", trim_validator)),
29            "date" => Ok(Self("date", date_validator)),
30            "artist" => Ok(Self("artist", artist_validator)),
31            "dot" => Ok(Self("dot", middle_dot_validator)),
32            "tidle" => Ok(Self("tidle", tidal_validator)),
33            _ => Err(()),
34        }
35    }
36}
37
38impl<'de> Deserialize<'de> for Validator {
39    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40    where
41        D: Deserializer<'de>,
42    {
43        let s = String::deserialize(deserializer)?;
44        Validator::from_str(s.as_str()).map_err(|_| D::Error::custom(s))
45    }
46}
47
48impl std::fmt::Debug for Validator {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(f, "{}", self.name())
51    }
52}
53
54#[derive(Default, Debug, Deserialize)]
55#[serde(transparent)]
56pub struct ValidatorList(Vec<Validator>);
57
58impl ValidatorList {
59    pub fn new(validators: &[&str]) -> Option<Self> {
60        validators
61            .iter()
62            .map(|v| Validator::from_str(v))
63            .collect::<Result<_, _>>()
64            .map(ValidatorList)
65            .ok()
66    }
67
68    pub fn validate(&self, input: &str) -> Vec<(&'static str, ValidateResult)> {
69        self.0
70            .iter()
71            .map(|v| (v.0, v.1(input)))
72            .filter(|v| !v.1.is_pass())
73            .collect()
74    }
75}
76
77pub enum ValidateResult {
78    Pass,
79    Warning(String),
80    Error(String),
81}
82
83impl ValidateResult {
84    fn pass() -> Self {
85        Self::Pass
86    }
87
88    fn pass_or(pass: bool, message: String) -> Self {
89        if pass {
90            Self::Pass
91        } else {
92            Self::Error(message)
93        }
94    }
95
96    fn warn(message: String) -> Self {
97        Self::Warning(message)
98    }
99
100    fn fail(message: String) -> Self {
101        Self::Error(message)
102    }
103
104    pub fn is_pass(&self) -> bool {
105        matches!(self, Self::Pass)
106    }
107
108    pub fn into_message(self) -> String {
109        match self {
110            Self::Warning(m) => m,
111            Self::Error(m) => m,
112            Self::Pass => unreachable!(),
113        }
114    }
115}
116
117pub fn number_validator(str: &str) -> ValidateResult {
118    let pass = str.chars().all(|c| c.is_numeric());
119    ValidateResult::pass_or(pass, "not a number".to_string())
120}
121
122pub fn trim_validator(str: &str) -> ValidateResult {
123    let mut is_start = true;
124    let mut is_empty = false;
125    for c in str.chars() {
126        is_empty = c.is_whitespace();
127        if is_start && is_empty {
128            break;
129        }
130        is_start = false;
131    }
132    let pass = !is_empty;
133    ValidateResult::pass_or(pass, "whitespaces need to be trimmed".to_string())
134}
135
136pub fn date_validator(str: &str) -> ValidateResult {
137    // 2021-01-01
138    // 0123456789
139    let mut mode = 0;
140    for c in str.chars() {
141        if mode > 9 || (!c.is_numeric() && c != '-') {
142            return ValidateResult::fail("invalid date".to_string());
143        }
144        if c == '-' {
145            if mode != 4 && mode != 7 {
146                return ValidateResult::fail("invalid date".to_string());
147            }
148        } else if !c.is_numeric() {
149            return ValidateResult::fail("invalid date".to_string());
150        }
151        mode += 1;
152    }
153    let is_year_month_day = mode == 10;
154    let is_year_month = mode == 7;
155    let is_year = mode == 4;
156    if is_year_month_day {
157        ValidateResult::pass()
158    } else if is_year_month {
159        ValidateResult::warn("Empty day field, could it be more accurate?".to_string())
160    } else if is_year {
161        ValidateResult::warn("Empty month and day fields, could it be more accurate?".to_string())
162    } else {
163        ValidateResult::fail("invalid date".to_string())
164    }
165}
166
167pub fn artist_validator(str: &str) -> ValidateResult {
168    match ArtistList::parse(str) {
169        Ok(_) => ValidateResult::pass(),
170        Err(err) => {
171            log::debug!("ArtistList parse error: {}", err);
172            ValidateResult::fail(err)
173        }
174    }
175}
176
177static DOTS: Lazy<Regex> = Lazy::new(|| {
178    Regex::new(r"[\u{00B7}\u{0387}\u{16eb}\u{2022}\u{2027}\u{2218}\u{2219}\u{22c5}\u{25e6}\u{2981}\u{2e30}\u{2e31}\u{ff65}\u{10101}]").unwrap()
179});
180
181/// http://www.0x08.org/posts/middle-dot
182pub fn middle_dot_validator(input: &str) -> ValidateResult {
183    let pass = !DOTS.is_match(input);
184    ValidateResult::pass_or(pass, "invalid dots detected".to_string())
185}
186
187pub fn middle_dot_replace(input: &str) -> String {
188    DOTS.replace_all(input, "\u{30fb}").to_string()
189}
190
191pub fn tidal_validator(input: &str) -> ValidateResult {
192    let pass = !input.contains('\u{301c}');
193    ValidateResult::pass_or(pass, "invalid tidal detected".to_string())
194}
195
196pub fn tidal_replace(input: &str) -> String {
197    input.replace('\u{301c}', "\u{ff5e}")
198}
199
200#[cfg(test)]
201mod tests {
202    use crate::validator::{
203        date_validator, middle_dot_replace, middle_dot_validator, trim_validator, ValidateResult,
204    };
205
206    #[test]
207    fn trim_exist() {
208        assert!(matches!(trim_validator("  1234"), ValidateResult::Error(_)));
209        assert!(matches!(
210            trim_validator("1234   "),
211            ValidateResult::Error(_)
212        ));
213        assert!(matches!(trim_validator("\n1234"), ValidateResult::Error(_)));
214    }
215
216    #[test]
217    fn trim_not_exist() {
218        assert!(matches!(trim_validator("1234"), ValidateResult::Pass));
219    }
220
221    #[test]
222    fn test_date_validator() {
223        assert!(matches!(date_validator("2021-01-01"), ValidateResult::Pass));
224
225        assert!(matches!(
226            date_validator("2020-01-012"),
227            ValidateResult::Error(_)
228        ));
229        assert!(matches!(
230            date_validator("2020~01-01"),
231            ValidateResult::Error(_)
232        ));
233        assert!(matches!(date_validator("?"), ValidateResult::Error(_)));
234    }
235
236    #[test]
237    fn middle_dot_detect() {
238        assert!(matches!(middle_dot_validator("123"), ValidateResult::Pass));
239
240        assert!(matches!(
241            middle_dot_validator("\u{00B7}"),
242            ValidateResult::Error(_)
243        ));
244        assert!(matches!(
245            middle_dot_validator("\u{0387}"),
246            ValidateResult::Error(_)
247        ));
248        assert!(matches!(
249            middle_dot_validator("\u{16eb}"),
250            ValidateResult::Error(_)
251        ));
252        assert!(matches!(
253            middle_dot_validator("\u{2022}"),
254            ValidateResult::Error(_)
255        ));
256        assert!(matches!(
257            middle_dot_validator("\u{2027}"),
258            ValidateResult::Error(_)
259        ));
260        assert!(matches!(
261            middle_dot_validator("\u{2218}"),
262            ValidateResult::Error(_)
263        ));
264        assert!(matches!(
265            middle_dot_validator("\u{2219}"),
266            ValidateResult::Error(_)
267        ));
268        assert!(matches!(
269            middle_dot_validator("\u{22c5}"),
270            ValidateResult::Error(_)
271        ));
272        assert!(matches!(
273            middle_dot_validator("\u{25e6}"),
274            ValidateResult::Error(_)
275        ));
276        assert!(matches!(
277            middle_dot_validator("\u{2981}"),
278            ValidateResult::Error(_)
279        ));
280        assert!(matches!(
281            middle_dot_validator("\u{2e30}"),
282            ValidateResult::Error(_)
283        ));
284        assert!(matches!(
285            middle_dot_validator("\u{2e31}"),
286            ValidateResult::Error(_)
287        ));
288        assert!(matches!(
289            middle_dot_validator("\u{ff65}"),
290            ValidateResult::Error(_)
291        ));
292        assert!(matches!(
293            middle_dot_validator("\u{10101}"),
294            ValidateResult::Error(_)
295        ));
296    }
297
298    #[test]
299    fn middle_dot_replace_all() {
300        assert_eq!(
301            middle_dot_replace("1\u{00B7}2\u{0387}3\u{16eb}4\u{2022}5\u{2027}6\u{2218}7\u{2219}8\u{22c5}9\u{25e6}1\u{2981}2\u{2e30}3\u{2e31}4\u{ff65}5\u{10101}6"),
302            "1・2・3・4・5・6・7・8・9・1・2・3・4・5・6"
303        );
304    }
305}