1use crate::ValidationError;
4use once_cell::sync::Lazy;
5use regex::Regex;
6
7static 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
24pub 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
42pub 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
60pub 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
78pub 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
95pub 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
112pub 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
129pub 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
146pub 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
164pub 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
181pub 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
214pub 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
245pub 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
274pub 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
295pub 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 assert!(IsUuid::validate("123e4567-e89b-12d3-a456-426614174000", "id").is_ok());
457 assert!(IsUuid::validate("123e4567e89b12d3a456426614174000", "id").is_err());
459 }
460
461 #[test]
462 fn test_empty_string_validators() {
463 assert!(IsAlpha::validate("", "field").is_err());
465 assert!(IsNumeric::validate("", "field").is_err());
466 assert!(IsAlphanumeric::validate("", "field").is_err());
467 }
468}