fluentval/
lib.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3
4/// Represents a validation error with a property name and error message
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ValidationError {
7    pub property: String,
8    pub message: String,
9}
10
11impl ValidationError {
12    pub fn new(property: impl Into<String>, message: impl Into<String>) -> Self {
13        Self {
14            property: property.into(),
15            message: message.into(),
16        }
17    }
18}
19
20impl Display for ValidationError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "{}: {}", self.property, self.message)
23    }
24}
25
26/// Result of validation containing errors if validation failed
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ValidationResult {
29    errors: Vec<ValidationError>,
30}
31
32impl ValidationResult {
33    /// Create a new empty validation result
34    pub fn new() -> Self {
35        Self { errors: Vec::new() }
36    }
37
38    /// Add a validation error
39    pub fn add_error(&mut self, error: ValidationError) {
40        self.errors.push(error);
41    }
42
43    /// Add multiple validation errors
44    pub fn add_errors(&mut self, errors: Vec<ValidationError>) {
45        self.errors.extend(errors);
46    }
47
48    /// Check if validation passed (no errors)
49    pub fn is_valid(&self) -> bool {
50        self.errors.is_empty()
51    }
52
53    /// Get all validation errors
54    pub fn errors(&self) -> &[ValidationError] {
55        &self.errors
56    }
57
58    /// Get errors grouped by property name
59    pub fn errors_by_property(&self) -> HashMap<String, Vec<String>> {
60        let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
61        for error in &self.errors {
62            grouped
63                .entry(error.property.clone())
64                .or_insert_with(Vec::new)
65                .push(error.message.clone());
66        }
67        grouped
68    }
69
70    /// Get the first error message for a property, if any
71    pub fn first_error_for(&self, property: &str) -> Option<&str> {
72        self.errors
73            .iter()
74            .find(|e| e.property == property)
75            .map(|e| e.message.as_str())
76    }
77}
78
79impl Default for ValidationResult {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Trait for defining validators
86pub trait Validator<T> {
87    fn validate(&self, instance: &T) -> ValidationResult;
88}
89
90/// Rule function type that validates a value and returns an optional error message
91pub type Rule<T> = Box<dyn Fn(&T) -> Option<String>>;
92
93/// Builder for creating validation rules in a fluent style
94pub struct RuleBuilder<T> {
95    property_name: String,
96    rules: Vec<Rule<T>>,
97}
98
99impl<T> RuleBuilder<T> {
100    /// Create a new rule builder for a property
101    pub fn for_property(property_name: impl Into<String>) -> Self {
102        Self {
103            property_name: property_name.into(),
104            rules: Vec::new(),
105        }
106    }
107
108    /// Add a custom rule
109    pub fn rule(mut self, rule: impl Fn(&T) -> Option<String> + 'static) -> Self {
110        self.rules.push(Box::new(rule));
111        self
112    }
113
114    /// Validate that the value is not empty (for strings)
115    /// 
116    /// # Arguments
117    /// * `message` - Optional custom error message. If not provided, uses default message.
118    pub fn not_empty(self, message: Option<impl Into<String>>) -> Self
119    where
120        T: AsRef<str>,
121    {
122        let msg = message.map(|m| m.into()).unwrap_or_else(|| "must not be empty".to_string());
123        self.rule(move |value| {
124            if value.as_ref().trim().is_empty() {
125                Some(msg.clone())
126            } else {
127                None
128            }
129        })
130    }
131
132    /// Validate that the value is not null/empty (for Option types)
133    /// 
134    /// # Arguments
135    /// * `message` - Optional custom error message. If not provided, uses default message.
136    pub fn not_null(self, message: Option<impl Into<String>>) -> Self
137    where
138        T: OptionLike,
139    {
140        let msg = message.map(|m| m.into()).unwrap_or_else(|| "must not be null".to_string());
141        self.rule(move |value| {
142            if value.is_none() {
143                Some(msg.clone())
144            } else {
145                None
146            }
147        })
148    }
149
150    /// Validate minimum length
151    /// 
152    /// # Arguments
153    /// * `min` - Minimum length required
154    /// * `message` - Optional custom error message. If not provided, uses default message with the min value.
155    pub fn min_length(self, min: usize, message: Option<impl Into<String> + Clone + 'static>) -> Self
156    where
157        T: AsRef<str>,
158    {
159        let msg = message.map(|m| m.into());
160        self.rule(move |value| {
161            let len = value.as_ref().len();
162            if len < min {
163                Some(msg.clone().unwrap_or_else(|| format!("must be at least {} characters long", min)))
164            } else {
165                None
166            }
167        })
168    }
169
170    /// Validate maximum length
171    /// 
172    /// # Arguments
173    /// * `max` - Maximum length allowed
174    /// * `message` - Optional custom error message. If not provided, uses default message with the max value.
175    pub fn max_length(self, max: usize, message: Option<impl Into<String> + Clone + 'static>) -> Self
176    where
177        T: AsRef<str>,
178    {
179        let msg = message.map(|m| m.into());
180        self.rule(move |value| {
181            let len = value.as_ref().len();
182            if len > max {
183                Some(msg.clone().unwrap_or_else(|| format!("must be at most {} characters long", max)))
184            } else {
185                None
186            }
187        })
188    }
189
190    /// Validate length range
191    /// 
192    /// # Arguments
193    /// * `min` - Minimum length required
194    /// * `max` - Maximum length allowed
195    /// * `min_message` - Optional custom error message for minimum length violation
196    /// * `max_message` - Optional custom error message for maximum length violation
197    pub fn length(self, min: usize, max: usize, min_message: Option<impl Into<String> + Clone + 'static>, max_message: Option<impl Into<String> + Clone + 'static>) -> Self
198    where
199        T: AsRef<str>,
200    {
201        self.min_length(min, min_message).max_length(max, max_message)
202    }
203
204    /// Validate email format
205    /// 
206    /// # Arguments
207    /// * `message` - Optional custom error message. If not provided, uses default message.
208    pub fn email(self, message: Option<impl Into<String>>) -> Self
209    where
210        T: AsRef<str>,
211    {
212        let msg = message.map(|m| m.into()).unwrap_or_else(|| "must be a valid email address".to_string());
213        self.rule(move |value| {
214            let email_regex = regex::Regex::new(
215                r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
216            )
217            .unwrap();
218            if !email_regex.is_match(value.as_ref()) {
219                Some(msg.clone())
220            } else {
221                None
222            }
223        })
224    }
225
226    /// Validate that value is greater than a minimum
227    /// 
228    /// # Arguments
229    /// * `min` - Minimum value (exclusive)
230    /// * `message` - Optional custom error message. If not provided, uses default message with the min value.
231    pub fn greater_than(self, min: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
232    where
233        T: Numeric,
234    {
235        let min_val = min.into();
236        let msg = message.map(|m| m.into());
237        self.rule(move |value| {
238            if value.to_f64() <= min_val {
239                Some(msg.clone().unwrap_or_else(|| format!("must be greater than {}", min_val)))
240            } else {
241                None
242            }
243        })
244    }
245
246    /// Validate that value is greater than or equal to a minimum
247    /// 
248    /// # Arguments
249    /// * `min` - Minimum value (inclusive)
250    /// * `message` - Optional custom error message. If not provided, uses default message with the min value.
251    pub fn greater_than_or_equal(self, min: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
252    where
253        T: Numeric,
254    {
255        let min_val = min.into();
256        let msg = message.map(|m| m.into());
257        self.rule(move |value| {
258            if value.to_f64() < min_val {
259                Some(msg.clone().unwrap_or_else(|| format!("must be greater than or equal to {}", min_val)))
260            } else {
261                None
262            }
263        })
264    }
265
266    /// Validate that value is less than a maximum
267    /// 
268    /// # Arguments
269    /// * `max` - Maximum value (exclusive)
270    /// * `message` - Optional custom error message. If not provided, uses default message with the max value.
271    pub fn less_than(self, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
272    where
273        T: Numeric,
274    {
275        let max_val = max.into();
276        let msg = message.map(|m| m.into());
277        self.rule(move |value| {
278            if value.to_f64() >= max_val {
279                Some(msg.clone().unwrap_or_else(|| format!("must be less than {}", max_val)))
280            } else {
281                None
282            }
283        })
284    }
285
286    /// Validate that value is less than or equal to a maximum
287    /// 
288    /// # Arguments
289    /// * `max` - Maximum value (inclusive)
290    /// * `message` - Optional custom error message. If not provided, uses default message with the max value.
291    pub fn less_than_or_equal(self, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
292    where
293        T: Numeric,
294    {
295        let max_val = max.into();
296        let msg = message.map(|m| m.into());
297        self.rule(move |value| {
298            if value.to_f64() > max_val {
299                Some(msg.clone().unwrap_or_else(|| format!("must be less than or equal to {}", max_val)))
300            } else {
301                None
302            }
303        })
304    }
305
306    /// Validate that value is within a range (inclusive)
307    /// 
308    /// # Arguments
309    /// * `min` - Minimum value (inclusive)
310    /// * `max` - Maximum value (inclusive)
311    /// * `message` - Optional custom error message. If not provided, uses default message with the min and max values.
312    pub fn inclusive_between(self, min: impl Into<f64> + Copy + 'static, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
313    where
314        T: Numeric,
315    {
316        let min_val = min.into();
317        let max_val = max.into();
318        let msg = message.map(|m| m.into());
319        self.rule(move |value| {
320            let val = value.to_f64();
321            if val < min_val || val > max_val {
322                Some(msg.clone().unwrap_or_else(|| format!("must be between {} and {}", min_val, max_val)))
323            } else {
324                None
325            }
326        })
327    }
328
329    /// Validate with a custom predicate
330    pub fn must(self, predicate: impl Fn(&T) -> bool + 'static, message: impl Into<String> + Clone + 'static) -> Self {
331        let msg = message.into();
332        self.rule(move |value| {
333            if !predicate(value) {
334                Some(msg.clone())
335            } else {
336                None
337            }
338        })
339    }
340
341    /// Build the rule and return a function that can be used in a validator
342    pub fn build(self) -> impl Fn(&T) -> Vec<ValidationError> {
343        let property_name = self.property_name.clone();
344        let rules = self.rules;
345        move |value: &T| {
346            let mut errors = Vec::new();
347            for rule in &rules {
348                if let Some(message) = rule(value) {
349                    errors.push(ValidationError::new(property_name.clone(), message));
350                }
351            }
352            errors
353        }
354    }
355}
356
357/// Trait for types that can be treated as numeric values
358pub trait Numeric {
359    fn to_f64(&self) -> f64;
360}
361
362impl Numeric for i8 { fn to_f64(&self) -> f64 { *self as f64 } }
363impl Numeric for i16 { fn to_f64(&self) -> f64 { *self as f64 } }
364impl Numeric for i32 { fn to_f64(&self) -> f64 { *self as f64 } }
365impl Numeric for i64 { fn to_f64(&self) -> f64 { *self as f64 } }
366impl Numeric for u8 { fn to_f64(&self) -> f64 { *self as f64 } }
367impl Numeric for u16 { fn to_f64(&self) -> f64 { *self as f64 } }
368impl Numeric for u32 { fn to_f64(&self) -> f64 { *self as f64 } }
369impl Numeric for u64 { fn to_f64(&self) -> f64 { *self as f64 } }
370impl Numeric for f32 { fn to_f64(&self) -> f64 { *self as f64 } }
371impl Numeric for f64 { fn to_f64(&self) -> f64 { *self } }
372
373/// Trait for types that can be treated as Option-like
374pub trait OptionLike {
375    fn is_none(&self) -> bool;
376}
377
378impl<T> OptionLike for Option<T> {
379    fn is_none(&self) -> bool {
380        Option::is_none(self)
381    }
382}
383
384/// Helper struct to build validators in a fluent style
385pub struct ValidatorBuilder<T> {
386    rules: Vec<Box<dyn Fn(&T) -> Vec<ValidationError>>>,
387}
388
389impl<T> ValidatorBuilder<T> {
390    /// Create a new validator builder
391    pub fn new() -> Self {
392        Self { rules: Vec::new() }
393    }
394
395    /// Add a rule for a property
396    pub fn rule_for<F, V>(mut self, _property_name: impl Into<String>, accessor: F, builder: RuleBuilder<V>) -> Self
397    where
398        F: Fn(&T) -> &V + 'static,
399        V: 'static,
400    {
401        let rule_fn = builder.build();
402        self.rules.push(Box::new(move |instance: &T| {
403            let value = accessor(instance);
404            rule_fn(value)
405        }));
406        self
407    }
408
409    /// Add a rule for a property that can access the entire object
410    /// 
411    /// This allows you to validate a property based on other properties in the object.
412    /// The closure receives both the object and the property value.
413    /// 
414    /// # Arguments
415    /// * `property_name` - Name of the property being validated
416    /// * `accessor` - Function to access the property value from the object
417    /// * `predicate` - Function that receives both the entire object and the property value, returns true if valid
418    /// * `message` - Error message to use if validation fails
419    /// 
420    /// # Example
421    /// ```rust,ignore
422    /// // Validate property using both object and property value
423    /// .must("taxNumber", |c| &c.tax_number,
424    ///     |command, tax_number| tax_number.is_valid_tax_number(&command.country_iso_code),
425    ///     "Tax number is not valid for the specified country")
426    /// 
427    /// // Validate property ignoring the object (use _ for object parameter)
428    /// .must("country", |c| &c.country,
429    ///     |_, country| Countries::allowed_countries().contains(country),
430    ///     "Country is not in the allowed list")
431    /// ```
432    pub fn must<F, V, P>(mut self, property_name: impl Into<String>, accessor: F, predicate: P, message: impl Into<String>) -> Self
433    where
434        F: Fn(&T) -> &V + 'static,
435        V: 'static,
436        P: Fn(&T, &V) -> bool + 'static,
437    {
438        let property_name = property_name.into();
439        let msg = message.into();
440        self.rules.push(Box::new(move |instance: &T| {
441            let value = accessor(instance);
442            if !predicate(instance, value) {
443                vec![ValidationError::new(property_name.clone(), msg.clone())]
444            } else {
445                Vec::new()
446            }
447        }));
448        self
449    }
450
451    /// Build the validator
452    pub fn build(self) -> impl Validator<T> {
453        ValidatorImpl { rules: self.rules }
454    }
455}
456
457impl<T> Default for ValidatorBuilder<T> {
458    fn default() -> Self {
459        Self::new()
460    }
461}
462
463struct ValidatorImpl<T> {
464    rules: Vec<Box<dyn Fn(&T) -> Vec<ValidationError>>>,
465}
466
467impl<T> Validator<T> for ValidatorImpl<T> {
468    fn validate(&self, instance: &T) -> ValidationResult {
469        let mut result = ValidationResult::new();
470        for rule in &self.rules {
471            let errors = rule(instance);
472            result.add_errors(errors);
473        }
474        result
475    }
476}
477
478/// Helper function to validate an instance with a validator
479pub fn validate<T>(instance: &T, validator: &dyn Validator<T>) -> ValidationResult {
480    validator.validate(instance)
481}
482
483