1use super::ValidationError;
6use regex::Regex;
7use std::sync::OnceLock;
8
9static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
11
12fn email_regex() -> &'static Regex {
13 EMAIL_REGEX
14 .get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap())
15}
16
17pub fn validate_email(field: &str, value: &str) -> Result<(), ValidationError> {
19 if email_regex().is_match(value) {
20 Ok(())
21 } else {
22 Err(ValidationError::Email {
23 field: field.to_string(),
24 value: value.to_string(),
25 })
26 }
27}
28
29pub fn validate_length(
31 field: &str,
32 value: &str,
33 min: usize,
34 max: usize,
35) -> Result<(), ValidationError> {
36 let actual = value.chars().count();
37 if actual >= min && actual <= max {
38 Ok(())
39 } else {
40 Err(ValidationError::Length {
41 field: field.to_string(),
42 min,
43 max,
44 actual,
45 })
46 }
47}
48
49pub fn validate_range<T: PartialOrd + std::fmt::Display>(
51 field: &str,
52 value: T,
53 min: T,
54 max: T,
55) -> Result<(), ValidationError> {
56 if value >= min && value <= max {
57 Ok(())
58 } else {
59 Err(ValidationError::Range {
60 field: field.to_string(),
61 min: min.to_string(),
62 max: max.to_string(),
63 actual: value.to_string(),
64 })
65 }
66}
67
68pub fn validate_regex(field: &str, value: &str, pattern: &str) -> Result<(), ValidationError> {
70 match Regex::new(pattern) {
71 Ok(re) => {
72 if re.is_match(value) {
73 Ok(())
74 } else {
75 Err(ValidationError::Regex {
76 field: field.to_string(),
77 pattern: pattern.to_string(),
78 value: value.to_string(),
79 })
80 }
81 }
82 Err(_) => Err(ValidationError::Regex {
83 field: field.to_string(),
84 pattern: pattern.to_string(),
85 value: value.to_string(),
86 }),
87 }
88}
89
90pub fn validate_required(field: &str, value: &str) -> Result<(), ValidationError> {
92 if value.is_empty() {
93 Err(ValidationError::Required {
94 field: field.to_string(),
95 })
96 } else {
97 Ok(())
98 }
99}
100
101pub fn validate_contains(field: &str, value: &str, substring: &str) -> Result<(), ValidationError> {
103 if value.contains(substring) {
104 Ok(())
105 } else {
106 Err(ValidationError::Contains {
107 field: field.to_string(),
108 substring: substring.to_string(),
109 })
110 }
111}
112
113pub fn validate_does_not_contain(
115 field: &str,
116 value: &str,
117 substring: &str,
118) -> Result<(), ValidationError> {
119 if !value.contains(substring) {
120 Ok(())
121 } else {
122 Err(ValidationError::DoesNotContain {
123 field: field.to_string(),
124 substring: substring.to_string(),
125 })
126 }
127}
128
129pub fn validate_custom(field: &str, reason: &str) -> Result<(), ValidationError> {
131 Err(ValidationError::Custom {
132 field: field.to_string(),
133 reason: reason.to_string(),
134 })
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_validate_email_valid() {
143 assert!(validate_email("email", "user@example.com").is_ok());
144 assert!(validate_email("email", "a.b+c@d.co").is_ok());
145 }
146
147 #[test]
148 fn test_validate_email_invalid() {
149 assert!(validate_email("email", "noatsign").is_err());
150 assert!(validate_email("email", "").is_err());
151 assert!(validate_email("email", "a@").is_err());
152 assert!(validate_email("email", "@b.com").is_err());
153 }
154
155 #[test]
156 fn test_validate_length_boundary() {
157 assert!(validate_length("name", "abc", 3, 5).is_ok());
158 assert!(validate_length("name", "abcde", 3, 5).is_ok());
159 }
160
161 #[test]
162 fn test_validate_length_too_long() {
163 assert!(validate_length("name", "abcdef", 3, 5).is_err());
164 }
165
166 #[test]
167 fn test_validate_length_empty() {
168 assert!(validate_length("name", "", 1, 5).is_err());
169 }
170
171 #[test]
172 fn test_validate_length_unicode() {
173 assert!(validate_length("name", "你好", 2, 2).is_ok());
174 }
175
176 #[test]
177 fn test_validate_range_boundary() {
178 assert!(validate_range("age", 18i64, 18, 65).is_ok());
179 assert!(validate_range("age", 65i64, 18, 65).is_ok());
180 }
181
182 #[test]
183 fn test_validate_range_out_of_range() {
184 assert!(validate_range("age", 17i64, 18, 65).is_err());
185 assert!(validate_range("age", 66i64, 18, 65).is_err());
186 }
187
188 #[test]
189 fn test_validate_range_negative() {
190 assert!(validate_range("temp", -10.5f64, -20.0, 50.0).is_ok());
191 assert!(validate_range("temp", -30.0f64, -20.0, 50.0).is_err());
192 }
193
194 #[test]
195 fn test_validate_regex_match() {
196 assert!(validate_regex("code", "ABC123", r"^[A-Z]{3}\d{3}$").is_ok());
197 }
198
199 #[test]
200 fn test_validate_regex_no_match() {
201 assert!(validate_regex("code", "abc123", r"^[A-Z]{3}\d{3}$").is_err());
202 }
203
204 #[test]
205 fn test_validate_regex_invalid_pattern() {
206 assert!(validate_regex("code", "abc", r"[invalid").is_err());
207 }
208
209 #[test]
210 fn test_validate_required_non_empty() {
211 assert!(validate_required("name", "value").is_ok());
212 }
213
214 #[test]
215 fn test_validate_required_empty() {
216 assert!(validate_required("name", "").is_err());
217 }
218
219 #[test]
220 fn test_validate_contains_found() {
221 assert!(validate_contains("url", "https://example.com", "https://").is_ok());
222 }
223
224 #[test]
225 fn test_validate_contains_not_found() {
226 assert!(validate_contains("url", "ftp://example.com", "https://").is_err());
227 }
228
229 #[test]
230 fn test_validate_contains_empty_substring() {
231 assert!(validate_contains("name", "anything", "").is_ok());
232 }
233
234 #[test]
235 fn test_validate_does_not_contain_clean() {
236 assert!(validate_does_not_contain("name", "hello", "sql").is_ok());
237 }
238
239 #[test]
240 fn test_validate_does_not_contain_dirty() {
241 assert!(validate_does_not_contain("name", "drop table", "drop").is_err());
242 }
243
244 #[test]
245 fn test_validate_does_not_contain_empty_substring() {
246 assert!(validate_does_not_contain("name", "anything", "").is_err());
247 }
248
249 #[test]
250 fn test_aggregate_empty() {
251 let results = vec![];
252 assert!(super::super::aggregate(results).is_ok());
253 }
254
255 #[test]
256 fn test_aggregate_all_ok() {
257 let results = vec![Ok(()), Ok(()), Ok(())];
258 assert!(super::super::aggregate(results).is_ok());
259 }
260
261 #[test]
262 fn test_aggregate_single_error() {
263 let results = vec![Ok(()), Err(ValidationError::Required { field: "x".into() })];
264 let result = super::super::aggregate(results);
265 assert!(matches!(result, Err(ValidationError::Required { .. })));
266 }
267
268 #[test]
269 fn test_aggregate_multiple_errors() {
270 let results = vec![
271 Err(ValidationError::Required { field: "a".into() }),
272 Ok(()),
273 Err(ValidationError::Required { field: "b".into() }),
274 ];
275 let result = super::super::aggregate(results);
276 match result {
277 Err(ValidationError::Aggregate { errors, count }) => {
278 assert_eq!(errors.len(), 2);
279 assert_eq!(count, 2);
280 }
281 _ => panic!("expected Aggregate with 2 errors"),
282 }
283 }
284}