Skip to main content

wellformed_validate/
registry.rs

1//! Static predicate registry using LazyLock.
2//!
3//! This module provides a zero-allocation predicate registry that is
4//! initialized once at startup and shared across all validation calls.
5//!
6//! ## Design
7//!
8//! Instead of creating a new `HashMap` for every validation call, we use
9//! `LazyLock` to initialize the registry once. Predicates are stored in
10//! a perfect hash table for O(1) lookup with minimal memory overhead.
11//!
12//! ## Usage
13//!
14//! ```rust
15//! use wellformed_validate::registry::{REGISTRY, Predicate};
16//! use serde_json::json;
17//!
18//! // Get a predicate by name
19//! if let Some(pred) = REGISTRY.get("is_ssn") {
20//!     let valid = pred.evaluate(&json!("123-45-6789"), &json!(null));
21//!     assert!(valid);
22//! }
23//! ```
24
25use serde_json::Value;
26use std::sync::LazyLock;
27
28use crate::tin::{validate_any, validate_atin, validate_ein, validate_itin, validate_ssn};
29
30// ============================================================================
31// Predicate Trait
32// ============================================================================
33
34/// A named predicate function.
35pub trait Predicate: Send + Sync {
36    /// The name of this predicate.
37    fn name(&self) -> &'static str;
38
39    /// Evaluate the predicate against a value.
40    fn evaluate(&self, value: &Value, args: &Value) -> bool;
41}
42
43// ============================================================================
44// Built-in Predicates
45// ============================================================================
46
47/// SSN validation predicate.
48struct 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
60/// EIN validation predicate.
61struct 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
73/// ITIN validation predicate.
74struct 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
86/// ATIN validation predicate.
87struct 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
99/// Any TIN validation predicate.
100struct 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        // Check for specific kind if provided
114        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
126/// Non-negative number predicate.
127struct 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
139/// Positive number predicate.
140struct 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
152/// Tax year validation predicate.
153struct 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
171/// US state code predicate.
172struct 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", // Territories
186        ];
187
188        value
189            .as_str()
190            .map(|s| US_STATES.contains(&s.to_uppercase().as_str()))
191            .unwrap_or(false)
192    }
193}
194
195/// US ZIP code predicate.
196struct 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        // 5 digits or 5+4 format
210        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
223/// Email validation predicate (basic check).
224struct 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        // Basic email check: contains @ with text before and after
238        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
249/// Phone number validation predicate.
250struct 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        // Count digits
264        let digit_count = s.chars().filter(|c| c.is_ascii_digit()).count();
265
266        // US phone: 10 digits, international: 7-15 digits
267        (7..=15).contains(&digit_count)
268    }
269}
270
271/// Luhn checksum validation predicate.
272struct 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
317// ============================================================================
318// Registry
319// ============================================================================
320
321/// Static array of all built-in predicates.
322static 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
338/// Predicate registry using a simple HashMap for reliable lookups.
339/// The LazyLock ensures this is only created once at startup.
340pub struct PredicateRegistry {
341    /// Predicates stored in a HashMap for reliable O(1) lookup.
342    predicates: std::collections::HashMap<&'static str, &'static dyn Predicate>,
343}
344
345impl PredicateRegistry {
346    /// Create a new registry with all built-in predicates.
347    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    /// Get a predicate by name.
358    #[inline]
359    pub fn get(&self, name: &str) -> Option<&'static dyn Predicate> {
360        self.predicates.get(name).copied()
361    }
362
363    /// Check if a predicate exists.
364    #[inline]
365    pub fn contains(&self, name: &str) -> bool {
366        self.predicates.contains_key(name)
367    }
368
369    /// Iterate over all predicates.
370    pub fn iter(&self) -> impl Iterator<Item = &'static dyn Predicate> + '_ {
371        self.predicates.values().copied()
372    }
373}
374
375/// Global predicate registry, initialized once at startup.
376pub static REGISTRY: LazyLock<PredicateRegistry> = LazyLock::new(PredicateRegistry::new);
377
378// ============================================================================
379// Convenience Functions
380// ============================================================================
381
382/// Evaluate a named predicate.
383#[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/// Check if a named predicate exists.
389#[inline]
390pub fn exists(name: &str) -> bool {
391    REGISTRY.contains(name)
392}
393
394// ============================================================================
395// Tests
396// ============================================================================
397
398#[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        // SSN
424        assert!(pred.evaluate(&json!("123-45-6789"), &json!({"kind": "SSN"})));
425
426        // EIN
427        assert!(pred.evaluate(&json!("12-3456789"), &json!({"kind": "EIN"})));
428
429        // Any
430        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))); // Case insensitive
448        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        // Valid Luhn numbers
474        assert!(pred.evaluate(&json!("79927398713"), &json!(null)));
475        assert!(pred.evaluate(&json!("4532015112830366"), &json!(null)));
476        // Invalid
477        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}