mx20022_validate/rules/
pattern.rs1use crate::error::{Severity, ValidationError};
8use crate::rules::Rule;
9use regex::Regex;
10
11pub struct PatternRule {
28 rule_id: String,
29 pattern: String,
30 regex: Regex,
31}
32
33impl PatternRule {
34 pub fn new(rule_id: impl Into<String>, pattern: &str) -> Result<Self, regex::Error> {
42 let rule_id = rule_id.into();
43 let anchored = format!("^(?:{pattern})$");
45 let regex = Regex::new(&anchored)?;
46 Ok(Self {
47 rule_id,
48 pattern: pattern.to_owned(),
49 regex,
50 })
51 }
52
53 pub fn pattern(&self) -> &str {
55 &self.pattern
56 }
57}
58
59impl Rule for PatternRule {
60 fn id(&self) -> &str {
61 &self.rule_id
62 }
63
64 fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
65 if self.regex.is_match(value) {
66 vec![]
67 } else {
68 vec![ValidationError::new(
69 path,
70 Severity::Error,
71 &self.rule_id,
72 format!("Value `{value}` does not match pattern `{}`", self.pattern),
73 )]
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::rules::Rule;
82
83 #[test]
84 fn two_letter_country_code_passes() {
85 let rule = PatternRule::new("COUNTRY_CODE", "[A-Z]{2}").unwrap();
86 assert!(rule.validate("GB", "/p").is_empty());
87 assert!(rule.validate("US", "/p").is_empty());
88 }
89
90 #[test]
91 fn two_letter_country_code_rejects_lowercase() {
92 let rule = PatternRule::new("COUNTRY_CODE", "[A-Z]{2}").unwrap();
93 let errors = rule.validate("gb", "/p");
94 assert!(!errors.is_empty());
95 }
96
97 #[test]
98 fn two_letter_country_code_rejects_extra_chars() {
99 let rule = PatternRule::new("COUNTRY_CODE", "[A-Z]{2}").unwrap();
100 let errors = rule.validate("GBR", "/p");
101 assert!(!errors.is_empty());
102 }
103
104 #[test]
105 fn bic_pattern_passes() {
106 let rule = PatternRule::new(
107 "BIC_PATTERN",
108 "[A-Z0-9]{4,4}[A-Z]{2,2}[A-Z0-9]{2,2}([A-Z0-9]{3,3}){0,1}",
109 )
110 .unwrap();
111 assert!(rule.validate("AAAAGB2L", "/p").is_empty());
112 assert!(rule.validate("AAAAGB2LXXX", "/p").is_empty());
113 }
114
115 #[test]
116 fn bic_pattern_rejects_short() {
117 let rule = PatternRule::new(
118 "BIC_PATTERN",
119 "[A-Z0-9]{4,4}[A-Z]{2,2}[A-Z0-9]{2,2}([A-Z0-9]{3,3}){0,1}",
120 )
121 .unwrap();
122 let errors = rule.validate("AAAA", "/p");
123 assert!(!errors.is_empty());
124 }
125
126 #[test]
127 fn error_contains_pattern_and_rule_id() {
128 let rule = PatternRule::new("MY_RULE", "[A-Z]{3}").unwrap();
129 let errors = rule.validate("abc", "/some/path");
130 assert_eq!(errors.len(), 1);
131 assert_eq!(errors[0].rule_id, "MY_RULE");
132 assert_eq!(errors[0].path, "/some/path");
133 assert!(errors[0].message.contains("[A-Z]{3}"));
134 }
135
136 #[test]
137 fn invalid_regex_returns_error() {
138 let result = PatternRule::new("BAD", "[unclosed");
139 assert!(result.is_err());
140 }
141
142 #[test]
143 fn empty_string_matches_empty_pattern() {
144 let rule = PatternRule::new("EMPTY", "").unwrap();
145 assert!(rule.validate("", "/p").is_empty());
146 let errors = rule.validate("x", "/p");
148 assert!(!errors.is_empty());
149 }
150
151 #[test]
152 fn pattern_is_full_string_match_not_partial() {
153 let rule = PatternRule::new("R", "[A-Z]{2}").unwrap();
155 let errors = rule.validate("ABCD", "/p");
156 assert!(!errors.is_empty(), "Partial match should be rejected");
157 }
158}