Skip to main content

rusdantic_core/rules/
pattern.rs

1//! Regex pattern validation rule.
2//!
3//! Validates that a string value matches a given regex pattern.
4//! The regex is compiled once at first use via OnceLock in the generated code
5//! and passed as a reference to this validator.
6
7use crate::error::{PathSegment, ValidationError, ValidationErrors};
8use crate::rules::AsStr;
9
10/// Validate that the value matches the given regex pattern.
11///
12/// The `regex` parameter is a pre-compiled `Regex` object (managed by
13/// an `OnceLock` static in the generated code). The `pattern_str` parameter
14/// is the raw regex string, included in error messages for debugging.
15///
16/// The pattern is matched against the entire string — it is automatically
17/// anchored. If the original regex does not anchor, this function still
18/// checks that the full string matches (via `is_match`). If partial matching
19/// is desired, the regex should use `.*` appropriately.
20/// Ensure a regex pattern matches the FULL string (not partial).
21///
22/// If the pattern doesn't start with `^` and end with `$`, it's wrapped
23/// in `^(?:...)$` to ensure full-string matching. This prevents patterns
24/// like `[0-9]{5}` from matching within "abc12345xyz".
25pub fn anchor_pattern(pattern: &str) -> String {
26    let starts = pattern.starts_with('^');
27    let ends = pattern.ends_with('$');
28    match (starts, ends) {
29        (true, true) => pattern.to_string(),
30        (true, false) => format!("{}$", pattern),
31        (false, true) => format!("^{}", pattern),
32        (false, false) => format!("^(?:{})$", pattern),
33    }
34}
35
36pub fn validate_pattern<T: AsStr>(
37    value: &T,
38    regex: &regex::Regex,
39    pattern_str: &str,
40    path: &[PathSegment],
41    errors: &mut ValidationErrors,
42) {
43    let s = value.as_str_ref();
44
45    if !regex.is_match(s) {
46        errors.add(
47            ValidationError::new(
48                "pattern",
49                format!("must match pattern '{}'", pattern_str),
50            )
51            .with_path(path.to_vec())
52            .with_param("pattern", serde_json::json!(pattern_str)),
53        );
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use regex::Regex;
61
62    fn path(name: &str) -> Vec<PathSegment> {
63        vec![PathSegment::Field(name.to_string())]
64    }
65
66    #[test]
67    fn test_pattern_matches() {
68        let re = Regex::new(r"^[a-z]+$").unwrap();
69        let mut errors = ValidationErrors::new();
70        validate_pattern(
71            &"hello".to_string(),
72            &re,
73            "^[a-z]+$",
74            &path("f"),
75            &mut errors,
76        );
77        assert!(errors.is_empty());
78    }
79
80    #[test]
81    fn test_pattern_no_match() {
82        let re = Regex::new(r"^[a-z]+$").unwrap();
83        let mut errors = ValidationErrors::new();
84        validate_pattern(
85            &"Hello123".to_string(),
86            &re,
87            "^[a-z]+$",
88            &path("f"),
89            &mut errors,
90        );
91        assert_eq!(errors.len(), 1);
92        assert_eq!(errors.errors()[0].code, "pattern");
93        assert_eq!(errors.errors()[0].params["pattern"], "^[a-z]+$");
94    }
95
96    #[test]
97    fn test_pattern_with_digits() {
98        let re = Regex::new(r"^\d{3}-\d{4}$").unwrap();
99        let mut errors = ValidationErrors::new();
100        validate_pattern(
101            &"123-4567".to_string(),
102            &re,
103            r"^\d{3}-\d{4}$",
104            &path("phone"),
105            &mut errors,
106        );
107        assert!(errors.is_empty());
108    }
109
110    #[test]
111    fn test_pattern_empty_string() {
112        let re = Regex::new(r"^.+$").unwrap();
113        let mut errors = ValidationErrors::new();
114        validate_pattern(&"".to_string(), &re, "^.+$", &path("f"), &mut errors);
115        assert_eq!(errors.len(), 1);
116    }
117
118    #[test]
119    fn test_pattern_unicode() {
120        let re = Regex::new(r"^\p{L}+$").unwrap();
121        let mut errors = ValidationErrors::new();
122        validate_pattern(
123            &"héllo".to_string(),
124            &re,
125            r"^\p{L}+$",
126            &path("f"),
127            &mut errors,
128        );
129        assert!(errors.is_empty());
130    }
131}