skp_validator_rules/custom/
custom_fn.rs

1//! Custom function validation rule.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::sync::Arc;
5
6/// Custom function validation rule - validates using a user-provided function.
7///
8/// # Example
9///
10/// ```rust
11/// use skp_validator_rules::custom::custom_fn::CustomFnRule;
12/// use skp_validator_core::{Rule, ValidationContext};
13///
14/// let rule = CustomFnRule::new("is_even", |value: &i32, _ctx| {
15///     value % 2 == 0
16/// }).message("Value must be even");
17///
18/// let ctx = ValidationContext::default();
19/// assert!(rule.validate(&4, &ctx).is_ok());
20/// assert!(rule.validate(&3, &ctx).is_err());
21/// ```
22pub struct CustomFnRule<T: ?Sized, F>
23where
24    F: Fn(&T, &ValidationContext) -> bool + Send + Sync,
25{
26    /// Rule name for error reporting
27    pub rule_name: String,
28    /// The validation function
29    pub validator: Arc<F>,
30    /// Custom error message
31    pub message: Option<String>,
32    /// Phantom data for T
33    _marker: std::marker::PhantomData<fn(&T)>,
34}
35
36impl<T: ?Sized, F> CustomFnRule<T, F>
37where
38    F: Fn(&T, &ValidationContext) -> bool + Send + Sync,
39{
40    /// Create a new custom function rule.
41    pub fn new(name: impl Into<String>, validator: F) -> Self {
42        Self {
43            rule_name: name.into(),
44            validator: Arc::new(validator),
45            message: None,
46            _marker: std::marker::PhantomData,
47        }
48    }
49
50    /// Set custom error message.
51    pub fn message(mut self, msg: impl Into<String>) -> Self {
52        self.message = Some(msg.into());
53        self
54    }
55
56    fn get_message(&self) -> String {
57        self.message.clone().unwrap_or_else(|| {
58            format!("Validation failed for rule '{}'", self.rule_name)
59        })
60    }
61}
62
63impl<T: ?Sized, F> std::fmt::Debug for CustomFnRule<T, F>
64where
65    F: Fn(&T, &ValidationContext) -> bool + Send + Sync,
66{
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("CustomFnRule")
69            .field("rule_name", &self.rule_name)
70            .field("message", &self.message)
71            .finish()
72    }
73}
74
75impl<T: ?Sized, F> Clone for CustomFnRule<T, F>
76where
77    F: Fn(&T, &ValidationContext) -> bool + Send + Sync,
78{
79    fn clone(&self) -> Self {
80        Self {
81            rule_name: self.rule_name.clone(),
82            validator: Arc::clone(&self.validator),
83            message: self.message.clone(),
84            _marker: std::marker::PhantomData,
85        }
86    }
87}
88
89impl<T: ?Sized, F> Rule<T> for CustomFnRule<T, F>
90where
91    F: Fn(&T, &ValidationContext) -> bool + Send + Sync,
92{
93    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
94        if (self.validator)(value, ctx) {
95            Ok(())
96        } else {
97            Err(ValidationErrors::from_iter([
98                ValidationError::root(&self.rule_name, self.get_message())
99            ]))
100        }
101    }
102
103    fn name(&self) -> &'static str {
104        "custom"
105    }
106
107    fn default_message(&self) -> String {
108        self.get_message()
109    }
110}
111
112/// Custom validation with result - allows returning detailed errors.
113pub struct CustomResultRule<T: ?Sized, F>
114where
115    F: Fn(&T, &ValidationContext) -> ValidationResult<()> + Send + Sync,
116{
117    /// Rule name for error reporting
118    pub rule_name: String,
119    /// The validation function
120    pub validator: Arc<F>,
121    /// Phantom data for T
122    _marker: std::marker::PhantomData<fn(&T)>,
123}
124
125impl<T: ?Sized, F> CustomResultRule<T, F>
126where
127    F: Fn(&T, &ValidationContext) -> ValidationResult<()> + Send + Sync,
128{
129    /// Create a new custom result rule.
130    pub fn new(name: impl Into<String>, validator: F) -> Self {
131        Self {
132            rule_name: name.into(),
133            validator: Arc::new(validator),
134            _marker: std::marker::PhantomData,
135        }
136    }
137}
138
139impl<T: ?Sized, F> std::fmt::Debug for CustomResultRule<T, F>
140where
141    F: Fn(&T, &ValidationContext) -> ValidationResult<()> + Send + Sync,
142{
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("CustomResultRule")
145            .field("rule_name", &self.rule_name)
146            .finish()
147    }
148}
149
150impl<T: ?Sized, F> Clone for CustomResultRule<T, F>
151where
152    F: Fn(&T, &ValidationContext) -> ValidationResult<()> + Send + Sync,
153{
154    fn clone(&self) -> Self {
155        Self {
156            rule_name: self.rule_name.clone(),
157            validator: Arc::clone(&self.validator),
158            _marker: std::marker::PhantomData,
159        }
160    }
161}
162
163impl<T: ?Sized, F> Rule<T> for CustomResultRule<T, F>
164where
165    F: Fn(&T, &ValidationContext) -> ValidationResult<()> + Send + Sync,
166{
167    fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
168        (self.validator)(value, ctx)
169    }
170
171    fn name(&self) -> &'static str {
172        "custom"
173    }
174
175    fn default_message(&self) -> String {
176        format!("Validation failed for rule '{}'", self.rule_name)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_custom_fn() {
186        let rule = CustomFnRule::new("is_positive", |value: &i32, _ctx: &ValidationContext| {
187            *value > 0
188        });
189        let ctx = ValidationContext::default();
190
191        assert!(rule.validate(&5, &ctx).is_ok());
192        assert!(rule.validate(&-1, &ctx).is_err());
193    }
194
195    #[test]
196    fn test_custom_message() {
197        let rule = CustomFnRule::new("is_even", |value: &i32, _ctx: &ValidationContext| {
198            value % 2 == 0
199        }).message("Must be an even number");
200        let ctx = ValidationContext::default();
201
202        let result = rule.validate(&3, &ctx);
203        assert!(result.is_err());
204        let errors = result.unwrap_err();
205        assert!(errors.to_string().contains("Must be an even number"));
206    }
207
208    #[test]
209    fn test_custom_result() {
210        let rule = CustomResultRule::new("complex_validation", |value: &i32, _ctx: &ValidationContext| {
211            if *value < 0 {
212                Err(ValidationErrors::from_iter([
213                    ValidationError::root("positive", "Must be positive")
214                ]))
215            } else if *value > 100 {
216                Err(ValidationErrors::from_iter([
217                    ValidationError::root("max", "Must be <= 100")
218                ]))
219            } else {
220                Ok(())
221            }
222        });
223        let ctx = ValidationContext::default();
224
225        assert!(rule.validate(&50, &ctx).is_ok());
226        assert!(rule.validate(&-1, &ctx).is_err());
227        assert!(rule.validate(&101, &ctx).is_err());
228    }
229}