skp_validator_rules/custom/
contextual.rs

1//! Contextual validation rule - validates based on context metadata.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::sync::Arc;
5
6/// Contextual validation rule - validates based on context metadata or locale.
7///
8/// # Example
9///
10/// ```rust
11/// use skp_validator_rules::custom::contextual::ContextualRule;
12/// use skp_validator_core::{Rule, ValidationContext};
13///
14/// // A rule that only validates when context has "strict" mode
15/// let rule = ContextualRule::new("strict_email")
16///     .when(|ctx| ctx.get_meta("mode") == Some("strict"))
17///     .validate_with(|value: &str, _ctx| {
18///         // More strict email validation
19///         value.contains('@') && value.contains('.') && value.len() > 5
20///     });
21///
22/// let strict_ctx = ValidationContext::new().with_meta("mode", "strict");
23/// let normal_ctx = ValidationContext::new();
24///
25/// // In strict mode, validation is applied
26/// assert!(rule.validate("a@b.c", &strict_ctx).is_err()); // Too short
27///
28/// // In normal mode, validation is skipped
29/// assert!(rule.validate("a@b.c", &normal_ctx).is_ok());
30/// ```
31/// Type alias for contextual condition function
32pub type ContextualCondition = Arc<dyn Fn(&ValidationContext) -> bool + Send + Sync>;
33
34/// Type alias for contextual validator function
35pub type ContextualValidator<T> = Arc<dyn Fn(&T, &ValidationContext) -> bool + Send + Sync>;
36
37pub struct ContextualRule<T>
38where
39    T: ?Sized,
40{
41    /// Rule name
42    pub rule_name: String,
43    /// Condition function - when to apply validation
44    pub condition: Option<ContextualCondition>,
45    /// Validation function
46    pub validator: Option<ContextualValidator<T>>,
47    /// Custom error message
48    pub message: Option<String>,
49}
50
51impl<T: ?Sized> ContextualRule<T> {
52    /// Create a new contextual rule.
53    pub fn new(name: impl Into<String>) -> Self {
54        Self {
55            rule_name: name.into(),
56            condition: None,
57            validator: None,
58            message: None,
59        }
60    }
61
62    /// Set condition for when to apply validation.
63    pub fn when<F>(mut self, condition: F) -> Self
64    where
65        F: Fn(&ValidationContext) -> bool + Send + Sync + 'static,
66    {
67        self.condition = Some(Arc::new(condition));
68        self
69    }
70
71    /// Set the validation function.
72    pub fn validate_with<F>(mut self, validator: F) -> Self
73    where
74        F: Fn(&T, &ValidationContext) -> bool + Send + Sync + 'static,
75    {
76        self.validator = Some(Arc::new(validator));
77        self
78    }
79
80    /// Set custom error message.
81    pub fn message(mut self, msg: impl Into<String>) -> Self {
82        self.message = Some(msg.into());
83        self
84    }
85
86    fn get_message(&self) -> String {
87        self.message.clone().unwrap_or_else(|| {
88            format!("Contextual validation '{}' failed", self.rule_name)
89        })
90    }
91}
92
93impl<T: ?Sized> std::fmt::Debug for ContextualRule<T> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("ContextualRule")
96            .field("rule_name", &self.rule_name)
97            .field("has_condition", &self.condition.is_some())
98            .field("has_validator", &self.validator.is_some())
99            .finish()
100    }
101}
102
103impl<T: ?Sized + 'static> Rule<T> for ContextualRule<T> {
104    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
105        // Check if condition is met
106        if let Some(ref condition) = self.condition
107            && !condition(ctx)
108        {
109            // Condition not met, skip validation
110            return Ok(());
111        }
112
113        // Run validation if validator is set
114        if let Some(ref validator) = self.validator
115            && !validator(value, ctx)
116        {
117            return Err(ValidationErrors::from_iter([
118                ValidationError::root(&self.rule_name, self.get_message())
119            ]));
120        }
121
122        Ok(())
123    }
124
125    fn name(&self) -> &'static str {
126        "contextual"
127    }
128
129    fn default_message(&self) -> String {
130        self.get_message()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_contextual_applies() {
140        let rule = ContextualRule::<str>::new("strict")
141            .when(|ctx| ctx.get_meta("mode") == Some("strict"))
142            .validate_with(|value: &str, _ctx| value.len() >= 5);
143
144        let strict_ctx = ValidationContext::new().with_meta("mode", "strict");
145        
146        assert!(rule.validate("hello", &strict_ctx).is_ok());
147        assert!(rule.validate("hi", &strict_ctx).is_err());
148    }
149
150    #[test]
151    fn test_contextual_skips() {
152        let rule = ContextualRule::<str>::new("strict")
153            .when(|ctx| ctx.get_meta("mode") == Some("strict"))
154            .validate_with(|value: &str, _ctx| value.len() >= 5);
155
156        let normal_ctx = ValidationContext::new();
157        
158        // Validation is skipped when condition is not met
159        assert!(rule.validate("hi", &normal_ctx).is_ok());
160    }
161}