tui_lipan/validation/
mod.rs1#![allow(clippy::type_complexity)]
2
3use std::rc::Rc;
21use std::sync::Arc;
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub struct ValidationError {
30 pub message: Arc<str>,
32}
33
34impl std::fmt::Display for ValidationError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.write_str(&self.message)
37 }
38}
39
40impl std::error::Error for ValidationError {}
41
42type Rule<T> = Rc<dyn Fn(&T) -> Result<(), ValidationError>>;
49
50pub struct Validator<T: ?Sized> {
56 rules: Vec<Rule<T>>,
57}
58
59impl<T: ?Sized> std::fmt::Debug for Validator<T> {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("Validator")
64 .field("rules", &self.rules.len())
65 .finish()
66 }
67}
68
69impl<T: ?Sized> Clone for Validator<T> {
70 fn clone(&self) -> Self {
71 Self {
72 rules: self.rules.clone(),
73 }
74 }
75}
76
77impl<T: ?Sized> Validator<T> {
78 pub fn new() -> Self {
80 Self { rules: Vec::new() }
81 }
82
83 pub fn rule(self, check: impl Fn(&T) -> Result<(), ValidationError> + 'static) -> Self {
88 let mut this = self;
89 this.rules.push(Rc::new(check));
90 this
91 }
92
93 pub fn validate(&self, value: &T) -> Result<(), ValidationError> {
98 for rule in &self.rules {
99 rule(value)?;
100 }
101 Ok(())
102 }
103
104 pub fn validate_all(&self, value: &T) -> Vec<ValidationError> {
108 self.rules
109 .iter()
110 .filter_map(|rule| rule(value).err())
111 .collect()
112 }
113}
114
115impl<T: ?Sized> Default for Validator<T> {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121pub type StringValidator = Validator<str>;
127
128impl Validator<str> {
129 pub fn required(self, msg: impl Into<Arc<str>>) -> Self {
133 let msg = msg.into();
134 self.rule(move |s: &str| {
135 if s.trim().is_empty() {
136 Err(ValidationError {
137 message: msg.clone(),
138 })
139 } else {
140 Ok(())
141 }
142 })
143 }
144
145 pub fn min_length(self, n: usize, msg: impl Into<Arc<str>>) -> Self {
147 let msg = msg.into();
148 self.rule(move |s: &str| {
149 if s.chars().count() < n {
150 Err(ValidationError {
151 message: msg.clone(),
152 })
153 } else {
154 Ok(())
155 }
156 })
157 }
158
159 pub fn max_length(self, n: usize, msg: impl Into<Arc<str>>) -> Self {
161 let msg = msg.into();
162 self.rule(move |s: &str| {
163 if s.chars().count() > n {
164 Err(ValidationError {
165 message: msg.clone(),
166 })
167 } else {
168 Ok(())
169 }
170 })
171 }
172}
173
174#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn required_rejects_empty() {
184 let v = StringValidator::new().required(Arc::from("required"));
185 assert!(v.validate("").is_err());
186 assert!(v.validate(" ").is_err());
187 }
188
189 #[test]
190 fn required_accepts_non_empty() {
191 let v = StringValidator::new().required(Arc::from("required"));
192 assert!(v.validate("hello").is_ok());
193 }
194
195 #[test]
196 fn min_length_rejects_short() {
197 let v = StringValidator::new().min_length(3, Arc::from("too short"));
198 assert!(v.validate("ab").is_err());
199 }
200
201 #[test]
202 fn min_length_accepts_exact() {
203 let v = StringValidator::new().min_length(3, Arc::from("too short"));
204 assert!(v.validate("abc").is_ok());
205 }
206
207 #[test]
208 fn max_length_rejects_long() {
209 let v = StringValidator::new().max_length(5, Arc::from("too long"));
210 assert!(v.validate("abcdef").is_err());
211 }
212
213 #[test]
214 fn max_length_accepts_exact() {
215 let v = StringValidator::new().max_length(5, Arc::from("too long"));
216 assert!(v.validate("abcde").is_ok());
217 }
218
219 #[test]
220 fn validate_returns_first_failure() {
221 let v = StringValidator::new()
222 .required(Arc::from("required"))
223 .min_length(3, Arc::from("too short"));
224 let err = v.validate("").unwrap_err();
225 assert_eq!(&*err.message, "required");
226 }
227
228 #[test]
229 fn validate_all_collects_all_failures() {
230 let v = StringValidator::new()
231 .min_length(5, Arc::from("too short"))
232 .max_length(2, Arc::from("too long"));
233 let errs = v.validate_all("abc");
234 assert_eq!(errs.len(), 2);
235 assert_eq!(&*errs[0].message, "too short");
236 assert_eq!(&*errs[1].message, "too long");
237 }
238
239 #[test]
240 fn chained_rules_compose_left_to_right() {
241 let v = StringValidator::new()
242 .required(Arc::from("required"))
243 .min_length(3, Arc::from("min 3"))
244 .max_length(10, Arc::from("max 10"));
245 let err = v.validate("ab").unwrap_err();
247 assert_eq!(&*err.message, "min 3");
248 assert!(v.validate("hello").is_ok());
250 }
251
252 #[test]
253 fn empty_validator_always_succeeds() {
254 let v: StringValidator = Validator::new();
255 assert!(v.validate("").is_ok());
256 assert!(v.validate("anything").is_ok());
257 assert!(v.validate_all("").is_empty());
258 }
259}