1use regex::Regex;
49use std::sync::OnceLock;
50
51#[derive(Debug, Clone)]
53pub struct ValidationError {
54 pub field: String,
55 pub message: String,
56}
57
58impl ValidationError {
59 pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
60 Self {
61 field: field.into(),
62 message: message.into(),
63 }
64 }
65}
66
67impl std::fmt::Display for ValidationError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(
70 f,
71 "Validation error on field '{}': {}",
72 self.field, self.message
73 )
74 }
75}
76
77impl std::error::Error for ValidationError {}
78
79fn email_regex() -> &'static Regex {
81 static EMAIL_REGEX: OnceLock<Regex> = OnceLock::new();
82 EMAIL_REGEX
83 .get_or_init(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap())
84}
85
86fn url_regex() -> &'static Regex {
88 static URL_REGEX: OnceLock<Regex> = OnceLock::new();
89 URL_REGEX.get_or_init(|| Regex::new(r"^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$").unwrap())
90}
91
92fn phone_regex() -> &'static Regex {
94 static PHONE_REGEX: OnceLock<Regex> = OnceLock::new();
95 PHONE_REGEX.get_or_init(|| Regex::new(r"^\+[1-9]\d{1,14}$").unwrap())
96}
97
98pub mod string {
100 use super::*;
101
102 pub fn email(value: &str) -> Result<(), String> {
103 if email_regex().is_match(value) {
104 Ok(())
105 } else {
106 Err("Invalid email format".into())
107 }
108 }
109
110 pub fn phone(value: &str) -> Result<(), String> {
111 if phone_regex().is_match(value) {
112 Ok(())
113 } else {
114 Err("Invalid phone number (use E.164 format: +1234567890)".into())
115 }
116 }
117
118 pub fn url(value: &str) -> Result<(), String> {
119 if url_regex().is_match(value) {
120 Ok(())
121 } else {
122 Err("Invalid URL format".into())
123 }
124 }
125
126 pub fn non_empty(value: &str) -> Result<(), String> {
127 if !value.is_empty() {
128 Ok(())
129 } else {
130 Err("Value cannot be empty".into())
131 }
132 }
133
134 pub fn min_length(value: &str, min: usize) -> Result<(), String> {
135 if value.len() >= min {
136 Ok(())
137 } else {
138 Err(format!("Value must be at least {min} characters"))
139 }
140 }
141
142 pub fn max_length(value: &str, max: usize) -> Result<(), String> {
143 if value.len() <= max {
144 Ok(())
145 } else {
146 Err(format!("Value must be at most {max} characters"))
147 }
148 }
149
150 pub fn pattern(value: &str, pattern: &str) -> Result<(), String> {
151 let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex pattern: {e}"))?;
152
153 if regex.is_match(value) {
154 Ok(())
155 } else {
156 Err(format!("Value does not match pattern: {pattern}"))
157 }
158 }
159
160 pub fn enum_values(value: &str, allowed: &[&str]) -> Result<(), String> {
161 if allowed.contains(&value) {
162 Ok(())
163 } else {
164 Err(format!("Value must be one of: {}", allowed.join(", ")))
165 }
166 }
167}
168
169pub mod integer {
171 pub fn min(value: i64, min: i64) -> Result<(), String> {
172 if value >= min {
173 Ok(())
174 } else {
175 Err(format!("Value must be at least {min}"))
176 }
177 }
178
179 pub fn max(value: i64, max: i64) -> Result<(), String> {
180 if value <= max {
181 Ok(())
182 } else {
183 Err(format!("Value must be at most {max}"))
184 }
185 }
186
187 pub fn range(value: i64, min: i64, max: i64) -> Result<(), String> {
188 if value >= min && value <= max {
189 Ok(())
190 } else {
191 Err(format!("Value must be between {min} and {max}"))
192 }
193 }
194
195 pub fn positive(value: i64) -> Result<(), String> {
196 if value > 0 {
197 Ok(())
198 } else {
199 Err("Value must be positive".into())
200 }
201 }
202
203 pub fn non_negative(value: i64) -> Result<(), String> {
204 if value >= 0 {
205 Ok(())
206 } else {
207 Err("Value must be non-negative".into())
208 }
209 }
210}
211
212pub mod decimal {
214 pub fn min(value: f64, min: f64) -> Result<(), String> {
215 if value >= min {
216 Ok(())
217 } else {
218 Err(format!("Value must be at least {min}"))
219 }
220 }
221
222 pub fn max(value: f64, max: f64) -> Result<(), String> {
223 if value <= max {
224 Ok(())
225 } else {
226 Err(format!("Value must be at most {max}"))
227 }
228 }
229
230 pub fn range(value: f64, min: f64, max: f64) -> Result<(), String> {
231 if value >= min && value <= max {
232 Ok(())
233 } else {
234 Err(format!("Value must be between {min} and {max}"))
235 }
236 }
237
238 pub fn positive(value: f64) -> Result<(), String> {
239 if value > 0.0 {
240 Ok(())
241 } else {
242 Err("Value must be positive".into())
243 }
244 }
245
246 pub fn non_negative(value: f64) -> Result<(), String> {
247 if value >= 0.0 {
248 Ok(())
249 } else {
250 Err("Value must be non-negative".into())
251 }
252 }
253}
254
255fn on_validation_err(validator: &str) -> impl FnOnce(String) -> String {
256 let validator = validator.to_string();
257 move |e: String| {
258 crate::instrumentation::record_validation_failure("", "", &validator);
259 e
260 }
261}
262
263pub fn validate_email(value: &str) -> Result<(), String> {
265 string::email(value).map_err(on_validation_err("email"))
266}
267
268pub fn validate_phone(value: &str) -> Result<(), String> {
269 string::phone(value).map_err(on_validation_err("phone"))
270}
271
272pub fn validate_non_empty(value: &str) -> Result<(), String> {
273 string::non_empty(value).map_err(on_validation_err("non_empty"))
274}
275
276pub fn validate_min_length(value: &str, min: usize) -> Result<(), String> {
277 string::min_length(value, min).map_err(on_validation_err("min_length"))
278}
279
280pub fn validate_max_length(value: &str, max: usize) -> Result<(), String> {
281 string::max_length(value, max).map_err(on_validation_err("max_length"))
282}
283
284pub fn validate_enum(value: &str, allowed: &[&str]) -> Result<(), String> {
285 string::enum_values(value, allowed).map_err(on_validation_err("enum"))
286}
287
288pub fn validate_pattern(value: &str, pattern: &str) -> Result<(), String> {
289 string::pattern(value, pattern).map_err(on_validation_err("pattern"))
290}
291
292pub fn validate_non_negative(value: &i64) -> Result<(), String> {
293 integer::non_negative(*value).map_err(on_validation_err("non_negative"))
294}
295
296pub fn validate_positive(value: &i64) -> Result<(), String> {
297 integer::positive(*value).map_err(on_validation_err("positive"))
298}
299
300pub fn validate_min(value: &i64, min: i64) -> Result<(), String> {
301 integer::min(*value, min).map_err(on_validation_err("min"))
302}
303
304pub fn validate_max(value: &i64, max: i64) -> Result<(), String> {
305 integer::max(*value, max).map_err(on_validation_err("max"))
306}
307
308pub fn validate_range(value: &i64, min: i64, max: i64) -> Result<(), String> {
309 integer::range(*value, min, max).map_err(on_validation_err("range"))
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_email_validator() {
318 assert!(string::email("test@example.com").is_ok());
319 assert!(string::email("user.name+tag@example.co.uk").is_ok());
320 assert!(string::email("invalid").is_err());
321 assert!(string::email("@example.com").is_err());
322 assert!(string::email("test@").is_err());
323 }
324
325 #[test]
326 fn test_phone_validator() {
327 assert!(string::phone("+12345678901").is_ok());
328 assert!(string::phone("+442071234567").is_ok());
329 assert!(string::phone("123456").is_err());
330 assert!(string::phone("+0123456").is_err()); }
332
333 #[test]
334 fn test_url_validator() {
335 assert!(string::url("https://example.com").is_ok());
336 assert!(string::url("http://example.com/path").is_ok());
337 assert!(string::url("example.com").is_err());
338 assert!(string::url("ftp://example.com").is_err());
339 }
340
341 #[test]
342 fn test_string_length() {
343 assert!(string::min_length("hello", 3).is_ok());
344 assert!(string::min_length("hi", 3).is_err());
345
346 assert!(string::max_length("hello", 10).is_ok());
347 assert!(string::max_length("hello world!", 5).is_err());
348 }
349
350 #[test]
351 fn test_integer_validators() {
352 assert!(integer::min(10, 5).is_ok());
353 assert!(integer::min(3, 5).is_err());
354
355 assert!(integer::max(10, 15).is_ok());
356 assert!(integer::max(20, 15).is_err());
357
358 assert!(integer::range(10, 5, 15).is_ok());
359 assert!(integer::range(3, 5, 15).is_err());
360
361 assert!(integer::positive(1).is_ok());
362 assert!(integer::positive(0).is_err());
363 assert!(integer::positive(-1).is_err());
364
365 assert!(integer::non_negative(0).is_ok());
366 assert!(integer::non_negative(1).is_ok());
367 assert!(integer::non_negative(-1).is_err());
368 }
369
370 #[test]
371 fn test_enum_validator() {
372 let allowed = &["active", "inactive", "pending"];
373
374 assert!(string::enum_values("active", allowed).is_ok());
375 assert!(string::enum_values("invalid", allowed).is_err());
376 }
377
378 #[test]
379 fn test_pattern_validator() {
380 assert!(string::pattern("ABC123", r"^[A-Z]{3}\d{3}$").is_ok());
381 assert!(string::pattern("abc123", r"^[A-Z]{3}\d{3}$").is_err());
382 }
383}