fraiseql_core/validation/
rich_scalars.rs1use std::sync::LazyLock;
7
8use regex::Regex;
9
10use crate::validation::patterns;
11
12static EMAIL_REGEX: LazyLock<Regex> =
14 LazyLock::new(|| Regex::new(patterns::EMAIL).expect("email regex is valid"));
15
16static PHONE_REGEX: LazyLock<Regex> =
19 LazyLock::new(|| Regex::new(patterns::PHONE_LENIENT).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 #[must_use]
43 pub const fn error_message() -> &'static str {
44 "Invalid email format"
45 }
46}
47
48pub struct PhoneNumberValidator;
50
51impl PhoneNumberValidator {
52 pub fn validate(value: &str) -> bool {
61 !value.is_empty() && value.len() <= 20 && PHONE_REGEX.is_match(value)
62 }
63
64 #[must_use]
66 pub const fn error_message() -> &'static str {
67 "Invalid phone number format"
68 }
69}
70
71pub struct VinValidator;
73
74impl VinValidator {
75 pub fn validate(value: &str) -> bool {
84 if value.len() != 17 {
87 return false;
88 }
89 let value_upper = value.to_uppercase();
90 VIN_REGEX.is_match(&value_upper)
91 }
92
93 #[must_use]
95 pub const fn error_message() -> &'static str {
96 "Invalid VIN format (must be 17 alphanumeric characters, excluding I, O, Q)"
97 }
98}
99
100#[doc(hidden)] pub struct CountryCodeValidator {
103 valid_codes: std::collections::HashSet<&'static str>,
104}
105
106impl CountryCodeValidator {
107 #[must_use]
109 pub fn new() -> Self {
110 let mut codes = std::collections::HashSet::new();
111 codes.insert("AD");
113 codes.insert("AE");
114 codes.insert("AF");
115 codes.insert("AG");
116 codes.insert("AI");
117 codes.insert("AL");
118 codes.insert("AM");
119 codes.insert("AO");
120 codes.insert("AQ");
121 codes.insert("AR");
122 codes.insert("AS");
123 codes.insert("AT");
124 codes.insert("AU");
125 codes.insert("AW");
126 codes.insert("AX");
127 codes.insert("AZ");
128 codes.insert("BA");
129 codes.insert("BB");
130 codes.insert("BD");
131 codes.insert("BE");
132 codes.insert("BF");
133 codes.insert("BG");
134 codes.insert("BH");
135 codes.insert("BI");
136 codes.insert("BJ");
137 codes.insert("BL");
138 codes.insert("BM");
139 codes.insert("BN");
140 codes.insert("BO");
141 codes.insert("BQ");
142 codes.insert("BR");
143 codes.insert("BS");
144 codes.insert("BT");
145 codes.insert("BV");
146 codes.insert("BW");
147 codes.insert("BY");
148 codes.insert("BZ");
149 codes.insert("CA");
150 codes.insert("CC");
151 codes.insert("CD");
152 codes.insert("CF");
153 codes.insert("CG");
154 codes.insert("CH");
155 codes.insert("CI");
156 codes.insert("CK");
157 codes.insert("CL");
158 codes.insert("CM");
159 codes.insert("CN");
160 codes.insert("CO");
161 codes.insert("CR");
162 codes.insert("CU");
163 codes.insert("CV");
164 codes.insert("CW");
165 codes.insert("CX");
166 codes.insert("CY");
167 codes.insert("CZ");
168 codes.insert("DE");
169 codes.insert("DJ");
170 codes.insert("DK");
171 codes.insert("DM");
172 codes.insert("DO");
173 codes.insert("DZ");
174 codes.insert("EC");
175 codes.insert("EE");
176 codes.insert("EG");
177 codes.insert("EH");
178 codes.insert("ER");
179 codes.insert("ES");
180 codes.insert("ET");
181 codes.insert("FI");
182 codes.insert("FJ");
183 codes.insert("FK");
184 codes.insert("FM");
185 codes.insert("FO");
186 codes.insert("FR");
187 codes.insert("GA");
188 codes.insert("GB");
189 codes.insert("GD");
190 codes.insert("GE");
191 codes.insert("GF");
192 codes.insert("GG");
193 codes.insert("GH");
194 codes.insert("GI");
195 codes.insert("GL");
196 codes.insert("GM");
197 codes.insert("GN");
198 codes.insert("GP");
199 codes.insert("GQ");
200 codes.insert("GR");
201 codes.insert("GS");
202 codes.insert("GT");
203 codes.insert("GU");
204 codes.insert("GW");
205 codes.insert("GY");
206 codes.insert("HK");
207 codes.insert("HM");
208 codes.insert("HN");
209 codes.insert("HR");
210 codes.insert("HT");
211 codes.insert("HU");
212 codes.insert("ID");
213 codes.insert("IE");
214 codes.insert("IL");
215 codes.insert("IM");
216 codes.insert("IN");
217 codes.insert("IO");
218 codes.insert("IQ");
219 codes.insert("IR");
220 codes.insert("IS");
221 codes.insert("IT");
222 codes.insert("JE");
223 codes.insert("JM");
224 codes.insert("JO");
225 codes.insert("JP");
226 codes.insert("KE");
227 codes.insert("KG");
228 codes.insert("KH");
229 codes.insert("KI");
230 codes.insert("KM");
231 codes.insert("KN");
232 codes.insert("KP");
233 codes.insert("KR");
234 codes.insert("KW");
235 codes.insert("KY");
236 codes.insert("KZ");
237 codes.insert("LA");
238 codes.insert("LB");
239 codes.insert("LC");
240 codes.insert("LI");
241 codes.insert("LK");
242 codes.insert("LR");
243 codes.insert("LS");
244 codes.insert("LT");
245 codes.insert("LU");
246 codes.insert("LV");
247 codes.insert("LY");
248 codes.insert("MA");
249 codes.insert("MC");
250 codes.insert("MD");
251 codes.insert("ME");
252 codes.insert("MF");
253 codes.insert("MG");
254 codes.insert("MH");
255 codes.insert("MK");
256 codes.insert("ML");
257 codes.insert("MM");
258 codes.insert("MN");
259 codes.insert("MO");
260 codes.insert("MP");
261 codes.insert("MQ");
262 codes.insert("MR");
263 codes.insert("MS");
264 codes.insert("MT");
265 codes.insert("MU");
266 codes.insert("MV");
267 codes.insert("MW");
268 codes.insert("MX");
269 codes.insert("MY");
270 codes.insert("MZ");
271 codes.insert("NA");
272 codes.insert("NC");
273 codes.insert("NE");
274 codes.insert("NF");
275 codes.insert("NG");
276 codes.insert("NI");
277 codes.insert("NL");
278 codes.insert("NO");
279 codes.insert("NP");
280 codes.insert("NR");
281 codes.insert("NU");
282 codes.insert("NZ");
283 codes.insert("OM");
284 codes.insert("PA");
285 codes.insert("PE");
286 codes.insert("PF");
287 codes.insert("PG");
288 codes.insert("PH");
289 codes.insert("PK");
290 codes.insert("PL");
291 codes.insert("PM");
292 codes.insert("PN");
293 codes.insert("PR");
294 codes.insert("PS");
295 codes.insert("PT");
296 codes.insert("PW");
297 codes.insert("PY");
298 codes.insert("QA");
299 codes.insert("RE");
300 codes.insert("RO");
301 codes.insert("RS");
302 codes.insert("RU");
303 codes.insert("RW");
304 codes.insert("SA");
305 codes.insert("SB");
306 codes.insert("SC");
307 codes.insert("SD");
308 codes.insert("SE");
309 codes.insert("SG");
310 codes.insert("SH");
311 codes.insert("SI");
312 codes.insert("SJ");
313 codes.insert("SK");
314 codes.insert("SL");
315 codes.insert("SM");
316 codes.insert("SN");
317 codes.insert("SO");
318 codes.insert("SR");
319 codes.insert("SS");
320 codes.insert("ST");
321 codes.insert("SV");
322 codes.insert("SX");
323 codes.insert("SY");
324 codes.insert("SZ");
325 codes.insert("TC");
326 codes.insert("TD");
327 codes.insert("TF");
328 codes.insert("TG");
329 codes.insert("TH");
330 codes.insert("TJ");
331 codes.insert("TK");
332 codes.insert("TL");
333 codes.insert("TM");
334 codes.insert("TN");
335 codes.insert("TO");
336 codes.insert("TR");
337 codes.insert("TT");
338 codes.insert("TV");
339 codes.insert("TW");
340 codes.insert("TZ");
341 codes.insert("UA");
342 codes.insert("UG");
343 codes.insert("UM");
344 codes.insert("US");
345 codes.insert("UY");
346 codes.insert("UZ");
347 codes.insert("VA");
348 codes.insert("VC");
349 codes.insert("VE");
350 codes.insert("VG");
351 codes.insert("VI");
352 codes.insert("VN");
353 codes.insert("VU");
354 codes.insert("WF");
355 codes.insert("WS");
356 codes.insert("YE");
357 codes.insert("YT");
358 codes.insert("ZA");
359 codes.insert("ZM");
360 codes.insert("ZW");
361 Self { valid_codes: codes }
362 }
363
364 pub fn validate(&self, value: &str) -> bool {
366 let value_upper = value.to_uppercase();
367 COUNTRY_CODE_REGEX.is_match(&value_upper) && self.valid_codes.contains(value_upper.as_str())
368 }
369
370 #[must_use]
372 pub const fn error_message() -> &'static str {
373 "Invalid country code (must be ISO 3166-1 alpha-2)"
374 }
375}
376
377impl Default for CountryCodeValidator {
378 fn default() -> Self {
379 Self::new()
380 }
381}