entrust_dialog/input/
validator.rs1use 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}