fraiseql_core/validation/
rich_scalars.rs1use std::sync::LazyLock;
7
8use regex::Regex;
9
10static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
12 Regex::new(
13 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])?)*$"
14 ).expect("email regex is valid")
15});
16
17static PHONE_REGEX: LazyLock<Regex> =
19 LazyLock::new(|| Regex::new(r"^\+?[1-9]\d{1,14}$").expect("phone regex is valid"));
20
21static VIN_REGEX: LazyLock<Regex> =
23 LazyLock::new(|| Regex::new(r"^[A-HJ-NPR-Z0-9]{17}$").expect("VIN regex is valid"));
24
25static COUNTRY_CODE_REGEX: LazyLock<Regex> =
27 LazyLock::new(|| Regex::new(r"^[A-Z]{2}$").expect("country code regex is valid"));
28
29pub struct EmailValidator;
31
32impl EmailValidator {
33 pub fn validate(value: &str) -> bool {
38 !value.is_empty() && value.len() <= 254 && EMAIL_REGEX.is_match(value)
39 }
40
41 pub fn error_message() -> &'static str {
42 "Invalid email format"
43 }
44}
45
46pub struct PhoneNumberValidator;
48
49impl PhoneNumberValidator {
50 pub fn validate(value: &str) -> bool {
59 !value.is_empty() && value.len() <= 20 && PHONE_REGEX.is_match(value)
60 }
61
62 pub fn error_message() -> &'static str {
63 "Invalid phone number format"
64 }
65}
66
67pub struct VinValidator;
69
70impl VinValidator {
71 pub fn validate(value: &str) -> bool {
80 let value_upper = value.to_uppercase();
81 VIN_REGEX.is_match(&value_upper)
82 }
83
84 pub fn error_message() -> &'static str {
85 "Invalid VIN format (must be 17 alphanumeric characters, excluding I, O, Q)"
86 }
87}
88
89pub struct CountryCodeValidator {
91 valid_codes: std::collections::HashSet<&'static str>,
92}
93
94impl CountryCodeValidator {
95 pub fn new() -> Self {
97 let mut codes = std::collections::HashSet::new();
98 codes.insert("AD");
100 codes.insert("AE");
101 codes.insert("AF");
102 codes.insert("AG");
103 codes.insert("AI");
104 codes.insert("AL");
105 codes.insert("AM");
106 codes.insert("AO");
107 codes.insert("AQ");
108 codes.insert("AR");
109 codes.insert("AS");
110 codes.insert("AT");
111 codes.insert("AU");
112 codes.insert("AW");
113 codes.insert("AX");
114 codes.insert("AZ");
115 codes.insert("BA");
116 codes.insert("BB");
117 codes.insert("BD");
118 codes.insert("BE");
119 codes.insert("BF");
120 codes.insert("BG");
121 codes.insert("BH");
122 codes.insert("BI");
123 codes.insert("BJ");
124 codes.insert("BL");
125 codes.insert("BM");
126 codes.insert("BN");
127 codes.insert("BO");
128 codes.insert("BQ");
129 codes.insert("BR");
130 codes.insert("BS");
131 codes.insert("BT");
132 codes.insert("BV");
133 codes.insert("BW");
134 codes.insert("BY");
135 codes.insert("BZ");
136 codes.insert("CA");
137 codes.insert("CC");
138 codes.insert("CD");
139 codes.insert("CF");
140 codes.insert("CG");
141 codes.insert("CH");
142 codes.insert("CI");
143 codes.insert("CK");
144 codes.insert("CL");
145 codes.insert("CM");
146 codes.insert("CN");
147 codes.insert("CO");
148 codes.insert("CR");
149 codes.insert("CU");
150 codes.insert("CV");
151 codes.insert("CW");
152 codes.insert("CX");
153 codes.insert("CY");
154 codes.insert("CZ");
155 codes.insert("DE");
156 codes.insert("DJ");
157 codes.insert("DK");
158 codes.insert("DM");
159 codes.insert("DO");
160 codes.insert("DZ");
161 codes.insert("EC");
162 codes.insert("EE");
163 codes.insert("EG");
164 codes.insert("EH");
165 codes.insert("ER");
166 codes.insert("ES");
167 codes.insert("ET");
168 codes.insert("FI");
169 codes.insert("FJ");
170 codes.insert("FK");
171 codes.insert("FM");
172 codes.insert("FO");
173 codes.insert("FR");
174 codes.insert("GA");
175 codes.insert("GB");
176 codes.insert("GD");
177 codes.insert("GE");
178 codes.insert("GF");
179 codes.insert("GG");
180 codes.insert("GH");
181 codes.insert("GI");
182 codes.insert("GL");
183 codes.insert("GM");
184 codes.insert("GN");
185 codes.insert("GP");
186 codes.insert("GQ");
187 codes.insert("GR");
188 codes.insert("GS");
189 codes.insert("GT");
190 codes.insert("GU");
191 codes.insert("GW");
192 codes.insert("GY");
193 codes.insert("HK");
194 codes.insert("HM");
195 codes.insert("HN");
196 codes.insert("HR");
197 codes.insert("HT");
198 codes.insert("HU");
199 codes.insert("ID");
200 codes.insert("IE");
201 codes.insert("IL");
202 codes.insert("IM");
203 codes.insert("IN");
204 codes.insert("IO");
205 codes.insert("IQ");
206 codes.insert("IR");
207 codes.insert("IS");
208 codes.insert("IT");
209 codes.insert("JE");
210 codes.insert("JM");
211 codes.insert("JO");
212 codes.insert("JP");
213 codes.insert("KE");
214 codes.insert("KG");
215 codes.insert("KH");
216 codes.insert("KI");
217 codes.insert("KM");
218 codes.insert("KN");
219 codes.insert("KP");
220 codes.insert("KR");
221 codes.insert("KW");
222 codes.insert("KY");
223 codes.insert("KZ");
224 codes.insert("LA");
225 codes.insert("LB");
226 codes.insert("LC");
227 codes.insert("LI");
228 codes.insert("LK");
229 codes.insert("LR");
230 codes.insert("LS");
231 codes.insert("LT");
232 codes.insert("LU");
233 codes.insert("LV");
234 codes.insert("LY");
235 codes.insert("MA");
236 codes.insert("MC");
237 codes.insert("MD");
238 codes.insert("ME");
239 codes.insert("MF");
240 codes.insert("MG");
241 codes.insert("MH");
242 codes.insert("MK");
243 codes.insert("ML");
244 codes.insert("MM");
245 codes.insert("MN");
246 codes.insert("MO");
247 codes.insert("MP");
248 codes.insert("MQ");
249 codes.insert("MR");
250 codes.insert("MS");
251 codes.insert("MT");
252 codes.insert("MU");
253 codes.insert("MV");
254 codes.insert("MW");
255 codes.insert("MX");
256 codes.insert("MY");
257 codes.insert("MZ");
258 codes.insert("NA");
259 codes.insert("NC");
260 codes.insert("NE");
261 codes.insert("NF");
262 codes.insert("NG");
263 codes.insert("NI");
264 codes.insert("NL");
265 codes.insert("NO");
266 codes.insert("NP");
267 codes.insert("NR");
268 codes.insert("NU");
269 codes.insert("NZ");
270 codes.insert("OM");
271 codes.insert("PA");
272 codes.insert("PE");
273 codes.insert("PF");
274 codes.insert("PG");
275 codes.insert("PH");
276 codes.insert("PK");
277 codes.insert("PL");
278 codes.insert("PM");
279 codes.insert("PN");
280 codes.insert("PR");
281 codes.insert("PS");
282 codes.insert("PT");
283 codes.insert("PW");
284 codes.insert("PY");
285 codes.insert("QA");
286 codes.insert("RE");
287 codes.insert("RO");
288 codes.insert("RS");
289 codes.insert("RU");
290 codes.insert("RW");
291 codes.insert("SA");
292 codes.insert("SB");
293 codes.insert("SC");
294 codes.insert("SD");
295 codes.insert("SE");
296 codes.insert("SG");
297 codes.insert("SH");
298 codes.insert("SI");
299 codes.insert("SJ");
300 codes.insert("SK");
301 codes.insert("SL");
302 codes.insert("SM");
303 codes.insert("SN");
304 codes.insert("SO");
305 codes.insert("SR");
306 codes.insert("SS");
307 codes.insert("ST");
308 codes.insert("SV");
309 codes.insert("SX");
310 codes.insert("SY");
311 codes.insert("SZ");
312 codes.insert("TC");
313 codes.insert("TD");
314 codes.insert("TF");
315 codes.insert("TG");
316 codes.insert("TH");
317 codes.insert("TJ");
318 codes.insert("TK");
319 codes.insert("TL");
320 codes.insert("TM");
321 codes.insert("TN");
322 codes.insert("TO");
323 codes.insert("TR");
324 codes.insert("TT");
325 codes.insert("TV");
326 codes.insert("TW");
327 codes.insert("TZ");
328 codes.insert("UA");
329 codes.insert("UG");
330 codes.insert("UM");
331 codes.insert("US");
332 codes.insert("UY");
333 codes.insert("UZ");
334 codes.insert("VA");
335 codes.insert("VC");
336 codes.insert("VE");
337 codes.insert("VG");
338 codes.insert("VI");
339 codes.insert("VN");
340 codes.insert("VU");
341 codes.insert("WF");
342 codes.insert("WS");
343 codes.insert("YE");
344 codes.insert("YT");
345 codes.insert("ZA");
346 codes.insert("ZM");
347 codes.insert("ZW");
348 Self { valid_codes: codes }
349 }
350
351 pub fn validate(&self, value: &str) -> bool {
353 let value_upper = value.to_uppercase();
354 COUNTRY_CODE_REGEX.is_match(&value_upper) && self.valid_codes.contains(value_upper.as_str())
355 }
356
357 pub fn error_message() -> &'static str {
358 "Invalid country code (must be ISO 3166-1 alpha-2)"
359 }
360}
361
362impl Default for CountryCodeValidator {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
374 fn test_email_valid() {
375 assert!(EmailValidator::validate("user@example.com"));
376 assert!(EmailValidator::validate("john.doe@company.co.uk"));
377 }
378
379 #[test]
380 fn test_email_invalid() {
381 assert!(!EmailValidator::validate("invalid.email"));
382 assert!(!EmailValidator::validate("user@"));
383 assert!(!EmailValidator::validate("@example.com"));
384 }
385
386 #[test]
387 fn test_email_empty() {
388 assert!(!EmailValidator::validate(""));
389 }
390
391 #[test]
393 fn test_phone_valid_plus_format() {
394 assert!(PhoneNumberValidator::validate("+1234567890"));
395 assert!(PhoneNumberValidator::validate("+33612345678"));
396 }
397
398 #[test]
399 fn test_phone_valid_no_plus() {
400 assert!(PhoneNumberValidator::validate("1234567890"));
401 }
402
403 #[test]
404 fn test_phone_invalid() {
405 assert!(!PhoneNumberValidator::validate("+0123456789")); assert!(!PhoneNumberValidator::validate(""));
407 }
408
409 #[test]
411 fn test_vin_valid() {
412 assert!(VinValidator::validate("3G1FB1E30D1109186"));
413 assert!(VinValidator::validate("JH2RC5004LM200591"));
414 }
415
416 #[test]
417 fn test_vin_valid_lowercase() {
418 assert!(VinValidator::validate("3g1fb1e30d1109186"));
419 }
420
421 #[test]
422 fn test_vin_invalid_length() {
423 assert!(!VinValidator::validate("3G1FB1E30D110918"));
424 assert!(!VinValidator::validate("3G1FB1E30D11091861"));
425 }
426
427 #[test]
428 fn test_vin_invalid_chars() {
429 assert!(!VinValidator::validate("3G1FB1E30D110918I")); assert!(!VinValidator::validate("3G1FB1E30D110918O")); assert!(!VinValidator::validate("3G1FB1E30D110918Q")); }
433
434 #[test]
436 fn test_country_code_valid() {
437 let validator = CountryCodeValidator::new();
438 assert!(validator.validate("US"));
439 assert!(validator.validate("GB"));
440 assert!(validator.validate("DE"));
441 assert!(validator.validate("FR"));
442 }
443
444 #[test]
445 fn test_country_code_lowercase() {
446 let validator = CountryCodeValidator::new();
447 assert!(validator.validate("us"));
448 assert!(validator.validate("gb"));
449 }
450
451 #[test]
452 fn test_country_code_invalid() {
453 let validator = CountryCodeValidator::new();
454 assert!(!validator.validate("XX"));
455 assert!(!validator.validate("USA"));
456 assert!(!validator.validate("U"));
457 }
458}