1use serde_json::Value;
26use std::sync::LazyLock;
27
28use crate::tin::{validate_any, validate_atin, validate_ein, validate_itin, validate_ssn};
29
30pub trait Predicate: Send + Sync {
36 fn name(&self) -> &'static str;
38
39 fn evaluate(&self, value: &Value, args: &Value) -> bool;
41}
42
43struct IsSsnPredicate;
49
50impl Predicate for IsSsnPredicate {
51 fn name(&self) -> &'static str {
52 "is_ssn"
53 }
54
55 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
56 value.as_str().map(validate_ssn).unwrap_or(false)
57 }
58}
59
60struct IsEinPredicate;
62
63impl Predicate for IsEinPredicate {
64 fn name(&self) -> &'static str {
65 "is_ein"
66 }
67
68 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
69 value.as_str().map(validate_ein).unwrap_or(false)
70 }
71}
72
73struct IsItinPredicate;
75
76impl Predicate for IsItinPredicate {
77 fn name(&self) -> &'static str {
78 "is_itin"
79 }
80
81 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
82 value.as_str().map(validate_itin).unwrap_or(false)
83 }
84}
85
86struct IsAtinPredicate;
88
89impl Predicate for IsAtinPredicate {
90 fn name(&self) -> &'static str {
91 "is_atin"
92 }
93
94 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
95 value.as_str().map(validate_atin).unwrap_or(false)
96 }
97}
98
99struct IsTinPredicate;
101
102impl Predicate for IsTinPredicate {
103 fn name(&self) -> &'static str {
104 "is_tin"
105 }
106
107 fn evaluate(&self, value: &Value, args: &Value) -> bool {
108 let s = match value.as_str() {
109 Some(s) => s,
110 None => return false,
111 };
112
113 let kind = args.get("kind").and_then(|v| v.as_str()).unwrap_or("ANY");
115
116 match kind {
117 "SSN" => validate_ssn(s),
118 "EIN" => validate_ein(s),
119 "ITIN" => validate_itin(s),
120 "ATIN" => validate_atin(s),
121 _ => validate_any(s),
122 }
123 }
124}
125
126struct IsNonNegativePredicate;
128
129impl Predicate for IsNonNegativePredicate {
130 fn name(&self) -> &'static str {
131 "is_non_negative"
132 }
133
134 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
135 value.as_f64().map(|n| n >= 0.0).unwrap_or(false)
136 }
137}
138
139struct IsPositivePredicate;
141
142impl Predicate for IsPositivePredicate {
143 fn name(&self) -> &'static str {
144 "is_positive"
145 }
146
147 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
148 value.as_f64().map(|n| n > 0.0).unwrap_or(false)
149 }
150}
151
152struct IsTaxYearPredicate;
154
155impl Predicate for IsTaxYearPredicate {
156 fn name(&self) -> &'static str {
157 "is_tax_year"
158 }
159
160 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
161 let year = match value {
162 Value::Number(n) => n.as_u64(),
163 Value::String(s) => s.parse::<u64>().ok(),
164 _ => None,
165 };
166
167 year.map(|y| (2020..=2100).contains(&y)).unwrap_or(false)
168 }
169}
170
171struct IsUsStatePredicate;
173
174impl Predicate for IsUsStatePredicate {
175 fn name(&self) -> &'static str {
176 "is_us_state"
177 }
178
179 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
180 static US_STATES: &[&str] = &[
181 "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN",
182 "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV",
183 "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN",
184 "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY", "DC", "PR", "VI", "GU", "AS",
185 "MP", ];
187
188 value
189 .as_str()
190 .map(|s| US_STATES.contains(&s.to_uppercase().as_str()))
191 .unwrap_or(false)
192 }
193}
194
195struct IsUsZipPredicate;
197
198impl Predicate for IsUsZipPredicate {
199 fn name(&self) -> &'static str {
200 "is_us_zip"
201 }
202
203 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
204 let s = match value.as_str() {
205 Some(s) => s,
206 None => return false,
207 };
208
209 let bytes = s.as_bytes();
211
212 if bytes.len() == 5 {
213 bytes.iter().all(|b| b.is_ascii_digit())
214 } else if bytes.len() == 10 && bytes[5] == b'-' {
215 bytes[..5].iter().all(|b| b.is_ascii_digit())
216 && bytes[6..].iter().all(|b| b.is_ascii_digit())
217 } else {
218 false
219 }
220 }
221}
222
223struct IsEmailPredicate;
225
226impl Predicate for IsEmailPredicate {
227 fn name(&self) -> &'static str {
228 "is_email"
229 }
230
231 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
232 let s = match value.as_str() {
233 Some(s) => s,
234 None => return false,
235 };
236
237 let at_pos = s.find('@');
239 match at_pos {
240 Some(pos) if pos > 0 && pos < s.len() - 1 => {
241 let domain = &s[pos + 1..];
242 domain.contains('.') && !domain.starts_with('.') && !domain.ends_with('.')
243 }
244 _ => false,
245 }
246 }
247}
248
249struct IsPhonePredicate;
251
252impl Predicate for IsPhonePredicate {
253 fn name(&self) -> &'static str {
254 "is_phone"
255 }
256
257 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
258 let s = match value.as_str() {
259 Some(s) => s,
260 None => return false,
261 };
262
263 let digit_count = s.chars().filter(|c| c.is_ascii_digit()).count();
265
266 (7..=15).contains(&digit_count)
268 }
269}
270
271struct LuhnPredicate;
273
274impl Predicate for LuhnPredicate {
275 fn name(&self) -> &'static str {
276 "luhn"
277 }
278
279 fn evaluate(&self, value: &Value, _args: &Value) -> bool {
280 let s = match value.as_str() {
281 Some(s) => s,
282 None => return false,
283 };
284
285 let digits: Vec<u32> = s
286 .chars()
287 .filter(|c| c.is_ascii_digit())
288 .filter_map(|c| c.to_digit(10))
289 .collect();
290
291 if digits.is_empty() {
292 return false;
293 }
294
295 let sum: u32 = digits
296 .iter()
297 .rev()
298 .enumerate()
299 .map(|(i, &d)| {
300 if i % 2 == 1 {
301 let doubled = d * 2;
302 if doubled > 9 {
303 doubled - 9
304 } else {
305 doubled
306 }
307 } else {
308 d
309 }
310 })
311 .sum();
312
313 sum % 10 == 0
314 }
315}
316
317static PREDICATES: &[&dyn Predicate] = &[
323 &IsSsnPredicate,
324 &IsEinPredicate,
325 &IsItinPredicate,
326 &IsAtinPredicate,
327 &IsTinPredicate,
328 &IsNonNegativePredicate,
329 &IsPositivePredicate,
330 &IsTaxYearPredicate,
331 &IsUsStatePredicate,
332 &IsUsZipPredicate,
333 &IsEmailPredicate,
334 &IsPhonePredicate,
335 &LuhnPredicate,
336];
337
338pub struct PredicateRegistry {
341 predicates: std::collections::HashMap<&'static str, &'static dyn Predicate>,
343}
344
345impl PredicateRegistry {
346 fn new() -> Self {
348 let mut predicates = std::collections::HashMap::with_capacity(PREDICATES.len());
349
350 for pred in PREDICATES {
351 predicates.insert(pred.name(), *pred);
352 }
353
354 Self { predicates }
355 }
356
357 #[inline]
359 pub fn get(&self, name: &str) -> Option<&'static dyn Predicate> {
360 self.predicates.get(name).copied()
361 }
362
363 #[inline]
365 pub fn contains(&self, name: &str) -> bool {
366 self.predicates.contains_key(name)
367 }
368
369 pub fn iter(&self) -> impl Iterator<Item = &'static dyn Predicate> + '_ {
371 self.predicates.values().copied()
372 }
373}
374
375pub static REGISTRY: LazyLock<PredicateRegistry> = LazyLock::new(PredicateRegistry::new);
377
378#[inline]
384pub fn evaluate(name: &str, value: &Value, args: &Value) -> Option<bool> {
385 REGISTRY.get(name).map(|p| p.evaluate(value, args))
386}
387
388#[inline]
390pub fn exists(name: &str) -> bool {
391 REGISTRY.contains(name)
392}
393
394#[cfg(test)]
399mod tests {
400 use super::*;
401 use serde_json::json;
402
403 #[test]
404 fn test_registry_lookup() {
405 assert!(REGISTRY.contains("is_ssn"));
406 assert!(REGISTRY.contains("is_ein"));
407 assert!(REGISTRY.contains("is_tin"));
408 assert!(REGISTRY.contains("luhn"));
409 assert!(!REGISTRY.contains("nonexistent"));
410 }
411
412 #[test]
413 fn test_is_ssn() {
414 let pred = REGISTRY.get("is_ssn").unwrap();
415 assert!(pred.evaluate(&json!("123-45-6789"), &json!(null)));
416 assert!(!pred.evaluate(&json!("000-00-0000"), &json!(null)));
417 }
418
419 #[test]
420 fn test_is_tin() {
421 let pred = REGISTRY.get("is_tin").unwrap();
422
423 assert!(pred.evaluate(&json!("123-45-6789"), &json!({"kind": "SSN"})));
425
426 assert!(pred.evaluate(&json!("12-3456789"), &json!({"kind": "EIN"})));
428
429 assert!(pred.evaluate(&json!("123456789"), &json!({"kind": "ANY"})));
431 }
432
433 #[test]
434 fn test_is_tax_year() {
435 let pred = REGISTRY.get("is_tax_year").unwrap();
436 assert!(pred.evaluate(&json!(2024), &json!(null)));
437 assert!(pred.evaluate(&json!("2024"), &json!(null)));
438 assert!(!pred.evaluate(&json!(1999), &json!(null)));
439 assert!(!pred.evaluate(&json!(2200), &json!(null)));
440 }
441
442 #[test]
443 fn test_is_us_state() {
444 let pred = REGISTRY.get("is_us_state").unwrap();
445 assert!(pred.evaluate(&json!("CA"), &json!(null)));
446 assert!(pred.evaluate(&json!("NY"), &json!(null)));
447 assert!(pred.evaluate(&json!("ca"), &json!(null))); assert!(!pred.evaluate(&json!("XX"), &json!(null)));
449 }
450
451 #[test]
452 fn test_is_us_zip() {
453 let pred = REGISTRY.get("is_us_zip").unwrap();
454 assert!(pred.evaluate(&json!("12345"), &json!(null)));
455 assert!(pred.evaluate(&json!("12345-6789"), &json!(null)));
456 assert!(!pred.evaluate(&json!("1234"), &json!(null)));
457 assert!(!pred.evaluate(&json!("123456"), &json!(null)));
458 }
459
460 #[test]
461 fn test_is_email() {
462 let pred = REGISTRY.get("is_email").unwrap();
463 assert!(pred.evaluate(&json!("test@example.com"), &json!(null)));
464 assert!(pred.evaluate(&json!("a@b.c"), &json!(null)));
465 assert!(!pred.evaluate(&json!("invalid"), &json!(null)));
466 assert!(!pred.evaluate(&json!("@example.com"), &json!(null)));
467 assert!(!pred.evaluate(&json!("test@"), &json!(null)));
468 }
469
470 #[test]
471 fn test_luhn() {
472 let pred = REGISTRY.get("luhn").unwrap();
473 assert!(pred.evaluate(&json!("79927398713"), &json!(null)));
475 assert!(pred.evaluate(&json!("4532015112830366"), &json!(null)));
476 assert!(!pred.evaluate(&json!("79927398710"), &json!(null)));
478 }
479
480 #[test]
481 fn test_evaluate_function() {
482 assert_eq!(
483 evaluate("is_ssn", &json!("123-45-6789"), &json!(null)),
484 Some(true)
485 );
486 assert_eq!(evaluate("nonexistent", &json!("test"), &json!(null)), None);
487 }
488}