use super::registry::{NamedPredicate, PredicateRegistry};
use postal::{Context, InitOptions, ParseAddressOptions};
use serde_json::Value;
use std::sync::{Arc, LazyLock, Mutex};
static POSTAL_CONTEXT: LazyLock<Result<Mutex<Context>, String>> = LazyLock::new(|| {
let mut context = Context::new();
context
.init(InitOptions {
expand_address: false,
parse_address: true,
})
.map(|()| Mutex::new(context))
.map_err(|err| err.to_string())
});
pub fn register_address_predicates(registry: &mut PredicateRegistry) {
registry.register(Arc::new(IsParseableAddressPredicate));
registry.register(Arc::new(HasAddressComponentPredicate));
registry.register(Arc::new(IsUsAddressPredicate));
registry.register(Arc::new(AddressComponentMatchesPredicate));
registry.register(Arc::new(IsUsZipPredicate));
registry.register(Arc::new(IsUsStatePredicate));
}
pub struct IsParseableAddressPredicate;
impl NamedPredicate for IsParseableAddressPredicate {
fn name(&self) -> &str {
"is_parseable_address"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s,
None => return false,
};
if s.trim().is_empty() {
return false;
}
parse_address_components(s, args).is_some_and(|parsed| !parsed.is_empty())
}
}
pub struct HasAddressComponentPredicate;
impl NamedPredicate for HasAddressComponentPredicate {
fn name(&self) -> &str {
"has_address_component"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s,
None => return false,
};
let component = match args.get("component").and_then(|v| v.as_str()) {
Some(c) => c,
None => return false,
};
parse_address_components(s, args)
.is_some_and(|parsed| component_value(&parsed, component).is_some())
}
}
pub struct IsUsAddressPredicate;
impl NamedPredicate for IsUsAddressPredicate {
fn name(&self) -> &str {
"is_us_address"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s,
None => return false,
};
match parse_address_components(s, args) {
Some(parsed) => {
let has_road = component_value(&parsed, "road").is_some();
let has_city = component_value(&parsed, "city").is_some();
let has_state = component_value(&parsed, "state").is_some();
let has_postcode = component_value(&parsed, "postcode").is_some();
let strict = args.get("strict").and_then(|v| v.as_bool()).unwrap_or(true);
if strict {
has_road && has_city && has_state && has_postcode
} else {
has_city && has_state
}
}
None => false,
}
}
}
pub struct AddressComponentMatchesPredicate;
impl NamedPredicate for AddressComponentMatchesPredicate {
fn name(&self) -> &str {
"address_component_matches"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s,
None => return false,
};
let component = match args.get("component").and_then(|v| v.as_str()) {
Some(c) => c,
None => return false,
};
let pattern = match args.get("pattern").and_then(|v| v.as_str()) {
Some(p) => p,
None => return false,
};
let regex = match regex::Regex::new(pattern) {
Ok(r) => r,
Err(_) => return false,
};
parse_address_components(s, args).is_some_and(|parsed| {
component_value(&parsed, component).is_some_and(|value| regex.is_match(value))
})
}
}
pub struct IsUsZipPredicate;
impl NamedPredicate for IsUsZipPredicate {
fn name(&self) -> &str {
"is_us_zip"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s,
None => return false,
};
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 5 && digits.len() != 9 {
return false;
}
if digits.chars().all(|c| c == '0') {
return false;
}
true
}
}
pub struct IsUsStatePredicate;
impl NamedPredicate for IsUsStatePredicate {
fn name(&self) -> &str {
"is_us_state"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim().to_uppercase(),
None => return false,
};
US_STATE_CODES.contains(&s.as_str())
}
}
type AddressComponents = Vec<(String, String)>;
fn parse_address_components(address: &str, _args: &Value) -> Option<AddressComponents> {
let context = POSTAL_CONTEXT.as_ref().ok()?;
let context = context.lock().ok()?;
let mut options = ParseAddressOptions::new();
let components = context.parse_address(address, &mut options).ok()?;
Some(
components
.map(|component| (component.label.to_string(), component.value.to_string()))
.collect(),
)
}
fn component_value<'a>(components: &'a AddressComponents, component: &str) -> Option<&'a str> {
components
.iter()
.find(|(label, _)| label == component)
.map(|(_, value)| value.as_str())
}
const US_STATE_CODES: &[&str] = &[
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS",
"KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY",
"NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV",
"WI", "WY", "DC", "AS", "GU", "MP", "PR", "VI", "UM", "AA", "AE", "AP",
];
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use serial_test::serial;
#[test]
#[serial]
fn test_is_parseable_address() {
let pred = IsParseableAddressPredicate;
assert!(pred.evaluate(&json!("123 Main St, San Francisco, CA 94102"), &json!(null)));
assert!(!pred.evaluate(&json!(""), &json!(null)));
assert!(!pred.evaluate(&json!(123), &json!(null)));
}
#[test]
#[serial]
fn test_has_address_component() {
let pred = HasAddressComponentPredicate;
assert!(pred.evaluate(
&json!("123 Main St, San Francisco, CA 94102"),
&json!({"component": "road"})
));
assert!(pred.evaluate(
&json!("123 Main St, San Francisco, CA 94102"),
&json!({"component": "postcode"})
));
assert!(!pred.evaluate(
&json!("123 Main St"),
&json!({"component": "invalid_component_xyz"})
));
}
#[test]
#[serial]
fn test_is_us_address() {
let pred = IsUsAddressPredicate;
assert!(pred.evaluate(
&json!("123 Main St, San Francisco, CA 94102"),
&json!({"strict": true})
));
assert!(pred.evaluate(&json!("San Francisco, CA"), &json!({"strict": false})));
}
#[test]
#[serial]
fn test_address_component_matches() {
let pred = AddressComponentMatchesPredicate;
assert!(pred.evaluate(
&json!("123 Main St, San Francisco, CA 94102"),
&json!({"component": "postcode", "pattern": r"^\d{5}$"})
));
assert!(!pred.evaluate(
&json!("123 Main St, San Francisco, CA 94102"),
&json!({"component": "postcode", "pattern": r"^XXXXX$"})
));
}
#[test]
fn test_is_us_zip() {
let pred = IsUsZipPredicate;
assert!(pred.evaluate(&json!("94102"), &json!(null)));
assert!(pred.evaluate(&json!("94102-1234"), &json!(null)));
assert!(pred.evaluate(&json!("941021234"), &json!(null)));
assert!(!pred.evaluate(&json!("00000"), &json!(null)));
assert!(!pred.evaluate(&json!("9410"), &json!(null)));
assert!(!pred.evaluate(&json!("9410212345"), &json!(null)));
assert!(!pred.evaluate(&json!(94102), &json!(null)));
}
#[test]
fn test_is_us_state() {
let pred = IsUsStatePredicate;
assert!(pred.evaluate(&json!("CA"), &json!(null)));
assert!(pred.evaluate(&json!("NY"), &json!(null)));
assert!(pred.evaluate(&json!("TX"), &json!(null)));
assert!(pred.evaluate(&json!("ca"), &json!(null)));
assert!(pred.evaluate(&json!("DC"), &json!(null)));
assert!(pred.evaluate(&json!("PR"), &json!(null)));
assert!(pred.evaluate(&json!("GU"), &json!(null)));
assert!(pred.evaluate(&json!("AA"), &json!(null)));
assert!(pred.evaluate(&json!("AE"), &json!(null)));
assert!(pred.evaluate(&json!("AP"), &json!(null)));
assert!(!pred.evaluate(&json!("XX"), &json!(null)));
assert!(!pred.evaluate(&json!("California"), &json!(null)));
}
}