Skip to main content

entrust_dialog/input/
validator.rs

1use crate::input::InputDialog;
2use std::borrow::Cow;
3use std::fmt::{Debug, Formatter};
4use std::ops::Add;
5
6pub trait ValidatorFn<'f>: 'f + Fn(&[char]) -> Option<Cow<'f, str>> {}
7impl<'f, F> ValidatorFn<'f> for F where F: 'f + Fn(&[char]) -> Option<Cow<'f, str>> {}
8
9pub struct Validator<'f> {
10    function: Box<dyn ValidatorFn<'f>>,
11}
12
13impl<'f> Validator<'f> {
14    pub fn new(function: impl ValidatorFn<'f>) -> Self {
15        Validator {
16            function: Box::new(function),
17        }
18    }
19    pub fn not_empty(message: &'f str) -> Self {
20        Validator::new(validate_not_empty(message))
21    }
22    pub fn filename() -> Validator<'static> {
23        Validator::new(validate_filename(false))
24    }
25    pub fn filename_cross_platform() -> Validator<'static> {
26        Validator::new(validate_filename(true))
27    }
28}
29
30pub fn validate_not_empty(message: &str) -> impl ValidatorFn<'_> {
31    |chars| {
32        if chars.is_empty() {
33            Some(Cow::Borrowed(message))
34        } else {
35            None
36        }
37    }
38}
39
40pub fn validate_filename(cross_platform: bool) -> impl ValidatorFn<'static> {
41    const WINDOWS_ILLEGAL_CHARS: &str = r#":*?"<>|"#;
42    move |chars| {
43        if chars.is_empty() {
44            return Some("Filename must not be empty".into());
45        }
46        if chars.last() == Some(&'/') {
47            return Some("Filename must not end with '/'".into());
48        }
49        if cross_platform || cfg!(windows) {
50            let contains_invalid = chars
51                .iter()
52                .any(|char| WINDOWS_ILLEGAL_CHARS.contains(*char));
53            if contains_invalid {
54                return Some(format!("Filename must not contain any of the following characters: {WINDOWS_ILLEGAL_CHARS}").into());
55            }
56        }
57        if cross_platform || cfg!(unix) {
58            let bytes_len = chars.iter().fold(0, |acc, e| acc + e.len_utf8());
59            if bytes_len > 255 {
60                return Some("Filename must not be longer than 255 bytes".into());
61            }
62        }
63        None
64    }
65}
66
67impl<'f, F> From<F> for Validator<'f>
68where
69    F: ValidatorFn<'f>,
70{
71    fn from(value: F) -> Self {
72        Validator::new(value)
73    }
74}
75
76impl Default for Validator<'_> {
77    fn default() -> Self {
78        Validator::new(|_| None)
79    }
80}
81
82impl<'f> Add for Validator<'f> {
83    type Output = Validator<'f>;
84
85    fn add(self, rhs: Self) -> Self::Output {
86        Validator::new(combine(self.function, rhs.function))
87    }
88}
89
90pub fn combine<'a>(
91    val_fn_1: impl ValidatorFn<'a>,
92    val_fn_2: impl ValidatorFn<'a>,
93) -> impl ValidatorFn<'a> {
94    move |chars| val_fn_1(chars).or(val_fn_2(chars))
95}
96
97impl<'p, 'c> InputDialog<'p, 'c> {
98    pub fn validation_message(&self) -> Option<Cow<'static, str>> {
99        (self.validator.function)(&self.content)
100    }
101}
102
103impl Debug for Validator<'_> {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        write!(f, "Validator")
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::dialog::{Dialog, DialogState};
113    use crate::input::Update;
114    use crate::input::Update::InsertChar;
115
116    #[test]
117    fn test_not_empty() {
118        let mut state =
119            InputDialog::default().with_validator(Validator::not_empty("must not be empty"));
120
121        assert_eq!(Some("must not be empty".into()), state.validation_message());
122
123        state.perform_update(Update::Confirm).unwrap();
124        assert_eq!(DialogState::Pending, state.state);
125
126        state.perform_update(InsertChar('a')).unwrap();
127        assert_eq!(None, state.validation_message());
128
129        state.perform_update(Update::Confirm).unwrap();
130        assert_eq!(DialogState::Completed, state.state);
131    }
132}