elif_validation/validators/
length.rs

1//! Length-based validators for strings and collections
2
3use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use serde_json::Value;
7
8/// Validator for string/array length constraints
9#[derive(Debug, Clone)]
10pub struct LengthValidator {
11    /// Minimum length (inclusive)
12    pub min: Option<usize>,
13    /// Maximum length (inclusive)
14    pub max: Option<usize>,
15    /// Exact length required
16    pub exact: Option<usize>,
17    /// Custom error message
18    pub message: Option<String>,
19}
20
21impl LengthValidator {
22    /// Create a new length validator with no constraints
23    pub fn new() -> Self {
24        Self {
25            min: None,
26            max: None,
27            exact: None,
28            message: None,
29        }
30    }
31
32    /// Set minimum length constraint
33    pub fn min(mut self, min: usize) -> Self {
34        self.min = Some(min);
35        self
36    }
37
38    /// Set maximum length constraint
39    pub fn max(mut self, max: usize) -> Self {
40        self.max = Some(max);
41        self
42    }
43
44    /// Set exact length requirement
45    pub fn exact(mut self, exact: usize) -> Self {
46        self.exact = Some(exact);
47        self
48    }
49
50    /// Set length range (min and max)
51    pub fn range(mut self, min: usize, max: usize) -> Self {
52        self.min = Some(min);
53        self.max = Some(max);
54        self
55    }
56
57    /// Set custom error message
58    pub fn message(mut self, message: impl Into<String>) -> Self {
59        self.message = Some(message.into());
60        self
61    }
62
63    /// Get the length of a value (supports strings and arrays)
64    fn get_length(&self, value: &Value) -> Option<usize> {
65        match value {
66            Value::String(s) => Some(s.chars().count()), // Unicode-aware length
67            Value::Array(arr) => Some(arr.len()),
68            _ => None,
69        }
70    }
71
72    /// Generate appropriate error message based on constraints
73    fn create_error_message(&self, field: &str, actual_length: usize) -> String {
74        if let Some(ref custom_message) = self.message {
75            return custom_message.clone();
76        }
77
78        if let Some(exact) = self.exact {
79            return format!("{} must be exactly {} characters long", field, exact);
80        }
81
82        match (self.min, self.max) {
83            (Some(min), Some(max)) if min == max => {
84                format!("{} must be exactly {} characters long", field, min)
85            }
86            (Some(min), Some(max)) => {
87                format!("{} must be between {} and {} characters long", field, min, max)
88            }
89            (Some(min), None) => {
90                format!("{} must be at least {} characters long", field, min)
91            }
92            (None, Some(max)) => {
93                format!("{} must be at most {} characters long", field, max)
94            }
95            (None, None) => {
96                format!("{} has invalid length: {}", field, actual_length)
97            }
98        }
99    }
100}
101
102impl Default for LengthValidator {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108#[async_trait]
109impl ValidationRule for LengthValidator {
110    async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()> {
111        // Skip validation for null values (use RequiredValidator for null checks)
112        if value.is_null() {
113            return Ok(());
114        }
115
116        let length = match self.get_length(value) {
117            Some(len) => len,
118            None => {
119                return Err(ValidationError::with_code(
120                    field,
121                    format!("{} must be a string or array for length validation", field),
122                    "invalid_type",
123                ).into());
124            }
125        };
126
127        // Check exact length first
128        if let Some(exact) = self.exact {
129            if length != exact {
130                return Err(ValidationError::with_code(
131                    field,
132                    self.create_error_message(field, length),
133                    "length_exact",
134                ).into());
135            }
136            return Ok(());
137        }
138
139        // Check minimum length
140        if let Some(min) = self.min {
141            if length < min {
142                return Err(ValidationError::with_code(
143                    field,
144                    self.create_error_message(field, length),
145                    "length_min",
146                ).into());
147            }
148        }
149
150        // Check maximum length
151        if let Some(max) = self.max {
152            if length > max {
153                return Err(ValidationError::with_code(
154                    field,
155                    self.create_error_message(field, length),
156                    "length_max",
157                ).into());
158            }
159        }
160
161        Ok(())
162    }
163
164    fn rule_name(&self) -> &'static str {
165        "length"
166    }
167
168    fn parameters(&self) -> Option<Value> {
169        let mut params = serde_json::Map::new();
170        
171        if let Some(min) = self.min {
172            params.insert("min".to_string(), Value::Number(serde_json::Number::from(min)));
173        }
174        if let Some(max) = self.max {
175            params.insert("max".to_string(), Value::Number(serde_json::Number::from(max)));
176        }
177        if let Some(exact) = self.exact {
178            params.insert("exact".to_string(), Value::Number(serde_json::Number::from(exact)));
179        }
180        if let Some(ref message) = self.message {
181            params.insert("message".to_string(), Value::String(message.clone()));
182        }
183
184        if params.is_empty() {
185            None
186        } else {
187            Some(Value::Object(params))
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[tokio::test]
197    async fn test_length_validator_min_constraint() {
198        let validator = LengthValidator::new().min(3);
199        
200        // Too short
201        let result = validator.validate(&Value::String("hi".to_string()), "name").await;
202        assert!(result.is_err());
203        
204        // Exact minimum
205        let result = validator.validate(&Value::String("bob".to_string()), "name").await;
206        assert!(result.is_ok());
207        
208        // Longer than minimum
209        let result = validator.validate(&Value::String("alice".to_string()), "name").await;
210        assert!(result.is_ok());
211    }
212
213    #[tokio::test]
214    async fn test_length_validator_max_constraint() {
215        let validator = LengthValidator::new().max(5);
216        
217        // Within limit
218        let result = validator.validate(&Value::String("hello".to_string()), "name").await;
219        assert!(result.is_ok());
220        
221        // Too long
222        let result = validator.validate(&Value::String("hello world".to_string()), "name").await;
223        assert!(result.is_err());
224    }
225
226    #[tokio::test]
227    async fn test_length_validator_exact_constraint() {
228        let validator = LengthValidator::new().exact(4);
229        
230        // Correct length
231        let result = validator.validate(&Value::String("test".to_string()), "code").await;
232        assert!(result.is_ok());
233        
234        // Too short
235        let result = validator.validate(&Value::String("hi".to_string()), "code").await;
236        assert!(result.is_err());
237        
238        // Too long
239        let result = validator.validate(&Value::String("testing".to_string()), "code").await;
240        assert!(result.is_err());
241    }
242
243    #[tokio::test]
244    async fn test_length_validator_range() {
245        let validator = LengthValidator::new().range(3, 10);
246        
247        // Too short
248        let result = validator.validate(&Value::String("hi".to_string()), "password").await;
249        assert!(result.is_err());
250        
251        // Within range
252        let result = validator.validate(&Value::String("secret".to_string()), "password").await;
253        assert!(result.is_ok());
254        
255        // Too long
256        let result = validator.validate(&Value::String("very_long_password".to_string()), "password").await;
257        assert!(result.is_err());
258    }
259
260    #[tokio::test]
261    async fn test_length_validator_with_arrays() {
262        let validator = LengthValidator::new().min(2).max(4);
263        
264        // Too few items
265        let result = validator.validate(&Value::Array(vec![Value::String("item1".to_string())]), "tags").await;
266        assert!(result.is_err());
267        
268        // Within range
269        let result = validator.validate(&Value::Array(vec![
270            Value::String("tag1".to_string()),
271            Value::String("tag2".to_string()),
272        ]), "tags").await;
273        assert!(result.is_ok());
274        
275        // Too many items
276        let result = validator.validate(&Value::Array(vec![
277            Value::String("tag1".to_string()),
278            Value::String("tag2".to_string()),
279            Value::String("tag3".to_string()),
280            Value::String("tag4".to_string()),
281            Value::String("tag5".to_string()),
282        ]), "tags").await;
283        assert!(result.is_err());
284    }
285
286    #[tokio::test]
287    async fn test_length_validator_unicode_support() {
288        let validator = LengthValidator::new().max(5);
289        
290        // Unicode characters should be counted correctly
291        let result = validator.validate(&Value::String("café".to_string()), "name").await;
292        assert!(result.is_ok());
293        
294        let result = validator.validate(&Value::String("🦀🚀✨".to_string()), "emoji").await;
295        assert!(result.is_ok());
296        
297        // This should be 6 characters, exceeding the max of 5
298        let result = validator.validate(&Value::String("🦀🚀✨🎉🔥💯".to_string()), "emoji").await;
299        assert!(result.is_err());
300    }
301
302    #[tokio::test]
303    async fn test_length_validator_with_null() {
304        let validator = LengthValidator::new().min(1);
305        
306        // Null values should be skipped (not validated)
307        let result = validator.validate(&Value::Null, "optional_field").await;
308        assert!(result.is_ok());
309    }
310
311    #[tokio::test]
312    async fn test_length_validator_invalid_type() {
313        let validator = LengthValidator::new().min(1);
314        
315        // Numbers should fail type validation
316        let result = validator.validate(&Value::Number(serde_json::Number::from(42)), "age").await;
317        assert!(result.is_err());
318        
319        let errors = result.unwrap_err();
320        let field_errors = errors.get_field_errors("age").unwrap();
321        assert_eq!(field_errors[0].code, "invalid_type");
322    }
323
324    #[tokio::test]
325    async fn test_length_validator_custom_message() {
326        let validator = LengthValidator::new()
327            .min(8)
328            .message("Password must be strong");
329        
330        let result = validator.validate(&Value::String("weak".to_string()), "password").await;
331        assert!(result.is_err());
332        
333        let errors = result.unwrap_err();
334        let field_errors = errors.get_field_errors("password").unwrap();
335        assert_eq!(field_errors[0].message, "Password must be strong");
336    }
337}