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<Regex>,
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        self.compiled.get_or_init(|| {
313            Regex::new(&self.pattern).unwrap_or_else(|_| Regex::new("^$").unwrap())
314        });
315
316        // Verify the pattern is valid
317        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/// URL format validation rule.
359///
360/// Validates that a string is a valid URL.
361#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
362pub struct UrlRule {
363    /// Custom error message
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub message: Option<String>,
366}
367
368impl UrlRule {
369    /// Create a new URL rule.
370    pub fn new() -> Self {
371        Self::default()
372    }
373
374    /// Create a URL rule with a custom message.
375    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/// Required (non-empty) validation rule.
410///
411/// Validates that a value is not empty.
412#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
413pub struct RequiredRule {
414    /// Custom error message
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub message: Option<String>,
417}
418
419impl RequiredRule {
420    /// Create a new required rule.
421    pub fn new() -> Self {
422        Self::default()
423    }
424
425    /// Create a required rule with a custom message.
426    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/// Credit Card validation rule (Luhn algorithm).
482#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
483pub struct CreditCardRule {
484    /// Custom error message
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub message: Option<String>,
487}
488
489impl CreditCardRule {
490    /// Create a new credit card rule.
491    pub fn new() -> Self {
492        Self::default()
493    }
494
495    /// Set a custom error message.
496    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        // Iterate over digits in reverse
508        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/// IP Address validation rule (IPv4 and IPv6).
557#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
558pub struct IpRule {
559    /// Check for IPv4 only
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub v4: Option<bool>,
562    /// Check for IPv6 only
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub v6: Option<bool>,
565    /// Custom error message
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub message: Option<String>,
568}
569
570impl IpRule {
571    /// Create a new IP rule (accepts both v4 and v6).
572    pub fn new() -> Self {
573        Self::default()
574    }
575
576    /// Create a rule for IPv4 only.
577    pub fn v4() -> Self {
578        Self {
579            v4: Some(true),
580            v6: None,
581            message: None,
582        }
583    }
584
585    /// Create a rule for IPv6 only.
586    pub fn v6() -> Self {
587        Self {
588            v4: None,
589            v6: Some(true),
590            message: None,
591        }
592    }
593
594    /// Set a custom error message.
595    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/// Phone number validation rule (E.164).
653#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
654pub struct PhoneRule {
655    /// Custom error message
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub message: Option<String>,
658}
659
660impl PhoneRule {
661    /// Create a new phone rule.
662    pub fn new() -> Self {
663        Self::default()
664    }
665
666    /// Set a custom error message.
667    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/// Contains substring validation rule.
702#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
703pub struct ContainsRule {
704    /// The substring that must be present
705    pub needle: String,
706    /// Custom error message
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub message: Option<String>,
709}
710
711impl ContainsRule {
712    /// Create a new contains rule.
713    pub fn new(needle: impl Into<String>) -> Self {
714        Self {
715            needle: needle.into(),
716            message: None,
717        }
718    }
719
720    /// Set a custom error message.
721    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}