elif_validation/validators/
pattern.rs

1//! Pattern-based validators using regular expressions
2
3use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use regex::Regex;
7use serde_json::Value;
8
9/// Validator for custom regular expression patterns
10#[derive(Debug, Clone)]
11pub struct PatternValidator {
12    /// The regular expression pattern
13    pattern: Regex,
14    /// Custom error message
15    pub message: Option<String>,
16    /// Whether to match the entire string (default) or just find a match
17    pub full_match: bool,
18    /// Case-sensitive matching (default: true)
19    pub case_sensitive: bool,
20}
21
22impl PatternValidator {
23    /// Create a new pattern validator
24    pub fn new(pattern: &str) -> Result<Self, regex::Error> {
25        let regex = Regex::new(pattern)?;
26        Ok(Self {
27            pattern: regex,
28            message: None,
29            full_match: true,
30            case_sensitive: true,
31        })
32    }
33
34    /// Create a case-insensitive pattern validator
35    pub fn new_case_insensitive(pattern: &str) -> Result<Self, regex::Error> {
36        let case_insensitive_pattern = format!("(?i){}", pattern);
37        let regex = Regex::new(&case_insensitive_pattern)?;
38        Ok(Self {
39            pattern: regex,
40            message: None,
41            full_match: true,
42            case_sensitive: false,
43        })
44    }
45
46    /// Create a validator from an existing Regex
47    pub fn from_regex(regex: Regex) -> Self {
48        Self {
49            pattern: regex,
50            message: None,
51            full_match: true,
52            case_sensitive: true,
53        }
54    }
55
56    /// Set custom error message
57    pub fn message(mut self, message: impl Into<String>) -> Self {
58        self.message = Some(message.into());
59        self
60    }
61
62    /// Set whether to match the full string or just find a match
63    pub fn full_match(mut self, full_match: bool) -> Self {
64        self.full_match = full_match;
65        self
66    }
67
68    /// Get the pattern string
69    pub fn pattern_string(&self) -> &str {
70        self.pattern.as_str()
71    }
72
73    /// Validate the string against the pattern
74    fn validate_pattern(&self, text: &str) -> bool {
75        if self.full_match {
76            self.pattern.is_match(text) && self.pattern.find(text).is_some_and(|m| m.as_str() == text)
77        } else {
78            self.pattern.is_match(text)
79        }
80    }
81}
82
83#[async_trait]
84impl ValidationRule for PatternValidator {
85    async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()> {
86        // Skip validation for null values
87        if value.is_null() {
88            return Ok(());
89        }
90
91        let text = match value.as_str() {
92            Some(text) => text,
93            None => {
94                return Err(ValidationError::with_code(
95                    field,
96                    format!("{} must be a string for pattern validation", field),
97                    "invalid_type",
98                ).into());
99            }
100        };
101
102        if !self.validate_pattern(text) {
103            let message = self
104                .message.clone()
105                .unwrap_or_else(|| format!("{} does not match the required pattern", field));
106
107            return Err(ValidationError::with_code(field, message, "pattern_mismatch").into());
108        }
109
110        Ok(())
111    }
112
113    fn rule_name(&self) -> &'static str {
114        "pattern"
115    }
116
117    fn parameters(&self) -> Option<Value> {
118        let mut params = serde_json::Map::new();
119        
120        params.insert("pattern".to_string(), Value::String(self.pattern.as_str().to_string()));
121        params.insert("full_match".to_string(), Value::Bool(self.full_match));
122        params.insert("case_sensitive".to_string(), Value::Bool(self.case_sensitive));
123        
124        if let Some(ref message) = self.message {
125            params.insert("message".to_string(), Value::String(message.clone()));
126        }
127
128        Some(Value::Object(params))
129    }
130}
131
132/// Common pattern validators for typical use cases
133impl PatternValidator {
134    /// Create a validator for alphanumeric strings only
135    pub fn alphanumeric() -> Self {
136        Self::new(r"^[a-zA-Z0-9]+$")
137            .unwrap()
138            .message("Must contain only letters and numbers")
139    }
140
141    /// Create a validator for alphabetic characters only
142    pub fn alphabetic() -> Self {
143        Self::new(r"^[a-zA-Z]+$")
144            .unwrap()
145            .message("Must contain only letters")
146    }
147
148    /// Create a validator for numeric strings only
149    pub fn numeric_string() -> Self {
150        Self::new(r"^[0-9]+$")
151            .unwrap()
152            .message("Must contain only numbers")
153    }
154
155    /// Create a validator for phone numbers (US format)
156    pub fn phone_us() -> Self {
157        Self::new(r"^\+?1?[-.\s]?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}$")
158            .unwrap()
159            .message("Must be a valid US phone number")
160    }
161
162    /// Create a validator for URLs
163    pub fn url() -> Self {
164        Self::new(r"^https?://[^\s/$.?#].[^\s]*$")
165            .unwrap()
166            .message("Must be a valid URL")
167    }
168
169    /// Create a validator for hexadecimal color codes
170    pub fn hex_color() -> Self {
171        Self::new(r"^#[0-9a-fA-F]{6}$")
172            .unwrap()
173            .message("Must be a valid hex color code (e.g., #FF5733)")
174    }
175
176    /// Create a validator for UUID v4
177    pub fn uuid_v4() -> Self {
178        Self::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
179            .unwrap()
180            .case_sensitive = false; // UUIDs can be uppercase or lowercase
181        Self::new_case_insensitive(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
182            .unwrap()
183            .message("Must be a valid UUID v4")
184    }
185
186    /// Create a validator for slug/URL-friendly strings
187    pub fn slug() -> Self {
188        Self::new(r"^[a-z0-9-]+$")
189            .unwrap()
190            .message("Must be a valid slug (lowercase letters, numbers, and hyphens only)")
191    }
192
193    /// Create a validator for strong passwords (simplified - checks length only)
194    /// Note: For complete password strength validation, use multiple validators
195    pub fn strong_password() -> Self {
196        Self::new(r"^.{8,}$") // Minimum 8 characters
197            .unwrap()
198            .message("Password must be at least 8 characters long")
199    }
200
201    /// Create a validator for IP addresses (IPv4)
202    pub fn ipv4() -> Self {
203        Self::new(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")
204            .unwrap()
205            .message("Must be a valid IPv4 address")
206    }
207
208    /// Create a validator for MAC addresses
209    pub fn mac_address() -> Self {
210        Self::new(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
211            .unwrap()
212            .message("Must be a valid MAC address (e.g., AA:BB:CC:DD:EE:FF)")
213    }
214
215    /// Create a validator for credit card numbers (basic Luhn algorithm check)
216    pub fn credit_card() -> Self {
217        Self::new(r"^[0-9]{13,19}$")
218            .unwrap()
219            .message("Must be a valid credit card number")
220    }
221
222    /// Create a validator for social security numbers (US format)
223    pub fn ssn_us() -> Self {
224        Self::new(r"^\d{3}-\d{2}-\d{4}$")
225            .unwrap()
226            .message("Must be a valid SSN format (XXX-XX-XXXX)")
227    }
228
229    /// Create a validator for postal codes (US ZIP codes)
230    pub fn zip_code_us() -> Self {
231        Self::new(r"^\d{5}(-\d{4})?$")
232            .unwrap()
233            .message("Must be a valid US ZIP code")
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[tokio::test]
242    async fn test_pattern_validator_basic() {
243        let validator = PatternValidator::new(r"^[a-zA-Z]+$").unwrap();
244        
245        // Valid alphabetic strings
246        assert!(validator.validate(&Value::String("hello".to_string()), "name").await.is_ok());
247        assert!(validator.validate(&Value::String("World".to_string()), "name").await.is_ok());
248        
249        // Invalid strings (contain numbers or special chars)
250        assert!(validator.validate(&Value::String("hello123".to_string()), "name").await.is_err());
251        assert!(validator.validate(&Value::String("hello@world".to_string()), "name").await.is_err());
252    }
253
254    #[tokio::test]
255    async fn test_pattern_validator_full_match() {
256        let validator = PatternValidator::new(r"abc")
257            .unwrap()
258            .full_match(false); // Just find a match, don't require full match
259        
260        // Should match strings containing "abc"
261        assert!(validator.validate(&Value::String("abcdef".to_string()), "text").await.is_ok());
262        assert!(validator.validate(&Value::String("123abc456".to_string()), "text").await.is_ok());
263        
264        // Should not match strings without "abc"
265        assert!(validator.validate(&Value::String("def".to_string()), "text").await.is_err());
266    }
267
268    #[tokio::test]
269    async fn test_pattern_validator_case_insensitive() {
270        let validator = PatternValidator::new_case_insensitive(r"^hello$").unwrap();
271        
272        // Should match regardless of case
273        assert!(validator.validate(&Value::String("hello".to_string()), "greeting").await.is_ok());
274        assert!(validator.validate(&Value::String("HELLO".to_string()), "greeting").await.is_ok());
275        assert!(validator.validate(&Value::String("Hello".to_string()), "greeting").await.is_ok());
276        
277        // Should not match different words
278        assert!(validator.validate(&Value::String("world".to_string()), "greeting").await.is_err());
279    }
280
281    #[tokio::test]
282    async fn test_pattern_validator_alphanumeric() {
283        let validator = PatternValidator::alphanumeric();
284        
285        assert!(validator.validate(&Value::String("abc123".to_string()), "username").await.is_ok());
286        assert!(validator.validate(&Value::String("user123".to_string()), "username").await.is_ok());
287        
288        // Should not allow special characters
289        assert!(validator.validate(&Value::String("user@123".to_string()), "username").await.is_err());
290        assert!(validator.validate(&Value::String("user 123".to_string()), "username").await.is_err());
291    }
292
293    #[tokio::test]
294    async fn test_pattern_validator_phone_us() {
295        let validator = PatternValidator::phone_us();
296        
297        let valid_phones = vec![
298            "123-456-7890",
299            "(123) 456-7890",
300            "123.456.7890",
301            "123 456 7890",
302            "+1-123-456-7890",
303            "1234567890",
304        ];
305
306        for phone in valid_phones {
307            let result = validator.validate(&Value::String(phone.to_string()), "phone").await;
308            assert!(result.is_ok(), "Phone '{}' should be valid", phone);
309        }
310
311        let invalid_phones = vec![
312            "123-45-6789",   // Too few digits
313            "123-456-78901", // Too many digits
314            "abc-def-ghij",  // Non-numeric
315            "123",           // Too short
316        ];
317
318        for phone in invalid_phones {
319            let result = validator.validate(&Value::String(phone.to_string()), "phone").await;
320            assert!(result.is_err(), "Phone '{}' should be invalid", phone);
321        }
322    }
323
324    #[tokio::test]
325    async fn test_pattern_validator_hex_color() {
326        let validator = PatternValidator::hex_color();
327        
328        // Valid hex colors
329        assert!(validator.validate(&Value::String("#FF5733".to_string()), "color").await.is_ok());
330        assert!(validator.validate(&Value::String("#000000".to_string()), "color").await.is_ok());
331        assert!(validator.validate(&Value::String("#ffffff".to_string()), "color").await.is_ok());
332        
333        // Invalid hex colors
334        assert!(validator.validate(&Value::String("FF5733".to_string()), "color").await.is_err()); // Missing #
335        assert!(validator.validate(&Value::String("#FF57".to_string()), "color").await.is_err()); // Too short
336        assert!(validator.validate(&Value::String("#GG5733".to_string()), "color").await.is_err()); // Invalid chars
337    }
338
339    #[tokio::test]
340    async fn test_pattern_validator_uuid_v4() {
341        let validator = PatternValidator::uuid_v4();
342        
343        // Valid UUID v4
344        assert!(validator.validate(&Value::String("550e8400-e29b-41d4-a716-446655440000".to_string()), "id").await.is_ok());
345        assert!(validator.validate(&Value::String("6ba7b810-9dad-11d1-80b4-00c04fd430c8".to_string()), "id").await.is_err()); // Not v4
346        
347        // Invalid UUIDs
348        assert!(validator.validate(&Value::String("550e8400-e29b-41d4-a716".to_string()), "id").await.is_err()); // Too short
349        assert!(validator.validate(&Value::String("not-a-uuid".to_string()), "id").await.is_err()); // Invalid format
350    }
351
352    #[tokio::test]
353    async fn test_pattern_validator_strong_password() {
354        let validator = PatternValidator::strong_password();
355        
356        // Valid passwords (8+ characters)
357        assert!(validator.validate(&Value::String("Password123!".to_string()), "password").await.is_ok());
358        assert!(validator.validate(&Value::String("MyP@ssw0rd".to_string()), "password").await.is_ok());
359        assert!(validator.validate(&Value::String("12345678".to_string()), "password").await.is_ok());
360        
361        // Invalid passwords (too short)
362        assert!(validator.validate(&Value::String("P@ss1".to_string()), "password").await.is_err()); // Too short
363        assert!(validator.validate(&Value::String("1234567".to_string()), "password").await.is_err()); // Too short
364    }
365
366    #[tokio::test]
367    async fn test_pattern_validator_ipv4() {
368        let validator = PatternValidator::ipv4();
369        
370        // Valid IPv4 addresses
371        assert!(validator.validate(&Value::String("192.168.1.1".to_string()), "ip").await.is_ok());
372        assert!(validator.validate(&Value::String("0.0.0.0".to_string()), "ip").await.is_ok());
373        assert!(validator.validate(&Value::String("255.255.255.255".to_string()), "ip").await.is_ok());
374        
375        // Invalid IPv4 addresses
376        assert!(validator.validate(&Value::String("256.1.1.1".to_string()), "ip").await.is_err()); // Out of range
377        assert!(validator.validate(&Value::String("192.168.1".to_string()), "ip").await.is_err()); // Incomplete
378        assert!(validator.validate(&Value::String("192.168.1.1.1".to_string()), "ip").await.is_err()); // Too many octets
379    }
380
381    #[tokio::test]
382    async fn test_pattern_validator_custom_message() {
383        let validator = PatternValidator::new(r"^[A-Z]+$")
384            .unwrap()
385            .message("Must be all uppercase letters");
386        
387        let result = validator.validate(&Value::String("hello".to_string()), "code").await;
388        assert!(result.is_err());
389        
390        let errors = result.unwrap_err();
391        let field_errors = errors.get_field_errors("code").unwrap();
392        assert_eq!(field_errors[0].message, "Must be all uppercase letters");
393    }
394
395    #[tokio::test]
396    async fn test_pattern_validator_with_null() {
397        let validator = PatternValidator::new(r"^[a-z]+$").unwrap();
398        
399        // Null values should be skipped
400        let result = validator.validate(&Value::Null, "optional_field").await;
401        assert!(result.is_ok());
402    }
403
404    #[tokio::test]
405    async fn test_pattern_validator_invalid_type() {
406        let validator = PatternValidator::new(r"^[a-z]+$").unwrap();
407        
408        // Numbers should fail type validation
409        let result = validator.validate(&Value::Number(serde_json::Number::from(42)), "field").await;
410        assert!(result.is_err());
411        
412        let errors = result.unwrap_err();
413        let field_errors = errors.get_field_errors("field").unwrap();
414        assert_eq!(field_errors[0].code, "invalid_type");
415    }
416
417    #[tokio::test]
418    async fn test_pattern_validator_zip_code_us() {
419        let validator = PatternValidator::zip_code_us();
420        
421        // Valid ZIP codes
422        assert!(validator.validate(&Value::String("12345".to_string()), "zip").await.is_ok());
423        assert!(validator.validate(&Value::String("12345-6789".to_string()), "zip").await.is_ok());
424        
425        // Invalid ZIP codes
426        assert!(validator.validate(&Value::String("1234".to_string()), "zip").await.is_err()); // Too short
427        assert!(validator.validate(&Value::String("123456".to_string()), "zip").await.is_err()); // Too long without dash
428        assert!(validator.validate(&Value::String("abcde".to_string()), "zip").await.is_err()); // Non-numeric
429    }
430}