Skip to main content

tui_lipan/validation/
mod.rs

1#![allow(clippy::type_complexity)]
2
3//! Composable validation for form fields.
4//!
5//! [`Validator<T>`] collects ordered *rules* that each return
6//! `Result<(), ValidationError>`.  Rules are added with the builder pattern
7//! and evaluated left-to-right.
8//!
9//! ```
10//! use tui_lipan::validation::{StringValidator, ValidationError};
11//!
12//! let v = StringValidator::new()
13//!     .required("Name is required")
14//!     .min_length(3, "At least 3 characters");
15//!
16//! assert!(v.validate("ab").is_err());
17//! assert!(v.validate("abc").is_ok());
18//! ```
19
20use std::rc::Rc;
21use std::sync::Arc;
22
23// ---------------------------------------------------------------------------
24// ValidationError
25// ---------------------------------------------------------------------------
26
27/// A single validation failure carrying a human-readable message.
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub struct ValidationError {
30    /// Human-readable description of what went wrong.
31    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
42// ---------------------------------------------------------------------------
43// Rule / Validator
44// ---------------------------------------------------------------------------
45
46/// A single validation rule: a closure that inspects a value and returns
47/// `Err(ValidationError)` on failure.
48type Rule<T> = Rc<dyn Fn(&T) -> Result<(), ValidationError>>;
49
50/// An ordered, cloneable collection of validation rules.
51///
52/// Add rules with the builder pattern (`rule`, `required`, `min_length`, …)
53/// and evaluate with [`Validator::validate`] (first failure) or
54/// [`Validator::validate_all`] (all failures).
55pub struct Validator<T: ?Sized> {
56    rules: Vec<Rule<T>>,
57}
58
59// `Rule<T>` is `Rc<dyn Fn>`, which is not `Debug`.  Provide a lightweight
60// implementation that just reports the rule count.
61impl<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    /// Create an empty validator (always validates successfully).
79    pub fn new() -> Self {
80        Self { rules: Vec::new() }
81    }
82
83    /// Add a custom validation rule (builder pattern).
84    ///
85    /// The closure receives a reference to the value and should return
86    /// `Err(ValidationError)` on failure.
87    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    /// Validate the value, returning the **first** failure (if any).
94    ///
95    /// Rules are evaluated left-to-right; evaluation stops at the first
96    /// error.
97    pub fn validate(&self, value: &T) -> Result<(), ValidationError> {
98        for rule in &self.rules {
99            rule(value)?;
100        }
101        Ok(())
102    }
103
104    /// Validate the value, collecting **all** failures.
105    ///
106    /// Every rule is evaluated regardless of earlier failures.
107    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
121// ---------------------------------------------------------------------------
122// StringValidator helpers
123// ---------------------------------------------------------------------------
124
125/// Convenience alias for the most common validator kind.
126pub type StringValidator = Validator<str>;
127
128impl Validator<str> {
129    /// Reject empty or whitespace-only strings.
130    ///
131    /// A string is considered *empty* when `trim()` yields an empty string.
132    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    /// Reject strings shorter than `n` characters.
146    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    /// Reject strings longer than `n` characters.
160    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// ---------------------------------------------------------------------------
175// Tests
176// ---------------------------------------------------------------------------
177
178#[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        // "ab" passes required but fails min_length
246        let err = v.validate("ab").unwrap_err();
247        assert_eq!(&*err.message, "min 3");
248        // "hello" passes all
249        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}