1use crate::v2::error::RuleError;
6use crate::v2::traits::ValidationRule;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10
11static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
13static URL_REGEX: OnceLock<Regex> = OnceLock::new();
14static PHONE_REGEX: OnceLock<Regex> = OnceLock::new();
15
16fn email_regex() -> &'static Regex {
17 EMAIL_REGEX.get_or_init(|| {
18 Regex::new(
20 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])?)*$"
21 ).unwrap()
22 })
23}
24
25fn url_regex() -> &'static Regex {
26 URL_REGEX.get_or_init(|| Regex::new(r"^(https?|ftp)://[^\s/$.?#].[^\s]*$").unwrap())
27}
28
29fn phone_regex() -> &'static Regex {
30 PHONE_REGEX.get_or_init(|| Regex::new(r"^\+[1-9]\d{1,14}$").unwrap())
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
38pub struct EmailRule {
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub message: Option<String>,
42}
43
44impl EmailRule {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_message(mut self, message: impl Into<String>) -> Self {
52 self.message = Some(message.into());
53 self
54 }
55}
56
57impl ValidationRule<str> for EmailRule {
58 fn validate(&self, value: &str) -> Result<(), RuleError> {
59 if email_regex().is_match(value) {
60 Ok(())
61 } else {
62 let message = self
63 .message
64 .clone()
65 .unwrap_or_else(|| "validation.email.invalid".to_string());
66 Err(RuleError::new("email", message))
67 }
68 }
69
70 fn rule_name(&self) -> &'static str {
71 "email"
72 }
73}
74
75impl ValidationRule<String> for EmailRule {
76 fn validate(&self, value: &String) -> Result<(), RuleError> {
77 <Self as ValidationRule<str>>::validate(self, value.as_str())
78 }
79
80 fn rule_name(&self) -> &'static str {
81 "email"
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct LengthRule {
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub min: Option<usize>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub max: Option<usize>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub message: Option<String>,
99}
100
101impl LengthRule {
102 pub fn new(min: usize, max: usize) -> Self {
104 Self {
105 min: Some(min),
106 max: Some(max),
107 message: None,
108 }
109 }
110
111 pub fn min(min: usize) -> Self {
113 Self {
114 min: Some(min),
115 max: None,
116 message: None,
117 }
118 }
119
120 pub fn max(max: usize) -> Self {
122 Self {
123 min: None,
124 max: Some(max),
125 message: None,
126 }
127 }
128
129 pub fn with_message(mut self, message: impl Into<String>) -> Self {
131 self.message = Some(message.into());
132 self
133 }
134}
135
136impl ValidationRule<str> for LengthRule {
137 fn validate(&self, value: &str) -> Result<(), RuleError> {
138 let len = value.chars().count();
139
140 if let Some(min) = self.min {
141 if len < min {
142 let message = self
143 .message
144 .clone()
145 .unwrap_or_else(|| "validation.length.min".to_string());
146 return Err(RuleError::new("length", message)
147 .param("min", min)
148 .param("max", self.max)
149 .param("actual", len));
150 }
151 }
152
153 if let Some(max) = self.max {
154 if len > max {
155 let message = self
156 .message
157 .clone()
158 .unwrap_or_else(|| "validation.length.max".to_string());
159 return Err(RuleError::new("length", message)
160 .param("min", self.min)
161 .param("max", max)
162 .param("actual", len));
163 }
164 }
165
166 Ok(())
167 }
168
169 fn rule_name(&self) -> &'static str {
170 "length"
171 }
172}
173
174impl ValidationRule<String> for LengthRule {
175 fn validate(&self, value: &String) -> Result<(), RuleError> {
176 <Self as ValidationRule<str>>::validate(self, value.as_str())
177 }
178
179 fn rule_name(&self) -> &'static str {
180 "length"
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
188pub struct RangeRule<T> {
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub min: Option<T>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub max: Option<T>,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub message: Option<String>,
198}
199
200impl<T> RangeRule<T> {
201 pub fn new(min: T, max: T) -> Self {
203 Self {
204 min: Some(min),
205 max: Some(max),
206 message: None,
207 }
208 }
209
210 pub fn min(min: T) -> Self {
212 Self {
213 min: Some(min),
214 max: None,
215 message: None,
216 }
217 }
218
219 pub fn max(max: T) -> Self {
221 Self {
222 min: None,
223 max: Some(max),
224 message: None,
225 }
226 }
227
228 pub fn with_message(mut self, message: impl Into<String>) -> Self {
230 self.message = Some(message.into());
231 self
232 }
233}
234
235impl<T> ValidationRule<T> for RangeRule<T>
236where
237 T: PartialOrd + std::fmt::Display + Copy + Send + Sync + std::fmt::Debug + Serialize,
238{
239 fn validate(&self, value: &T) -> Result<(), RuleError> {
240 if let Some(ref min) = self.min {
241 if value < min {
242 let message = self
243 .message
244 .clone()
245 .unwrap_or_else(|| "validation.range.min".to_string());
246 return Err(RuleError::new("range", message)
247 .param("min", *min)
248 .param("max", self.max)
249 .param("actual", *value));
250 }
251 }
252
253 if let Some(ref max) = self.max {
254 if value > max {
255 let message = self
256 .message
257 .clone()
258 .unwrap_or_else(|| "validation.range.max".to_string());
259 return Err(RuleError::new("range", message)
260 .param("min", self.min)
261 .param("max", *max)
262 .param("actual", *value));
263 }
264 }
265
266 Ok(())
267 }
268
269 fn rule_name(&self) -> &'static str {
270 "range"
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct RegexRule {
279 pub pattern: String,
281 #[serde(skip)]
283 compiled: OnceLock<Regex>,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub message: Option<String>,
287}
288
289impl PartialEq for RegexRule {
290 fn eq(&self, other: &Self) -> bool {
291 self.pattern == other.pattern && self.message == other.message
292 }
293}
294
295impl RegexRule {
296 pub fn new(pattern: impl Into<String>) -> Self {
298 Self {
299 pattern: pattern.into(),
300 compiled: OnceLock::new(),
301 message: None,
302 }
303 }
304
305 pub fn with_message(mut self, message: impl Into<String>) -> Self {
307 self.message = Some(message.into());
308 self
309 }
310
311 fn get_regex(&self) -> Result<&Regex, RuleError> {
312 self.compiled.get_or_init(|| {
313 Regex::new(&self.pattern).unwrap_or_else(|_| Regex::new("^$").unwrap())
314 });
315
316 if Regex::new(&self.pattern).is_err() {
318 return Err(RuleError::new(
319 "regex",
320 format!("Invalid regex pattern: {}", self.pattern),
321 ));
322 }
323
324 Ok(self.compiled.get().unwrap())
325 }
326}
327
328impl ValidationRule<str> for RegexRule {
329 fn validate(&self, value: &str) -> Result<(), RuleError> {
330 let regex = self.get_regex()?;
331
332 if regex.is_match(value) {
333 Ok(())
334 } else {
335 let message = self
336 .message
337 .clone()
338 .unwrap_or_else(|| "validation.regex.mismatch".to_string());
339 Err(RuleError::new("regex", message).param("pattern", self.pattern.clone()))
340 }
341 }
342
343 fn rule_name(&self) -> &'static str {
344 "regex"
345 }
346}
347
348impl ValidationRule<String> for RegexRule {
349 fn validate(&self, value: &String) -> Result<(), RuleError> {
350 <Self as ValidationRule<str>>::validate(self, value.as_str())
351 }
352
353 fn rule_name(&self) -> &'static str {
354 "regex"
355 }
356}
357
358#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
362pub struct UrlRule {
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub message: Option<String>,
366}
367
368impl UrlRule {
369 pub fn new() -> Self {
371 Self::default()
372 }
373
374 pub fn with_message(mut self, message: impl Into<String>) -> Self {
376 self.message = Some(message.into());
377 self
378 }
379}
380
381impl ValidationRule<str> for UrlRule {
382 fn validate(&self, value: &str) -> Result<(), RuleError> {
383 if url_regex().is_match(value) {
384 Ok(())
385 } else {
386 let message = self
387 .message
388 .clone()
389 .unwrap_or_else(|| "validation.url.invalid".to_string());
390 Err(RuleError::new("url", message))
391 }
392 }
393
394 fn rule_name(&self) -> &'static str {
395 "url"
396 }
397}
398
399impl ValidationRule<String> for UrlRule {
400 fn validate(&self, value: &String) -> Result<(), RuleError> {
401 <Self as ValidationRule<str>>::validate(self, value.as_str())
402 }
403
404 fn rule_name(&self) -> &'static str {
405 "url"
406 }
407}
408
409#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
413pub struct RequiredRule {
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub message: Option<String>,
417}
418
419impl RequiredRule {
420 pub fn new() -> Self {
422 Self::default()
423 }
424
425 pub fn with_message(mut self, message: impl Into<String>) -> Self {
427 self.message = Some(message.into());
428 self
429 }
430}
431
432impl ValidationRule<str> for RequiredRule {
433 fn validate(&self, value: &str) -> Result<(), RuleError> {
434 if !value.trim().is_empty() {
435 Ok(())
436 } else {
437 let message = self
438 .message
439 .clone()
440 .unwrap_or_else(|| "validation.required.missing".to_string());
441 Err(RuleError::new("required", message))
442 }
443 }
444
445 fn rule_name(&self) -> &'static str {
446 "required"
447 }
448}
449
450impl ValidationRule<String> for RequiredRule {
451 fn validate(&self, value: &String) -> Result<(), RuleError> {
452 <Self as ValidationRule<str>>::validate(self, value.as_str())
453 }
454
455 fn rule_name(&self) -> &'static str {
456 "required"
457 }
458}
459
460impl<T> ValidationRule<Option<T>> for RequiredRule
461where
462 T: std::fmt::Debug + Send + Sync,
463{
464 fn validate(&self, value: &Option<T>) -> Result<(), RuleError> {
465 if value.is_some() {
466 Ok(())
467 } else {
468 let message = self
469 .message
470 .clone()
471 .unwrap_or_else(|| "validation.required.missing".to_string());
472 Err(RuleError::new("required", message))
473 }
474 }
475
476 fn rule_name(&self) -> &'static str {
477 "required"
478 }
479}
480
481#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
483pub struct CreditCardRule {
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub message: Option<String>,
487}
488
489impl CreditCardRule {
490 pub fn new() -> Self {
492 Self::default()
493 }
494
495 pub fn with_message(mut self, message: impl Into<String>) -> Self {
497 self.message = Some(message.into());
498 self
499 }
500}
501
502impl ValidationRule<str> for CreditCardRule {
503 fn validate(&self, value: &str) -> Result<(), RuleError> {
504 let mut sum = 0;
505 let mut double = false;
506
507 for c in value.chars().rev() {
509 if !c.is_ascii_digit() {
510 let message = self
511 .message
512 .clone()
513 .unwrap_or_else(|| "validation.credit_card.invalid_format".to_string());
514 return Err(RuleError::new("credit_card", message));
515 }
516
517 let mut digit = c.to_digit(10).unwrap();
518
519 if double {
520 digit *= 2;
521 if digit > 9 {
522 digit -= 9;
523 }
524 }
525
526 sum += digit;
527 double = !double;
528 }
529
530 if sum > 0 && sum % 10 == 0 {
531 Ok(())
532 } else {
533 let message = self
534 .message
535 .clone()
536 .unwrap_or_else(|| "validation.credit_card.invalid".to_string());
537 Err(RuleError::new("credit_card", message))
538 }
539 }
540
541 fn rule_name(&self) -> &'static str {
542 "credit_card"
543 }
544}
545
546impl ValidationRule<String> for CreditCardRule {
547 fn validate(&self, value: &String) -> Result<(), RuleError> {
548 <Self as ValidationRule<str>>::validate(self, value.as_str())
549 }
550
551 fn rule_name(&self) -> &'static str {
552 "credit_card"
553 }
554}
555
556#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
558pub struct IpRule {
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub v4: Option<bool>,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub v6: Option<bool>,
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub message: Option<String>,
568}
569
570impl IpRule {
571 pub fn new() -> Self {
573 Self::default()
574 }
575
576 pub fn v4() -> Self {
578 Self {
579 v4: Some(true),
580 v6: None,
581 message: None,
582 }
583 }
584
585 pub fn v6() -> Self {
587 Self {
588 v4: None,
589 v6: Some(true),
590 message: None,
591 }
592 }
593
594 pub fn with_message(mut self, message: impl Into<String>) -> Self {
596 self.message = Some(message.into());
597 self
598 }
599}
600
601impl ValidationRule<str> for IpRule {
602 fn validate(&self, value: &str) -> Result<(), RuleError> {
603 use std::net::IpAddr;
604
605 match value.parse::<IpAddr>() {
606 Ok(ip) => {
607 if let Some(true) = self.v4 {
608 if !ip.is_ipv4() {
609 let message = self
610 .message
611 .clone()
612 .unwrap_or_else(|| "validation.ip.v4_required".to_string());
613 return Err(RuleError::new("ip", message));
614 }
615 }
616 if let Some(true) = self.v6 {
617 if !ip.is_ipv6() {
618 let message = self
619 .message
620 .clone()
621 .unwrap_or_else(|| "validation.ip.v6_required".to_string());
622 return Err(RuleError::new("ip", message));
623 }
624 }
625 Ok(())
626 }
627 Err(_) => {
628 let message = self
629 .message
630 .clone()
631 .unwrap_or_else(|| "validation.ip.invalid".to_string());
632 Err(RuleError::new("ip", message))
633 }
634 }
635 }
636
637 fn rule_name(&self) -> &'static str {
638 "ip"
639 }
640}
641
642impl ValidationRule<String> for IpRule {
643 fn validate(&self, value: &String) -> Result<(), RuleError> {
644 <Self as ValidationRule<str>>::validate(self, value.as_str())
645 }
646
647 fn rule_name(&self) -> &'static str {
648 "ip"
649 }
650}
651
652#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
654pub struct PhoneRule {
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub message: Option<String>,
658}
659
660impl PhoneRule {
661 pub fn new() -> Self {
663 Self::default()
664 }
665
666 pub fn with_message(mut self, message: impl Into<String>) -> Self {
668 self.message = Some(message.into());
669 self
670 }
671}
672
673impl ValidationRule<str> for PhoneRule {
674 fn validate(&self, value: &str) -> Result<(), RuleError> {
675 if phone_regex().is_match(value) {
676 Ok(())
677 } else {
678 let message = self
679 .message
680 .clone()
681 .unwrap_or_else(|| "validation.phone.invalid".to_string());
682 Err(RuleError::new("phone", message))
683 }
684 }
685
686 fn rule_name(&self) -> &'static str {
687 "phone"
688 }
689}
690
691impl ValidationRule<String> for PhoneRule {
692 fn validate(&self, value: &String) -> Result<(), RuleError> {
693 <Self as ValidationRule<str>>::validate(self, value.as_str())
694 }
695
696 fn rule_name(&self) -> &'static str {
697 "phone"
698 }
699}
700
701#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
703pub struct ContainsRule {
704 pub needle: String,
706 #[serde(skip_serializing_if = "Option::is_none")]
708 pub message: Option<String>,
709}
710
711impl ContainsRule {
712 pub fn new(needle: impl Into<String>) -> Self {
714 Self {
715 needle: needle.into(),
716 message: None,
717 }
718 }
719
720 pub fn with_message(mut self, message: impl Into<String>) -> Self {
722 self.message = Some(message.into());
723 self
724 }
725}
726
727impl ValidationRule<str> for ContainsRule {
728 fn validate(&self, value: &str) -> Result<(), RuleError> {
729 if value.contains(&self.needle) {
730 Ok(())
731 } else {
732 let message = self
733 .message
734 .clone()
735 .unwrap_or_else(|| "validation.contains.missing".to_string());
736 Err(RuleError::new("contains", message).param("needle", self.needle.clone()))
737 }
738 }
739
740 fn rule_name(&self) -> &'static str {
741 "contains"
742 }
743}
744
745impl ValidationRule<String> for ContainsRule {
746 fn validate(&self, value: &String) -> Result<(), RuleError> {
747 <Self as ValidationRule<str>>::validate(self, value.as_str())
748 }
749
750 fn rule_name(&self) -> &'static str {
751 "contains"
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn email_rule_valid() {
761 let rule = EmailRule::new();
762 assert!(rule.validate("test@example.com").is_ok());
763 assert!(rule.validate("user.name+tag@domain.co.uk").is_ok());
764 }
765
766 #[test]
767 fn email_rule_invalid() {
768 let rule = EmailRule::new();
769 assert!(rule.validate("invalid").is_err());
770 assert!(rule.validate("@domain.com").is_err());
771 assert!(rule.validate("user@").is_err());
772 }
773
774 #[test]
775 fn email_rule_custom_message() {
776 let rule = EmailRule::new().with_message("Please enter a valid email");
777 let err = rule.validate("invalid").unwrap_err();
778 assert_eq!(err.message, "Please enter a valid email");
779 }
780
781 #[test]
782 fn length_rule_valid() {
783 let rule = LengthRule::new(3, 10);
784 assert!(rule.validate("abc").is_ok());
785 assert!(rule.validate("abcdefghij").is_ok());
786 }
787
788 #[test]
789 fn length_rule_too_short() {
790 let rule = LengthRule::new(3, 10);
791 let err = rule.validate("ab").unwrap_err();
792 assert_eq!(err.code, "length");
793 }
794
795 #[test]
796 fn length_rule_too_long() {
797 let rule = LengthRule::new(3, 10);
798 let err = rule.validate("abcdefghijk").unwrap_err();
799 assert_eq!(err.code, "length");
800 }
801
802 #[test]
803 fn range_rule_valid() {
804 let rule = RangeRule::new(18, 120);
805 assert!(rule.validate(&18).is_ok());
806 assert!(rule.validate(&50).is_ok());
807 assert!(rule.validate(&120).is_ok());
808 }
809
810 #[test]
811 fn range_rule_too_low() {
812 let rule = RangeRule::new(18, 120);
813 let err = rule.validate(&17).unwrap_err();
814 assert_eq!(err.code, "range");
815 }
816
817 #[test]
818 fn range_rule_too_high() {
819 let rule = RangeRule::new(18, 120);
820 let err = rule.validate(&121).unwrap_err();
821 assert_eq!(err.code, "range");
822 }
823
824 #[test]
825 fn regex_rule_valid() {
826 let rule = RegexRule::new(r"^\d{3}-\d{4}$");
827 assert!(rule.validate("123-4567").is_ok());
828 }
829
830 #[test]
831 fn regex_rule_invalid() {
832 let rule = RegexRule::new(r"^\d{3}-\d{4}$");
833 assert!(rule.validate("1234567").is_err());
834 }
835
836 #[test]
837 fn url_rule_valid() {
838 let rule = UrlRule::new();
839 assert!(rule.validate("https://example.com").is_ok());
840 assert!(rule.validate("http://example.com/path?query=1").is_ok());
841 }
842
843 #[test]
844 fn url_rule_invalid() {
845 let rule = UrlRule::new();
846 assert!(rule.validate("not-a-url").is_err());
847 assert!(rule.validate("ftp://").is_err());
848 }
849
850 #[test]
851 fn required_rule_valid() {
852 let rule = RequiredRule::new();
853 assert!(rule.validate("value").is_ok());
854 assert!(rule.validate(" value ").is_ok());
855 }
856
857 #[test]
858 fn required_rule_empty() {
859 let rule = RequiredRule::new();
860 assert!(rule.validate("").is_err());
861 assert!(rule.validate(" ").is_err());
862 }
863
864 #[test]
865 fn required_rule_option() {
866 let rule = RequiredRule::new();
867 assert!(ValidationRule::<Option<i32>>::validate(&rule, &Some(42)).is_ok());
868 assert!(ValidationRule::<Option<i32>>::validate(&rule, &None).is_err());
869 }
870
871 #[test]
872 fn rule_serialization_roundtrip() {
873 let rule = LengthRule::new(3, 50).with_message("Custom message");
874 let json = serde_json::to_string(&rule).unwrap();
875 let parsed: LengthRule = serde_json::from_str(&json).unwrap();
876 assert_eq!(rule, parsed);
877 }
878}