armature_validation/
validators.rs

1// Built-in validators
2
3use crate::ValidationError;
4use once_cell::sync::Lazy;
5use regex::Regex;
6
7// Common regex patterns
8static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
9    Regex::new(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").unwrap()
10});
11
12static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap());
13
14static UUID_REGEX: Lazy<Regex> = Lazy::new(|| {
15    Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap()
16});
17
18static ALPHA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z]+$").unwrap());
19
20static ALPHANUMERIC_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9]+$").unwrap());
21
22static NUMERIC_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[0-9]+$").unwrap());
23
24// String validators
25
26/// Validates that a string is not empty
27pub struct NotEmpty;
28
29impl NotEmpty {
30    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
31        if value.trim().is_empty() {
32            Err(
33                ValidationError::new(field, format!("{} should not be empty", field))
34                    .with_constraint("notEmpty"),
35            )
36        } else {
37            Ok(())
38        }
39    }
40}
41
42/// Validates minimum string length
43pub struct MinLength(pub usize);
44
45impl MinLength {
46    pub fn validate(&self, value: &str, field: &str) -> Result<(), ValidationError> {
47        if value.len() < self.0 {
48            Err(ValidationError::new(
49                field,
50                format!("{} must be at least {} characters", field, self.0),
51            )
52            .with_constraint("minLength")
53            .with_value(value.to_string()))
54        } else {
55            Ok(())
56        }
57    }
58}
59
60/// Validates maximum string length
61pub struct MaxLength(pub usize);
62
63impl MaxLength {
64    pub fn validate(&self, value: &str, field: &str) -> Result<(), ValidationError> {
65        if value.len() > self.0 {
66            Err(ValidationError::new(
67                field,
68                format!("{} must be at most {} characters", field, self.0),
69            )
70            .with_constraint("maxLength")
71            .with_value(value.to_string()))
72        } else {
73            Ok(())
74        }
75    }
76}
77
78/// Validates email format
79pub struct IsEmail;
80
81impl IsEmail {
82    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
83        if EMAIL_REGEX.is_match(value) {
84            Ok(())
85        } else {
86            Err(
87                ValidationError::new(field, format!("{} must be a valid email", field))
88                    .with_constraint("isEmail")
89                    .with_value(value.to_string()),
90            )
91        }
92    }
93}
94
95/// Validates URL format
96pub struct IsUrl;
97
98impl IsUrl {
99    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
100        if URL_REGEX.is_match(value) {
101            Ok(())
102        } else {
103            Err(
104                ValidationError::new(field, format!("{} must be a valid URL", field))
105                    .with_constraint("isUrl")
106                    .with_value(value.to_string()),
107            )
108        }
109    }
110}
111
112/// Validates UUID format
113pub struct IsUuid;
114
115impl IsUuid {
116    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
117        if UUID_REGEX.is_match(value) {
118            Ok(())
119        } else {
120            Err(
121                ValidationError::new(field, format!("{} must be a valid UUID", field))
122                    .with_constraint("isUuid")
123                    .with_value(value.to_string()),
124            )
125        }
126    }
127}
128
129/// Validates alphabetic characters only
130pub struct IsAlpha;
131
132impl IsAlpha {
133    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
134        if ALPHA_REGEX.is_match(value) {
135            Ok(())
136        } else {
137            Err(
138                ValidationError::new(field, format!("{} must contain only letters", field))
139                    .with_constraint("isAlpha")
140                    .with_value(value.to_string()),
141            )
142        }
143    }
144}
145
146/// Validates alphanumeric characters only
147pub struct IsAlphanumeric;
148
149impl IsAlphanumeric {
150    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
151        if ALPHANUMERIC_REGEX.is_match(value) {
152            Ok(())
153        } else {
154            Err(ValidationError::new(
155                field,
156                format!("{} must contain only letters and numbers", field),
157            )
158            .with_constraint("isAlphanumeric")
159            .with_value(value.to_string()))
160        }
161    }
162}
163
164/// Validates numeric characters only
165pub struct IsNumeric;
166
167impl IsNumeric {
168    pub fn validate(value: &str, field: &str) -> Result<(), ValidationError> {
169        if NUMERIC_REGEX.is_match(value) {
170            Ok(())
171        } else {
172            Err(
173                ValidationError::new(field, format!("{} must contain only numbers", field))
174                    .with_constraint("isNumeric")
175                    .with_value(value.to_string()),
176            )
177        }
178    }
179}
180
181// Number validators
182
183/// Validates minimum value
184pub struct Min<T>(pub T);
185
186impl Min<i32> {
187    pub fn validate(&self, value: i32, field: &str) -> Result<(), ValidationError> {
188        if value < self.0 {
189            Err(
190                ValidationError::new(field, format!("{} must be at least {}", field, self.0))
191                    .with_constraint("min")
192                    .with_value(value.to_string()),
193            )
194        } else {
195            Ok(())
196        }
197    }
198}
199
200impl Min<f64> {
201    pub fn validate(&self, value: f64, field: &str) -> Result<(), ValidationError> {
202        if value < self.0 {
203            Err(
204                ValidationError::new(field, format!("{} must be at least {}", field, self.0))
205                    .with_constraint("min")
206                    .with_value(value.to_string()),
207            )
208        } else {
209            Ok(())
210        }
211    }
212}
213
214/// Validates maximum value
215pub struct Max<T>(pub T);
216
217impl Max<i32> {
218    pub fn validate(&self, value: i32, field: &str) -> Result<(), ValidationError> {
219        if value > self.0 {
220            Err(
221                ValidationError::new(field, format!("{} must be at most {}", field, self.0))
222                    .with_constraint("max")
223                    .with_value(value.to_string()),
224            )
225        } else {
226            Ok(())
227        }
228    }
229}
230
231impl Max<f64> {
232    pub fn validate(&self, value: f64, field: &str) -> Result<(), ValidationError> {
233        if value > self.0 {
234            Err(
235                ValidationError::new(field, format!("{} must be at most {}", field, self.0))
236                    .with_constraint("max")
237                    .with_value(value.to_string()),
238            )
239        } else {
240            Ok(())
241        }
242    }
243}
244
245/// Validates value is positive
246pub struct IsPositive;
247
248impl IsPositive {
249    pub fn validate_i32(value: i32, field: &str) -> Result<(), ValidationError> {
250        if value > 0 {
251            Ok(())
252        } else {
253            Err(
254                ValidationError::new(field, format!("{} must be a positive number", field))
255                    .with_constraint("isPositive")
256                    .with_value(value.to_string()),
257            )
258        }
259    }
260
261    pub fn validate_f64(value: f64, field: &str) -> Result<(), ValidationError> {
262        if value > 0.0 {
263            Ok(())
264        } else {
265            Err(
266                ValidationError::new(field, format!("{} must be a positive number", field))
267                    .with_constraint("isPositive")
268                    .with_value(value.to_string()),
269            )
270        }
271    }
272}
273
274/// Validates value is in range
275pub struct InRange<T> {
276    pub min: T,
277    pub max: T,
278}
279
280impl InRange<i32> {
281    pub fn validate(&self, value: i32, field: &str) -> Result<(), ValidationError> {
282        if value >= self.min && value <= self.max {
283            Ok(())
284        } else {
285            Err(ValidationError::new(
286                field,
287                format!("{} must be between {} and {}", field, self.min, self.max),
288            )
289            .with_constraint("inRange")
290            .with_value(value.to_string()))
291        }
292    }
293}
294
295/// Custom regex validator
296pub struct Matches(pub Regex);
297
298impl Matches {
299    pub fn new(pattern: &str) -> Result<Self, regex::Error> {
300        Ok(Self(Regex::new(pattern)?))
301    }
302
303    pub fn validate(&self, value: &str, field: &str) -> Result<(), ValidationError> {
304        if self.0.is_match(value) {
305            Ok(())
306        } else {
307            Err(
308                ValidationError::new(field, format!("{} does not match required pattern", field))
309                    .with_constraint("matches")
310                    .with_value(value.to_string()),
311            )
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_not_empty() {
322        assert!(NotEmpty::validate("test", "field").is_ok());
323        assert!(NotEmpty::validate("", "field").is_err());
324        assert!(NotEmpty::validate("   ", "field").is_err());
325    }
326
327    #[test]
328    fn test_min_length() {
329        let validator = MinLength(5);
330        assert!(validator.validate("hello", "field").is_ok());
331        assert!(validator.validate("hi", "field").is_err());
332    }
333
334    #[test]
335    fn test_is_email() {
336        assert!(IsEmail::validate("test@example.com", "email").is_ok());
337        assert!(IsEmail::validate("invalid", "email").is_err());
338    }
339
340    #[test]
341    fn test_min_value() {
342        let validator = Min(10);
343        assert!(validator.validate(15, "age").is_ok());
344        assert!(validator.validate(5, "age").is_err());
345    }
346
347    #[test]
348    fn test_max_length() {
349        let validator = MaxLength(10);
350        assert!(validator.validate("short", "field").is_ok());
351        assert!(validator.validate("this is too long", "field").is_err());
352    }
353
354    #[test]
355    fn test_max_value() {
356        let validator = Max(100i32);
357        assert!(validator.validate(50i32, "value").is_ok());
358        assert!(validator.validate(150i32, "value").is_err());
359    }
360
361    #[test]
362    fn test_in_range() {
363        let validator = InRange {
364            min: 10i32,
365            max: 20i32,
366        };
367        assert!(validator.validate(15i32, "value").is_ok());
368        assert!(validator.validate(5i32, "value").is_err());
369        assert!(validator.validate(25i32, "value").is_err());
370    }
371
372    #[test]
373    fn test_is_url() {
374        assert!(IsUrl::validate("https://example.com", "url").is_ok());
375        assert!(IsUrl::validate("http://test.org/path", "url").is_ok());
376        assert!(IsUrl::validate("not a url", "url").is_err());
377    }
378
379    #[test]
380    fn test_is_alpha() {
381        assert!(IsAlpha::validate("abcXYZ", "field").is_ok());
382        assert!(IsAlpha::validate("abc123", "field").is_err());
383        assert!(IsAlpha::validate("abc xyz", "field").is_err());
384    }
385
386    #[test]
387    fn test_is_alphanumeric() {
388        assert!(IsAlphanumeric::validate("abc123", "field").is_ok());
389        assert!(IsAlphanumeric::validate("abc@123", "field").is_err());
390        assert!(IsAlphanumeric::validate("test", "field").is_ok());
391    }
392
393    #[test]
394    fn test_is_numeric() {
395        assert!(IsNumeric::validate("12345", "field").is_ok());
396        assert!(IsNumeric::validate("123.45", "field").is_err());
397        assert!(IsNumeric::validate("abc", "field").is_err());
398    }
399
400    #[test]
401    fn test_is_uuid() {
402        assert!(IsUuid::validate("550e8400-e29b-41d4-a716-446655440000", "id").is_ok());
403        assert!(IsUuid::validate("not-a-uuid", "id").is_err());
404        assert!(IsUuid::validate("", "id").is_err());
405    }
406
407    #[test]
408    fn test_not_empty_with_whitespace_only() {
409        assert!(NotEmpty::validate("\t\n  \r", "field").is_err());
410    }
411
412    #[test]
413    fn test_min_length_exact() {
414        let validator = MinLength(5);
415        assert!(validator.validate("exact", "field").is_ok());
416        assert!(validator.validate("four", "field").is_err());
417    }
418
419    #[test]
420    fn test_max_length_exact() {
421        let validator = MaxLength(5);
422        assert!(validator.validate("exact", "field").is_ok());
423        assert!(validator.validate("sixsix", "field").is_err());
424    }
425
426    #[test]
427    fn test_in_range_boundaries() {
428        let validator = InRange {
429            min: 0i32,
430            max: 10i32,
431        };
432        assert!(validator.validate(0i32, "value").is_ok());
433        assert!(validator.validate(10i32, "value").is_ok());
434        assert!(validator.validate(-1i32, "value").is_err());
435        assert!(validator.validate(11i32, "value").is_err());
436    }
437
438    #[test]
439    fn test_email_variations() {
440        assert!(IsEmail::validate("user+tag@example.com", "email").is_ok());
441        assert!(IsEmail::validate("user.name@example.co.uk", "email").is_ok());
442        assert!(IsEmail::validate("@example.com", "email").is_err());
443        assert!(IsEmail::validate("user@", "email").is_err());
444    }
445
446    #[test]
447    fn test_url_variations() {
448        assert!(IsUrl::validate("https://example.com", "url").is_ok());
449        assert!(IsUrl::validate("http://test.com/path", "url").is_ok());
450        assert!(IsUrl::validate("//example.com", "url").is_err());
451    }
452
453    #[test]
454    fn test_uuid_formats() {
455        // UUID v4 format
456        assert!(IsUuid::validate("123e4567-e89b-12d3-a456-426614174000", "id").is_ok());
457        // Without hyphens should fail
458        assert!(IsUuid::validate("123e4567e89b12d3a456426614174000", "id").is_err());
459    }
460
461    #[test]
462    fn test_empty_string_validators() {
463        // Empty strings fail because regex requires at least one character
464        assert!(IsAlpha::validate("", "field").is_err());
465        assert!(IsNumeric::validate("", "field").is_err());
466        assert!(IsAlphanumeric::validate("", "field").is_err());
467    }
468}