skp_validator_core/
traits.rs

1//! Core validation traits.
2//!
3//! This module defines the fundamental traits for the validation system:
4//! - [`Validate`] - Main trait for validatable types
5//! - [`Rule`] - Trait for individual validation rules
6//! - [`Transform`] - Trait for field transformations
7//! - [`ValidateNested`] - Trait for nested struct validation
8//! - [`ValidateDive`] - Trait for collection validation
9
10use crate::context::ValidationContext;
11use crate::error::ValidationErrors;
12use crate::path::FieldPath;
13use crate::result::ValidationResult;
14
15/// Core validation trait for types that can be validated.
16///
17/// This trait is typically implemented by the derive macro, but can also
18/// be implemented manually for custom validation logic.
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use skp_validator_core::{Validate, ValidationContext, ValidationResult};
24///
25/// struct User {
26///     name: String,
27///     email: String,
28/// }
29///
30/// impl Validate for User {
31///     fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
32///         let mut errors = ValidationErrors::new();
33///
34///         if self.name.is_empty() {
35///             errors.add_field_error("name",
36///                 ValidationError::new("name", "required", "Name is required"));
37///         }
38///
39///         if errors.is_empty() { Ok(()) } else { Err(errors) }
40///     }
41/// }
42/// ```
43pub trait Validate {
44    /// Validate this instance with default context.
45    fn validate(&self) -> ValidationResult<()> {
46        self.validate_with_context(&ValidationContext::default())
47    }
48
49    /// Validate this instance with a custom context.
50    ///
51    /// This is the primary validation method that should be implemented.
52    fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()>;
53
54    /// Validate and return a transformed copy (if transformations are applied).
55    ///
56    /// This method is useful when validation includes transformations like
57    /// trimming, case conversion, etc.
58    fn validate_and_transform(&self) -> ValidationResult<Self>
59    where
60        Self: Clone + Sized,
61    {
62        self.validate()?;
63        Ok(self.clone())
64    }
65}
66
67// === Blanket Implementations ===
68
69impl<T: Validate + ?Sized> Validate for Box<T> {
70    fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
71        (**self).validate_with_context(ctx)
72    }
73}
74
75impl<T: Validate + ?Sized> Validate for std::rc::Rc<T> {
76    fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
77        (**self).validate_with_context(ctx)
78    }
79}
80
81impl<T: Validate + ?Sized> Validate for std::sync::Arc<T> {
82    fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
83        (**self).validate_with_context(ctx)
84    }
85}
86
87impl<T: Validate + ?Sized> Validate for &T {
88    fn validate_with_context(&self, ctx: &ValidationContext) -> ValidationResult<()> {
89        (**self).validate_with_context(ctx)
90    }
91}
92
93/// Trait for individual validation rules.
94///
95/// Each validation rule (email, length, range, etc.) implements this trait.
96/// Rules are generic over the value type they validate.
97///
98/// # Example
99///
100/// ```rust,ignore
101/// use skp_validator_core::{Rule, ValidationContext, ValidationResult};
102///
103/// struct MinLengthRule {
104///     min: usize,
105/// }
106///
107/// impl Rule<str> for MinLengthRule {
108///     fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
109///         if value.len() >= self.min {
110///             Ok(())
111///         } else {
112///             Err(ValidationErrors::from_error(
113///                 ValidationError::new("", "length.min", "Too short")
114///             ))
115///         }
116///     }
117///
118///     fn name(&self) -> &'static str { "min_length" }
119///     fn default_message(&self) -> String { format!("Must be at least {} characters", self.min) }
120/// }
121/// ```
122pub trait Rule<T: ?Sized>: Send + Sync {
123    /// Validate the value.
124    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()>;
125
126    /// Validate the value with a field path for error reporting.
127    fn validate_at(
128        &self,
129        value: &T,
130        path: &FieldPath,
131        ctx: &ValidationContext,
132    ) -> ValidationResult<()> {
133        self.validate(value, ctx).map_err(|mut errors| {
134            // Prepend path to all errors
135            for error in &mut errors.errors {
136                let mut new_path = path.clone();
137                for segment in error.path.segments().iter().cloned() {
138                    match segment {
139                        crate::path::PathSegment::Field(f) => new_path.append_field(f),
140                        crate::path::PathSegment::Index(i) => new_path.append_index(i),
141                        crate::path::PathSegment::Key(k) => new_path.append_key(k),
142                    }
143                }
144                error.path = new_path;
145            }
146            errors
147        })
148    }
149
150    /// Get the rule name for error reporting.
151    fn name(&self) -> &'static str;
152
153    /// Get the default error message.
154    fn default_message(&self) -> String;
155
156    /// Get the error code.
157    fn error_code(&self) -> String {
158        self.name().to_string()
159    }
160
161    /// Check if this rule is a transformation (not validation).
162    fn is_transform(&self) -> bool {
163        false
164    }
165}
166
167/// Trait for field transformations.
168///
169/// Transformations modify values during validation (e.g., trim, uppercase).
170pub trait Transform<T>: Send + Sync {
171    /// Transform the value.
172    fn transform(&self, value: T) -> T;
173
174    /// Get the transformation name.
175    fn name(&self) -> &'static str;
176}
177
178/// Trait for nested struct validation.
179///
180/// Implemented by types that contain nested validatable structures.
181pub trait ValidateNested: Validate {
182    /// Validate as a nested field with path prefix.
183    fn validate_nested(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()>;
184}
185
186/// Trait for collection validation (dive).
187///
188/// Implemented by collection types (Vec, HashMap, etc.) to validate each item.
189pub trait ValidateDive<T: Validate> {
190    /// Validate each item in the collection.
191    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()>;
192}
193
194// === Default implementations ===
195
196impl<T: Validate> ValidateNested for T {
197    fn validate_nested(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
198        self.validate_with_context(ctx).map_err(|errors| {
199            let mut nested = ValidationErrors::new();
200            if let Some(field) = path.last_field_name() {
201                nested.add_nested_errors(field, errors);
202            } else {
203                nested.merge(errors);
204            }
205            nested
206        })
207    }
208}
209
210impl<T: Validate> ValidateDive<T> for Vec<T> {
211    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
212        let mut errors = std::collections::BTreeMap::new();
213
214        for (idx, item) in self.iter().enumerate() {
215            if let Err(item_errors) = item.validate_with_context(ctx) {
216                errors.insert(idx, Box::new(item_errors));
217            }
218        }
219
220        if errors.is_empty() {
221            Ok(())
222        } else {
223            let mut result = ValidationErrors::new();
224            if let Some(field) = path.last_field_name() {
225                result.add_list_errors(field, errors);
226            }
227            Err(result)
228        }
229    }
230}
231
232impl<T: Validate> ValidateDive<T> for Option<T> {
233    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
234        if let Some(value) = self {
235            value.validate_nested(path, ctx)
236        } else {
237            Ok(())
238        }
239    }
240}
241
242impl<K, V> ValidateDive<V> for std::collections::HashMap<K, V>
243where
244    K: std::fmt::Display + Eq + std::hash::Hash,
245    V: Validate,
246{
247    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
248        let mut errors = std::collections::BTreeMap::new();
249
250        for (key, value) in self.iter() {
251            if let Err(item_errors) = value.validate_with_context(ctx) {
252                errors.insert(key.to_string(), Box::new(item_errors));
253            }
254        }
255
256        if errors.is_empty() {
257            Ok(())
258        } else {
259            let mut result = ValidationErrors::new();
260            if let Some(field) = path.last_field_name() {
261                result.add_map_errors(field, errors);
262            }
263            Err(result)
264        }
265    }
266}
267
268impl<K, V> ValidateDive<V> for std::collections::BTreeMap<K, V>
269where
270    K: std::fmt::Display + Ord,
271    V: Validate,
272{
273    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
274        let mut errors = std::collections::BTreeMap::new();
275
276        for (key, value) in self.iter() {
277            if let Err(item_errors) = value.validate_with_context(ctx) {
278                errors.insert(key.to_string(), Box::new(item_errors));
279            }
280        }
281
282        if errors.is_empty() {
283            Ok(())
284        } else {
285            let mut result = ValidationErrors::new();
286            if let Some(field) = path.last_field_name() {
287                result.add_map_errors(field, errors);
288            }
289            Err(result)
290        }
291    }
292}
293
294impl<T: Validate + Eq + std::hash::Hash> ValidateDive<T> for std::collections::HashSet<T> {
295    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
296        let mut errors = std::collections::BTreeMap::new();
297
298        for (idx, item) in self.iter().enumerate() {
299            if let Err(item_errors) = item.validate_with_context(ctx) {
300                errors.insert(idx, Box::new(item_errors));
301            }
302        }
303
304        if errors.is_empty() {
305            Ok(())
306        } else {
307            let mut result = ValidationErrors::new();
308            if let Some(field) = path.last_field_name() {
309                result.add_list_errors(field, errors);
310            }
311            Err(result)
312        }
313    }
314}
315
316impl<T: Validate + Ord> ValidateDive<T> for std::collections::BTreeSet<T> {
317    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
318        let mut errors = std::collections::BTreeMap::new();
319
320        for (idx, item) in self.iter().enumerate() {
321            if let Err(item_errors) = item.validate_with_context(ctx) {
322                errors.insert(idx, Box::new(item_errors));
323            }
324        }
325
326        if errors.is_empty() {
327            Ok(())
328        } else {
329            let mut result = ValidationErrors::new();
330            if let Some(field) = path.last_field_name() {
331                result.add_list_errors(field, errors);
332            }
333            Err(result)
334        }
335    }
336}
337
338// Smart pointer implementations - these dive into the inner type
339impl<T: Validate> ValidateDive<T> for Box<T> {
340    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
341        self.as_ref().validate_nested(path, ctx)
342    }
343}
344
345impl<T: Validate> ValidateDive<T> for std::rc::Rc<T> {
346    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
347        self.as_ref().validate_nested(path, ctx)
348    }
349}
350
351impl<T: Validate> ValidateDive<T> for std::sync::Arc<T> {
352    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
353        self.as_ref().validate_nested(path, ctx)
354    }
355}
356
357// Slice implementation
358impl<T: Validate> ValidateDive<T> for [T] {
359    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
360        let mut errors = std::collections::BTreeMap::new();
361
362        for (idx, item) in self.iter().enumerate() {
363            if let Err(item_errors) = item.validate_with_context(ctx) {
364                errors.insert(idx, Box::new(item_errors));
365            }
366        }
367
368        if errors.is_empty() {
369            Ok(())
370        } else {
371            let mut result = ValidationErrors::new();
372            if let Some(field) = path.last_field_name() {
373                result.add_list_errors(field, errors);
374            }
375            Err(result)
376        }
377    }
378}
379
380// Array implementations for common sizes
381impl<T: Validate, const N: usize> ValidateDive<T> for [T; N] {
382    fn validate_dive(&self, path: &FieldPath, ctx: &ValidationContext) -> ValidationResult<()> {
383        let mut errors = std::collections::BTreeMap::new();
384
385        for (idx, item) in self.iter().enumerate() {
386            if let Err(item_errors) = item.validate_with_context(ctx) {
387                errors.insert(idx, Box::new(item_errors));
388            }
389        }
390
391        if errors.is_empty() {
392            Ok(())
393        } else {
394            let mut result = ValidationErrors::new();
395            if let Some(field) = path.last_field_name() {
396                result.add_list_errors(field, errors);
397            }
398            Err(result)
399        }
400    }
401}
402
403/// A composite rule that combines multiple rules.
404pub struct CompositeRule<T: ?Sized> {
405    rules: Vec<Box<dyn Rule<T>>>,
406}
407
408impl<T: ?Sized> CompositeRule<T> {
409    /// Create a new composite rule.
410    pub fn new() -> Self {
411        Self { rules: Vec::new() }
412    }
413
414    /// Add a rule to this composite.
415    pub fn push_rule(mut self, rule: impl Rule<T> + 'static) -> Self {
416        self.rules.push(Box::new(rule));
417        self
418    }
419}
420
421impl<T: ?Sized> Default for CompositeRule<T> {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427impl<T: ?Sized + Sync> Rule<T> for CompositeRule<T> {
428    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
429        let mut all_errors = ValidationErrors::new();
430
431        for rule in &self.rules {
432            if let Err(errors) = rule.validate(value, ctx) {
433                all_errors.merge(errors);
434
435                // Fail fast if enabled
436                if ctx.is_fail_fast() && !all_errors.is_empty() {
437                    return Err(all_errors);
438                }
439            }
440        }
441
442        if all_errors.is_empty() {
443            Ok(())
444        } else {
445            Err(all_errors)
446        }
447    }
448
449    fn name(&self) -> &'static str {
450        "composite"
451    }
452
453    fn default_message(&self) -> String {
454        "Validation failed".to_string()
455    }
456}
457
458/// Any rule - passes if any of the inner rules pass (OR logic).
459pub struct AnyRule<T: ?Sized> {
460    rules: Vec<Box<dyn Rule<T>>>,
461}
462
463impl<T: ?Sized> AnyRule<T> {
464    /// Create a new any rule.
465    pub fn new() -> Self {
466        Self { rules: Vec::new() }
467    }
468
469    /// Add a rule.
470    pub fn push_rule(mut self, rule: impl Rule<T> + 'static) -> Self {
471        self.rules.push(Box::new(rule));
472        self
473    }
474}
475
476impl<T: ?Sized> Default for AnyRule<T> {
477    fn default() -> Self {
478        Self::new()
479    }
480}
481
482impl<T: ?Sized + Sync> Rule<T> for AnyRule<T> {
483    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
484        if self.rules.is_empty() {
485            return Ok(());
486        }
487
488        for rule in &self.rules {
489            if rule.validate(value, ctx).is_ok() {
490                return Ok(()); // At least one passed
491            }
492        }
493
494        // None passed, return generic error
495        Err(ValidationErrors::from_iter([crate::ValidationError::root(
496            "any",
497            self.default_message(),
498        )]))
499    }
500
501    fn name(&self) -> &'static str {
502        "any"
503    }
504
505    fn default_message(&self) -> String {
506        "Must satisfy at least one validation rule".to_string()
507    }
508}
509
510/// All rule - passes only if all inner rules pass (AND logic, same as CompositeRule).
511pub type AllRule<T> = CompositeRule<T>;
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::ValidationError;
517
518    // Mock rule for testing
519    struct MockRule {
520        should_pass: bool,
521    }
522
523    impl Rule<str> for MockRule {
524        fn validate(&self, _value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
525            if self.should_pass {
526                Ok(())
527            } else {
528                Err(ValidationErrors::from_iter([ValidationError::new(
529                    "",
530                    "mock",
531                    "Mock error",
532                )]))
533            }
534        }
535
536        fn name(&self) -> &'static str {
537            "mock"
538        }
539
540        fn default_message(&self) -> String {
541            "Mock error".to_string()
542        }
543    }
544
545    #[test]
546    fn test_composite_rule() {
547        let rule = CompositeRule::new()
548            .push_rule(MockRule { should_pass: true })
549            .push_rule(MockRule { should_pass: true });
550
551        let ctx = ValidationContext::default();
552        assert!(rule.validate("test", &ctx).is_ok());
553    }
554
555    #[test]
556    fn test_composite_rule_fails() {
557        let rule = CompositeRule::new()
558            .push_rule(MockRule { should_pass: true })
559            .push_rule(MockRule { should_pass: false });
560
561        let ctx = ValidationContext::default();
562        assert!(rule.validate("test", &ctx).is_err());
563    }
564
565    #[test]
566    fn test_any_rule() {
567        let rule = AnyRule::new()
568            .push_rule(MockRule { should_pass: false })
569            .push_rule(MockRule { should_pass: true });
570
571        let ctx = ValidationContext::default();
572        assert!(rule.validate("test", &ctx).is_ok());
573    }
574
575    #[test]
576    fn test_any_rule_all_fail() {
577        let rule = AnyRule::new()
578            .push_rule(MockRule { should_pass: false })
579            .push_rule(MockRule { should_pass: false });
580
581        let ctx = ValidationContext::default();
582        assert!(rule.validate("test", &ctx).is_err());
583    }
584}