skp_validator_rules/custom/
dependency.rs

1//! Field dependency validation rule.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::sync::Arc;
5
6/// Dependency condition type
7#[derive(Debug, Clone)]
8pub enum DependencyCondition {
9    /// Field must be present (not empty/null)
10    Present,
11    /// Field must be absent (empty/null)
12    Absent,
13    /// Field must equal a specific value
14    Equals(String),
15    /// Field must not equal a specific value
16    NotEquals(String),
17    /// Field must be one of these values
18    In(Vec<String>),
19    /// Field must not be one of these values
20    NotIn(Vec<String>),
21}
22
23/// Type alias for dependency validator function
24pub type DependencyValidator<T> = Arc<dyn Fn(&T, &ValidationContext) -> bool + Send + Sync>;
25
26/// Field dependency validation rule - validates based on other field values.
27///
28/// # Example
29///
30/// ```rust
31/// use skp_validator_rules::custom::dependency::{DependencyRule, DependencyCondition};
32/// use skp_validator_core::{Rule, ValidationContext};
33///
34/// // Shipping address is required when shipping_method is "delivery"
35/// let rule = DependencyRule::<str>::new("shipping_address")
36///     .depends_on("shipping_method", DependencyCondition::Equals("delivery".to_string()))
37///     .then_required();
38/// ```
39pub struct DependencyRule<T>
40where
41    T: ?Sized,
42{
43    /// The field being validated
44    pub field_name: String,
45    /// The field we depend on
46    pub depends_on_field: Option<String>,
47    /// The condition on the dependency field
48    pub condition: Option<DependencyCondition>,
49    /// Validation to apply when condition is met
50    pub validator: Option<DependencyValidator<T>>,
51    /// Whether field is required when condition is met
52    pub required_when_met: bool,
53    /// Custom error message
54    pub message: Option<String>,
55}
56
57impl<T: ?Sized> DependencyRule<T> {
58    /// Create a new dependency rule.
59    pub fn new(field_name: impl Into<String>) -> Self {
60        Self {
61            field_name: field_name.into(),
62            depends_on_field: None,
63            condition: None,
64            validator: None,
65            required_when_met: false,
66            message: None,
67        }
68    }
69
70    /// Set the field this depends on and condition.
71    pub fn depends_on(mut self, field: impl Into<String>, condition: DependencyCondition) -> Self {
72        self.depends_on_field = Some(field.into());
73        self.condition = Some(condition);
74        self
75    }
76
77    /// Make field required when condition is met.
78    pub fn then_required(mut self) -> Self {
79        self.required_when_met = true;
80        self
81    }
82
83    /// Set custom validation when condition is met.
84    pub fn then_validate<F>(mut self, validator: F) -> Self
85    where
86        F: Fn(&T, &ValidationContext) -> bool + Send + Sync + 'static,
87    {
88        self.validator = Some(Arc::new(validator));
89        self
90    }
91
92    /// Set custom error message.
93    pub fn message(mut self, msg: impl Into<String>) -> Self {
94        self.message = Some(msg.into());
95        self
96    }
97
98    fn get_message(&self) -> String {
99        self.message.clone().unwrap_or_else(|| {
100            if let Some(ref dep_field) = self.depends_on_field {
101                format!("Field '{}' is required based on '{}'", self.field_name, dep_field)
102            } else {
103                format!("Field '{}' validation failed", self.field_name)
104            }
105        })
106    }
107
108    /// Check if dependency condition is met
109    fn check_condition(&self, ctx: &ValidationContext) -> bool {
110        let Some(ref dep_field) = self.depends_on_field else {
111            return false;
112        };
113
114        let Some(ref condition) = self.condition else {
115            return false;
116        };
117
118        let dep_value = ctx.get_string(dep_field);
119
120        match condition {
121            DependencyCondition::Present => dep_value.is_some_and(|v| !v.is_empty()),
122            DependencyCondition::Absent => dep_value.is_none_or(|v| v.is_empty()),
123            DependencyCondition::Equals(expected) => dep_value.is_some_and(|v| v == expected),
124            DependencyCondition::NotEquals(expected) => dep_value.is_none_or(|v| v != expected),
125            DependencyCondition::In(values) => {
126                dep_value.is_some_and(|v| values.iter().any(|expected| v == expected))
127            }
128            DependencyCondition::NotIn(values) => {
129                dep_value.is_none_or(|v| !values.iter().any(|expected| v == expected))
130            }
131        }
132    }
133}
134
135impl<T: ?Sized> std::fmt::Debug for DependencyRule<T> {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct("DependencyRule")
138            .field("field_name", &self.field_name)
139            .field("depends_on_field", &self.depends_on_field)
140            .field("condition", &self.condition)
141            .field("required_when_met", &self.required_when_met)
142            .finish()
143    }
144}
145
146impl Rule<str> for DependencyRule<str> {
147    fn validate(&self, value: &str, ctx: &ValidationContext) -> ValidationResult<()> {
148        // Check if dependency condition is met
149        if !self.check_condition(ctx) {
150            // Condition not met, skip validation
151            return Ok(());
152        }
153
154        // Condition is met, apply validation
155        if self.required_when_met && value.trim().is_empty() {
156            return Err(ValidationErrors::from_iter([
157                ValidationError::root("dependency.required", self.get_message())
158            ]));
159        }
160
161        // Run custom validator if set
162        if let Some(ref validator) = self.validator
163            && !validator(value, ctx)
164        {
165            return Err(ValidationErrors::from_iter([
166                ValidationError::root("dependency.custom", self.get_message())
167            ]));
168        }
169
170        Ok(())
171    }
172
173    fn name(&self) -> &'static str {
174        "dependency"
175    }
176
177    fn default_message(&self) -> String {
178        self.get_message()
179    }
180}
181
182impl Rule<String> for DependencyRule<String> {
183    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
184        // Check if dependency condition is met
185        if !self.check_condition(ctx) {
186            return Ok(());
187        }
188
189        if self.required_when_met && value.trim().is_empty() {
190            return Err(ValidationErrors::from_iter([
191                ValidationError::root("dependency.required", self.get_message())
192            ]));
193        }
194
195        if let Some(ref validator) = self.validator
196            && !validator(value, ctx)
197        {
198            return Err(ValidationErrors::from_iter([
199                ValidationError::root("dependency.custom", self.get_message())
200            ]));
201        }
202
203        Ok(())
204    }
205
206    fn name(&self) -> &'static str {
207        "dependency"
208    }
209
210    fn default_message(&self) -> String {
211        self.get_message()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_depends_on_equals() {
221        let rule = DependencyRule::<str>::new("shipping_address")
222            .depends_on("shipping_method", DependencyCondition::Equals("delivery".to_string()))
223            .then_required();
224
225        // When shipping_method is "delivery", address is required
226        let delivery_ctx = ValidationContext::new()
227            .with_field("shipping_method", "delivery");
228        
229        assert!(rule.validate("", &delivery_ctx).is_err()); // Empty = error
230        assert!(rule.validate("123 Main St", &delivery_ctx).is_ok());
231
232        // When shipping_method is "pickup", address is optional
233        let pickup_ctx = ValidationContext::new()
234            .with_field("shipping_method", "pickup");
235        
236        assert!(rule.validate("", &pickup_ctx).is_ok()); // Empty = ok
237    }
238
239    #[test]
240    fn test_depends_on_present() {
241        let rule = DependencyRule::<str>::new("postal_code")
242            .depends_on("country", DependencyCondition::Present)
243            .then_required();
244
245        let with_country = ValidationContext::new()
246            .with_field("country", "US");
247        
248        assert!(rule.validate("", &with_country).is_err());
249
250        let without_country = ValidationContext::new();
251        assert!(rule.validate("", &without_country).is_ok());
252    }
253
254    #[test]
255    fn test_depends_on_in() {
256        let rule = DependencyRule::<str>::new("state")
257            .depends_on("country", DependencyCondition::In(vec!["US".to_string(), "CA".to_string()]))
258            .then_required();
259
260        let us_ctx = ValidationContext::new().with_field("country", "US");
261        assert!(rule.validate("", &us_ctx).is_err());
262
263        let uk_ctx = ValidationContext::new().with_field("country", "UK");
264        assert!(rule.validate("", &uk_ctx).is_ok()); // UK not in list, skip
265    }
266}