Skip to main content

rustapi_validate/v2/rules/
sync_rules.rs

1//! Synchronous validation rules.
2//!
3//! These rules perform validation without requiring async operations.
4
5use crate::v2::error::RuleError;
6use crate::v2::traits::ValidationRule;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10
11// Pre-compiled regex patterns
12static 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        // RFC 5322 simplified email regex
19        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    // E.164 format (e.g. +14155552671)
31    PHONE_REGEX.get_or_init(|| Regex::new(r"^\+[1-9]\d{1,14}$").unwrap())
32}
33
34/// Email format validation rule.
35///
36/// Validates that a string is a valid email address according to RFC 5322.
37#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
38pub struct EmailRule {
39    /// Custom error message
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub message: Option<String>,
42}
43
44impl EmailRule {
45    /// Create a new email rule with default message.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Create an email rule with a custom message.
51    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/// String length validation rule.
86///
87/// Validates that a string's length is within specified bounds.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
89pub struct LengthRule {
90    /// Minimum length (inclusive)
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub min: Option<usize>,
93    /// Maximum length (inclusive)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub max: Option<usize>,
96    /// Custom error message
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub message: Option<String>,
99}
100
101impl LengthRule {
102    /// Create a length rule with min and max bounds.
103    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    /// Create a length rule with only a minimum.
112    pub fn min(min: usize) -> Self {
113        Self {
114            min: Some(min),
115            max: None,
116            message: None,
117        }
118    }
119
120    /// Create a length rule with only a maximum.
121    pub fn max(max: usize) -> Self {
122        Self {
123            min: None,
124            max: Some(max),
125            message: None,
126        }
127    }
128
129    /// Set a custom error message.
130    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/// Numeric range validation rule.
185///
186/// Validates that a number is within specified bounds.
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
188pub struct RangeRule<T> {
189    /// Minimum value (inclusive)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub min: Option<T>,
192    /// Maximum value (inclusive)
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub max: Option<T>,
195    /// Custom error message
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub message: Option<String>,
198}
199
200impl<T> RangeRule<T> {
201    /// Create a range rule with min and max bounds.
202    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    /// Create a range rule with only a minimum.
211    pub fn min(min: T) -> Self {
212        Self {
213            min: Some(min),
214            max: None,
215            message: None,
216        }
217    }
218
219    /// Create a range rule with only a maximum.
220    pub fn max(max: T) -> Self {
221        Self {
222            min: None,
223            max: Some(max),
224            message: None,
225        }
226    }
227
228    /// Set a custom error message.
229    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/// Regex pattern validation rule.
275///
276/// Validates that a string matches a regex pattern.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct RegexRule {
279    /// The regex pattern
280    pub pattern: String,
281    /// Compiled regex (not serialized)
282    #[serde(skip)]
283    compiled: OnceLock<Result<Regex, String>>,
284    /// Custom error message
285    #[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    /// Create a new regex rule.
297    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    /// Set a custom error message.
306    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        let result = self.compiled.get_or_init(|| {
313            Regex::new(&self.pattern)
314                .map_err(|_| format!("Invalid regex pattern: {}", self.pattern))
315        });
316
317        match result {
318            Ok(regex) => Ok(regex),
319            Err(msg) => Err(RuleError::new("regex", msg.clone())),
320        }
321    }
322}
323
324impl ValidationRule<str> for RegexRule {
325    fn validate(&self, value: &str) -> Result<(), RuleError> {
326        let regex = self.get_regex()?;
327
328        if regex.is_match(value) {
329            Ok(())
330        } else {
331            let message = self
332                .message
333                .clone()
334                .unwrap_or_else(|| "validation.regex.mismatch".to_string());
335            Err(RuleError::new("regex", message).param("pattern", self.pattern.clone()))
336        }
337    }
338
339    fn rule_name(&self) -> &'static str {
340        "regex"
341    }
342}
343
344impl ValidationRule<String> for RegexRule {
345    fn validate(&self, value: &String) -> Result<(), RuleError> {
346        <Self as ValidationRule<str>>::validate(self, value.as_str())
347    }
348
349    fn rule_name(&self) -> &'static str {
350        "regex"
351    }
352}
353
354/// URL format validation rule.
355///
356/// Validates that a string is a valid URL.
357#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
358pub struct UrlRule {
359    /// Custom error message
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub message: Option<String>,
362}
363
364impl UrlRule {
365    /// Create a new URL rule.
366    pub fn new() -> Self {
367        Self::default()
368    }
369
370    /// Create a URL rule with a custom message.
371    pub fn with_message(mut self, message: impl Into<String>) -> Self {
372        self.message = Some(message.into());
373        self
374    }
375}
376
377impl ValidationRule<str> for UrlRule {
378    fn validate(&self, value: &str) -> Result<(), RuleError> {
379        if url_regex().is_match(value) {
380            Ok(())
381        } else {
382            let message = self
383                .message
384                .clone()
385                .unwrap_or_else(|| "validation.url.invalid".to_string());
386            Err(RuleError::new("url", message))
387        }
388    }
389
390    fn rule_name(&self) -> &'static str {
391        "url"
392    }
393}
394
395impl ValidationRule<String> for UrlRule {
396    fn validate(&self, value: &String) -> Result<(), RuleError> {
397        <Self as ValidationRule<str>>::validate(self, value.as_str())
398    }
399
400    fn rule_name(&self) -> &'static str {
401        "url"
402    }
403}
404
405/// Required (non-empty) validation rule.
406///
407/// Validates that a value is not empty.
408#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
409pub struct RequiredRule {
410    /// Custom error message
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub message: Option<String>,
413}
414
415impl RequiredRule {
416    /// Create a new required rule.
417    pub fn new() -> Self {
418        Self::default()
419    }
420
421    /// Create a required rule with a custom message.
422    pub fn with_message(mut self, message: impl Into<String>) -> Self {
423        self.message = Some(message.into());
424        self
425    }
426}
427
428impl ValidationRule<str> for RequiredRule {
429    fn validate(&self, value: &str) -> Result<(), RuleError> {
430        if !value.trim().is_empty() {
431            Ok(())
432        } else {
433            let message = self
434                .message
435                .clone()
436                .unwrap_or_else(|| "validation.required.missing".to_string());
437            Err(RuleError::new("required", message))
438        }
439    }
440
441    fn rule_name(&self) -> &'static str {
442        "required"
443    }
444}
445
446impl ValidationRule<String> for RequiredRule {
447    fn validate(&self, value: &String) -> Result<(), RuleError> {
448        <Self as ValidationRule<str>>::validate(self, value.as_str())
449    }
450
451    fn rule_name(&self) -> &'static str {
452        "required"
453    }
454}
455
456impl<T> ValidationRule<Option<T>> for RequiredRule
457where
458    T: std::fmt::Debug + Send + Sync,
459{
460    fn validate(&self, value: &Option<T>) -> Result<(), RuleError> {
461        if value.is_some() {
462            Ok(())
463        } else {
464            let message = self
465                .message
466                .clone()
467                .unwrap_or_else(|| "validation.required.missing".to_string());
468            Err(RuleError::new("required", message))
469        }
470    }
471
472    fn rule_name(&self) -> &'static str {
473        "required"
474    }
475}
476
477/// Credit Card validation rule (Luhn algorithm).
478#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
479pub struct CreditCardRule {
480    /// Custom error message
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub message: Option<String>,
483}
484
485impl CreditCardRule {
486    /// Create a new credit card rule.
487    pub fn new() -> Self {
488        Self::default()
489    }
490
491    /// Set a custom error message.
492    pub fn with_message(mut self, message: impl Into<String>) -> Self {
493        self.message = Some(message.into());
494        self
495    }
496}
497
498impl ValidationRule<str> for CreditCardRule {
499    fn validate(&self, value: &str) -> Result<(), RuleError> {
500        let mut sum = 0;
501        let mut double = false;
502
503        // Iterate over digits in reverse
504        for c in value.chars().rev() {
505            if !c.is_ascii_digit() {
506                let message = self
507                    .message
508                    .clone()
509                    .unwrap_or_else(|| "validation.credit_card.invalid_format".to_string());
510                return Err(RuleError::new("credit_card", message));
511            }
512
513            let mut digit = c.to_digit(10).unwrap();
514
515            if double {
516                digit *= 2;
517                if digit > 9 {
518                    digit -= 9;
519                }
520            }
521
522            sum += digit;
523            double = !double;
524        }
525
526        if sum > 0 && sum % 10 == 0 {
527            Ok(())
528        } else {
529            let message = self
530                .message
531                .clone()
532                .unwrap_or_else(|| "validation.credit_card.invalid".to_string());
533            Err(RuleError::new("credit_card", message))
534        }
535    }
536
537    fn rule_name(&self) -> &'static str {
538        "credit_card"
539    }
540}
541
542impl ValidationRule<String> for CreditCardRule {
543    fn validate(&self, value: &String) -> Result<(), RuleError> {
544        <Self as ValidationRule<str>>::validate(self, value.as_str())
545    }
546
547    fn rule_name(&self) -> &'static str {
548        "credit_card"
549    }
550}
551
552/// IP Address validation rule (IPv4 and IPv6).
553#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
554pub struct IpRule {
555    /// Check for IPv4 only
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub v4: Option<bool>,
558    /// Check for IPv6 only
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub v6: Option<bool>,
561    /// Custom error message
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub message: Option<String>,
564}
565
566impl IpRule {
567    /// Create a new IP rule (accepts both v4 and v6).
568    pub fn new() -> Self {
569        Self::default()
570    }
571
572    /// Create a rule for IPv4 only.
573    pub fn v4() -> Self {
574        Self {
575            v4: Some(true),
576            v6: None,
577            message: None,
578        }
579    }
580
581    /// Create a rule for IPv6 only.
582    pub fn v6() -> Self {
583        Self {
584            v4: None,
585            v6: Some(true),
586            message: None,
587        }
588    }
589
590    /// Set a custom error message.
591    pub fn with_message(mut self, message: impl Into<String>) -> Self {
592        self.message = Some(message.into());
593        self
594    }
595}
596
597impl ValidationRule<str> for IpRule {
598    fn validate(&self, value: &str) -> Result<(), RuleError> {
599        use std::net::IpAddr;
600
601        match value.parse::<IpAddr>() {
602            Ok(ip) => {
603                if let Some(true) = self.v4 {
604                    if !ip.is_ipv4() {
605                        let message = self
606                            .message
607                            .clone()
608                            .unwrap_or_else(|| "validation.ip.v4_required".to_string());
609                        return Err(RuleError::new("ip", message));
610                    }
611                }
612                if let Some(true) = self.v6 {
613                    if !ip.is_ipv6() {
614                        let message = self
615                            .message
616                            .clone()
617                            .unwrap_or_else(|| "validation.ip.v6_required".to_string());
618                        return Err(RuleError::new("ip", message));
619                    }
620                }
621                Ok(())
622            }
623            Err(_) => {
624                let message = self
625                    .message
626                    .clone()
627                    .unwrap_or_else(|| "validation.ip.invalid".to_string());
628                Err(RuleError::new("ip", message))
629            }
630        }
631    }
632
633    fn rule_name(&self) -> &'static str {
634        "ip"
635    }
636}
637
638impl ValidationRule<String> for IpRule {
639    fn validate(&self, value: &String) -> Result<(), RuleError> {
640        <Self as ValidationRule<str>>::validate(self, value.as_str())
641    }
642
643    fn rule_name(&self) -> &'static str {
644        "ip"
645    }
646}
647
648/// Phone number validation rule (E.164).
649#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
650pub struct PhoneRule {
651    /// Custom error message
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub message: Option<String>,
654}
655
656impl PhoneRule {
657    /// Create a new phone rule.
658    pub fn new() -> Self {
659        Self::default()
660    }
661
662    /// Set a custom error message.
663    pub fn with_message(mut self, message: impl Into<String>) -> Self {
664        self.message = Some(message.into());
665        self
666    }
667}
668
669impl ValidationRule<str> for PhoneRule {
670    fn validate(&self, value: &str) -> Result<(), RuleError> {
671        if phone_regex().is_match(value) {
672            Ok(())
673        } else {
674            let message = self
675                .message
676                .clone()
677                .unwrap_or_else(|| "validation.phone.invalid".to_string());
678            Err(RuleError::new("phone", message))
679        }
680    }
681
682    fn rule_name(&self) -> &'static str {
683        "phone"
684    }
685}
686
687impl ValidationRule<String> for PhoneRule {
688    fn validate(&self, value: &String) -> Result<(), RuleError> {
689        <Self as ValidationRule<str>>::validate(self, value.as_str())
690    }
691
692    fn rule_name(&self) -> &'static str {
693        "phone"
694    }
695}
696
697/// Contains substring validation rule.
698#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
699pub struct ContainsRule {
700    /// The substring that must be present
701    pub needle: String,
702    /// Custom error message
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub message: Option<String>,
705}
706
707impl ContainsRule {
708    /// Create a new contains rule.
709    pub fn new(needle: impl Into<String>) -> Self {
710        Self {
711            needle: needle.into(),
712            message: None,
713        }
714    }
715
716    /// Set a custom error message.
717    pub fn with_message(mut self, message: impl Into<String>) -> Self {
718        self.message = Some(message.into());
719        self
720    }
721}
722
723impl ValidationRule<str> for ContainsRule {
724    fn validate(&self, value: &str) -> Result<(), RuleError> {
725        if value.contains(&self.needle) {
726            Ok(())
727        } else {
728            let message = self
729                .message
730                .clone()
731                .unwrap_or_else(|| "validation.contains.missing".to_string());
732            Err(RuleError::new("contains", message).param("needle", self.needle.clone()))
733        }
734    }
735
736    fn rule_name(&self) -> &'static str {
737        "contains"
738    }
739}
740
741impl ValidationRule<String> for ContainsRule {
742    fn validate(&self, value: &String) -> Result<(), RuleError> {
743        <Self as ValidationRule<str>>::validate(self, value.as_str())
744    }
745
746    fn rule_name(&self) -> &'static str {
747        "contains"
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    #[test]
756    fn email_rule_valid() {
757        let rule = EmailRule::new();
758        assert!(rule.validate("test@example.com").is_ok());
759        assert!(rule.validate("user.name+tag@domain.co.uk").is_ok());
760    }
761
762    #[test]
763    fn email_rule_invalid() {
764        let rule = EmailRule::new();
765        assert!(rule.validate("invalid").is_err());
766        assert!(rule.validate("@domain.com").is_err());
767        assert!(rule.validate("user@").is_err());
768    }
769
770    #[test]
771    fn email_rule_custom_message() {
772        let rule = EmailRule::new().with_message("Please enter a valid email");
773        let err = rule.validate("invalid").unwrap_err();
774        assert_eq!(err.message, "Please enter a valid email");
775    }
776
777    #[test]
778    fn length_rule_valid() {
779        let rule = LengthRule::new(3, 10);
780        assert!(rule.validate("abc").is_ok());
781        assert!(rule.validate("abcdefghij").is_ok());
782    }
783
784    #[test]
785    fn length_rule_too_short() {
786        let rule = LengthRule::new(3, 10);
787        let err = rule.validate("ab").unwrap_err();
788        assert_eq!(err.code, "length");
789    }
790
791    #[test]
792    fn length_rule_too_long() {
793        let rule = LengthRule::new(3, 10);
794        let err = rule.validate("abcdefghijk").unwrap_err();
795        assert_eq!(err.code, "length");
796    }
797
798    #[test]
799    fn range_rule_valid() {
800        let rule = RangeRule::new(18, 120);
801        assert!(rule.validate(&18).is_ok());
802        assert!(rule.validate(&50).is_ok());
803        assert!(rule.validate(&120).is_ok());
804    }
805
806    #[test]
807    fn range_rule_too_low() {
808        let rule = RangeRule::new(18, 120);
809        let err = rule.validate(&17).unwrap_err();
810        assert_eq!(err.code, "range");
811    }
812
813    #[test]
814    fn range_rule_too_high() {
815        let rule = RangeRule::new(18, 120);
816        let err = rule.validate(&121).unwrap_err();
817        assert_eq!(err.code, "range");
818    }
819
820    #[test]
821    fn regex_rule_valid() {
822        let rule = RegexRule::new(r"^\d{3}-\d{4}$");
823        assert!(rule.validate("123-4567").is_ok());
824    }
825
826    #[test]
827    fn regex_rule_invalid() {
828        let rule = RegexRule::new(r"^\d{3}-\d{4}$");
829        assert!(rule.validate("1234567").is_err());
830    }
831
832    #[test]
833    fn url_rule_valid() {
834        let rule = UrlRule::new();
835        assert!(rule.validate("https://example.com").is_ok());
836        assert!(rule.validate("http://example.com/path?query=1").is_ok());
837    }
838
839    #[test]
840    fn url_rule_invalid() {
841        let rule = UrlRule::new();
842        assert!(rule.validate("not-a-url").is_err());
843        assert!(rule.validate("ftp://").is_err());
844    }
845
846    #[test]
847    fn required_rule_valid() {
848        let rule = RequiredRule::new();
849        assert!(rule.validate("value").is_ok());
850        assert!(rule.validate("  value  ").is_ok());
851    }
852
853    #[test]
854    fn required_rule_empty() {
855        let rule = RequiredRule::new();
856        assert!(rule.validate("").is_err());
857        assert!(rule.validate("   ").is_err());
858    }
859
860    #[test]
861    fn required_rule_option() {
862        let rule = RequiredRule::new();
863        assert!(ValidationRule::<Option<i32>>::validate(&rule, &Some(42)).is_ok());
864        assert!(ValidationRule::<Option<i32>>::validate(&rule, &None).is_err());
865    }
866
867    #[test]
868    fn rule_serialization_roundtrip() {
869        let rule = LengthRule::new(3, 50).with_message("Custom message");
870        let json = serde_json::to_string(&rule).unwrap();
871        let parsed: LengthRule = serde_json::from_str(&json).unwrap();
872        assert_eq!(rule, parsed);
873    }
874}