rustapi_validate/
custom.rs

1use crate::error::{FieldError, ValidationError};
2use std::collections::HashMap;
3
4/// A validation rule trait.
5pub trait Rule<T: ?Sized> {
6    /// Validate the value.
7    fn validate(&self, value: &T) -> Result<(), FieldError>;
8}
9
10/// Type alias for validation rule functions to reduce complexity.
11type ValidationRuleFn<T> = Box<dyn Fn(&T) -> Result<(), FieldError> + Send + Sync>;
12
13/// A functional validator builder.
14pub struct Validator<T> {
15    rules: Vec<ValidationRuleFn<T>>,
16}
17
18impl<T> Default for Validator<T> {
19    fn default() -> Self {
20        Self { rules: Vec::new() }
21    }
22}
23
24impl<T> Validator<T> {
25    /// Create a new validator.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Add a validation rule.
31    pub fn rule<F>(mut self, rule: F) -> Self
32    where
33        F: Fn(&T) -> Result<(), FieldError> + Send + Sync + 'static,
34    {
35        self.rules.push(Box::new(rule));
36        self
37    }
38
39    /// Validate the value.
40    pub fn validate(&self, value: &T) -> Result<(), ValidationError> {
41        let mut errors = Vec::new();
42
43        for rule in &self.rules {
44            if let Err(e) = rule(value) {
45                errors.push(e);
46            }
47        }
48
49        if errors.is_empty() {
50            Ok(())
51        } else {
52            Err(ValidationError::new(errors))
53        }
54    }
55}
56
57/// Common validation rules.
58pub mod rules {
59    use super::*;
60
61    /// Create a required field rule (not null/empty).
62    pub fn required<T: AsRef<str>>(
63        message: impl Into<String>,
64    ) -> impl Fn(&Option<T>) -> Result<(), FieldError> {
65        let message = message.into();
66        move |value: &Option<T>| match value {
67            Some(v) if !v.as_ref().is_empty() => Ok(()),
68            _ => Err(FieldError::new("required", "required", message.clone())),
69        }
70    }
71
72    /// Create a string length rule.
73    pub fn length<T: AsRef<str> + ?Sized>(
74        min: Option<usize>,
75        max: Option<usize>,
76        message: impl Into<String>,
77    ) -> impl Fn(&T) -> Result<(), FieldError> {
78        let message = message.into();
79        move |value: &T| {
80            let value = value.as_ref();
81            let len = value.len();
82            if let Some(min) = min {
83                if len < min {
84                    let mut params = HashMap::new();
85                    params.insert("min".to_string(), serde_json::json!(min));
86                    params.insert("max".to_string(), serde_json::json!(max));
87                    params.insert("value".to_string(), serde_json::json!(len));
88                    return Err(FieldError::with_params(
89                        "length",
90                        "length",
91                        message.clone(),
92                        params,
93                    ));
94                }
95            }
96            if let Some(max) = max {
97                if len > max {
98                    let mut params = HashMap::new();
99                    params.insert("min".to_string(), serde_json::json!(min));
100                    params.insert("max".to_string(), serde_json::json!(max));
101                    params.insert("value".to_string(), serde_json::json!(len));
102                    return Err(FieldError::with_params(
103                        "length",
104                        "length",
105                        message.clone(),
106                        params,
107                    ));
108                }
109            }
110            Ok(())
111        }
112    }
113
114    /// Create a numeric range rule.
115    pub fn range<T: PartialOrd + Copy + serde::Serialize>(
116        min: Option<T>,
117        max: Option<T>,
118        message: impl Into<String>,
119    ) -> impl Fn(&T) -> Result<(), FieldError> {
120        let message = message.into();
121        move |value: &T| {
122            if let Some(min) = min {
123                if *value < min {
124                    let mut params = HashMap::new();
125                    params.insert("min".to_string(), serde_json::json!(min));
126                    params.insert("max".to_string(), serde_json::json!(max));
127                    params.insert("value".to_string(), serde_json::json!(value));
128                    return Err(FieldError::with_params(
129                        "range",
130                        "range",
131                        message.clone(),
132                        params,
133                    ));
134                }
135            }
136            if let Some(max) = max {
137                if *value > max {
138                    let mut params = HashMap::new();
139                    params.insert("min".to_string(), serde_json::json!(min));
140                    params.insert("max".to_string(), serde_json::json!(max));
141                    params.insert("value".to_string(), serde_json::json!(value));
142                    return Err(FieldError::with_params(
143                        "range",
144                        "range",
145                        message.clone(),
146                        params,
147                    ));
148                }
149            }
150            Ok(())
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_custom_validator() {
161        let validator = Validator::<String>::new().rule(rules::length(
162            Some(3),
163            Some(10),
164            "Must be between 3 and 10 chars",
165        ));
166
167        assert!(validator.validate(&"ab".to_string()).is_err());
168        assert!(validator.validate(&"abc".to_string()).is_ok());
169        assert!(validator.validate(&"abcdefghijk".to_string()).is_err());
170    }
171
172    #[test]
173    fn test_range_validator() {
174        let validator = Validator::<i32>::new().rule(rules::range(
175            Some(18),
176            Some(100),
177            "Must be between 18 and 100",
178        ));
179
180        assert!(validator.validate(&17).is_err());
181        assert!(validator.validate(&18).is_ok());
182        assert!(validator.validate(&100).is_ok());
183        assert!(validator.validate(&101).is_err());
184    }
185}