elif_validation/validators/
pattern.rs1use crate::error::{ValidationError, ValidationResult};
4use crate::traits::ValidationRule;
5use async_trait::async_trait;
6use regex::Regex;
7use serde_json::Value;
8
9#[derive(Debug, Clone)]
11pub struct PatternValidator {
12 pattern: Regex,
14 pub message: Option<String>,
16 pub full_match: bool,
18 pub case_sensitive: bool,
20}
21
22impl PatternValidator {
23 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 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 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 pub fn message(mut self, message: impl Into<String>) -> Self {
58 self.message = Some(message.into());
59 self
60 }
61
62 pub fn full_match(mut self, full_match: bool) -> Self {
64 self.full_match = full_match;
65 self
66 }
67
68 pub fn pattern_string(&self) -> &str {
70 self.pattern.as_str()
71 }
72
73 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 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
132impl PatternValidator {
134 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 pub fn alphabetic() -> Self {
143 Self::new(r"^[a-zA-Z]+$")
144 .unwrap()
145 .message("Must contain only letters")
146 }
147
148 pub fn numeric_string() -> Self {
150 Self::new(r"^[0-9]+$")
151 .unwrap()
152 .message("Must contain only numbers")
153 }
154
155 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 pub fn url() -> Self {
164 Self::new(r"^https?://[^\s/$.?#].[^\s]*$")
165 .unwrap()
166 .message("Must be a valid URL")
167 }
168
169 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 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; 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 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 pub fn strong_password() -> Self {
196 Self::new(r"^.{8,}$") .unwrap()
198 .message("Password must be at least 8 characters long")
199 }
200
201 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 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 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 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 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 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 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); 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 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 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 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 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", "123-456-78901", "abc-def-ghij", "123", ];
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 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 assert!(validator.validate(&Value::String("FF5733".to_string()), "color").await.is_err()); assert!(validator.validate(&Value::String("#FF57".to_string()), "color").await.is_err()); assert!(validator.validate(&Value::String("#GG5733".to_string()), "color").await.is_err()); }
338
339 #[tokio::test]
340 async fn test_pattern_validator_uuid_v4() {
341 let validator = PatternValidator::uuid_v4();
342
343 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()); assert!(validator.validate(&Value::String("550e8400-e29b-41d4-a716".to_string()), "id").await.is_err()); assert!(validator.validate(&Value::String("not-a-uuid".to_string()), "id").await.is_err()); }
351
352 #[tokio::test]
353 async fn test_pattern_validator_strong_password() {
354 let validator = PatternValidator::strong_password();
355
356 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 assert!(validator.validate(&Value::String("P@ss1".to_string()), "password").await.is_err()); assert!(validator.validate(&Value::String("1234567".to_string()), "password").await.is_err()); }
365
366 #[tokio::test]
367 async fn test_pattern_validator_ipv4() {
368 let validator = PatternValidator::ipv4();
369
370 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 assert!(validator.validate(&Value::String("256.1.1.1".to_string()), "ip").await.is_err()); assert!(validator.validate(&Value::String("192.168.1".to_string()), "ip").await.is_err()); assert!(validator.validate(&Value::String("192.168.1.1.1".to_string()), "ip").await.is_err()); }
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 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 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 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 assert!(validator.validate(&Value::String("1234".to_string()), "zip").await.is_err()); assert!(validator.validate(&Value::String("123456".to_string()), "zip").await.is_err()); assert!(validator.validate(&Value::String("abcde".to_string()), "zip").await.is_err()); }
430}