skp_validator_rules/comparison/
must_match.rs

1//! Must match validation rule for field comparison.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Must match validation rule - field must equal another field.
6///
7/// This is commonly used for password confirmation.
8///
9/// # Example
10///
11/// ```rust,ignore
12/// use skp_validator_rules::comparison::must_match::MustMatchRule;
13/// use skp_validator_core::{Rule, ValidationContext};
14///
15/// // In a struct context, you would compare password_confirm to password
16/// let rule = MustMatchRule::new("password");
17/// ```
18#[derive(Debug, Clone)]
19pub struct MustMatchRule {
20    /// The field to match against
21    pub other_field: String,
22    /// Custom error message
23    pub message: Option<String>,
24}
25
26impl MustMatchRule {
27    /// Create a new must_match rule.
28    pub fn new(other_field: impl Into<String>) -> Self {
29        Self {
30            other_field: other_field.into(),
31            message: None,
32        }
33    }
34
35    /// Set custom error message.
36    pub fn message(mut self, msg: impl Into<String>) -> Self {
37        self.message = Some(msg.into());
38        self
39    }
40
41    fn get_message(&self) -> String {
42        self.message.clone().unwrap_or_else(|| {
43            format!("Must match '{}'", self.other_field)
44        })
45    }
46}
47
48impl Rule<str> for MustMatchRule {
49    fn validate(&self, value: &str, ctx: &ValidationContext) -> ValidationResult<()> {
50        // Get the other field's value from context
51        if let Some(other_value) = ctx.get_string(&self.other_field) {
52            if value == other_value {
53                return Ok(());
54            } else {
55                return Err(ValidationErrors::from_iter([
56                    ValidationError::root("must_match", self.get_message())
57                        .with_param("other_field", self.other_field.clone())
58                ]));
59            }
60        }
61
62        // If other field not found, validation passes (the other field's validation will fail)
63        Ok(())
64    }
65
66    fn name(&self) -> &'static str {
67        "must_match"
68    }
69
70    fn default_message(&self) -> String {
71        format!("Must match '{}'", self.other_field)
72    }
73}
74
75impl Rule<String> for MustMatchRule {
76    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
77        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
78    }
79
80    fn name(&self) -> &'static str {
81        "must_match"
82    }
83
84    fn default_message(&self) -> String {
85        format!("Must match '{}'", self.other_field)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    #[cfg(feature = "serde")]
95    fn test_must_match() {
96        use serde_json::json;
97        
98        let rule = MustMatchRule::new("password");
99        let ctx = ValidationContext::from_json(&json!({
100            "password": "secret123",
101            "password_confirm": "secret123"
102        }));
103
104        // Matches
105        assert!(rule.validate("secret123", &ctx).is_ok());
106        
107        // Doesn't match
108        assert!(rule.validate("different", &ctx).is_err());
109    }
110
111    #[test]
112    fn test_custom_message() {
113        let rule = MustMatchRule::new("password").message("Passwords must match");
114        assert_eq!(rule.get_message(), "Passwords must match");
115    }
116}