use super::registry::{NamedPredicate, PredicateRegistry};
use memchr::memchr;
use serde_json::Value;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
pub fn register_contact_predicates(registry: &mut PredicateRegistry) {
let phone_number = Arc::new(PhoneNumberPredicate);
let phone_number_us = Arc::new(PhoneNumberUSPredicate);
registry.register(phone_number.clone());
registry.register(phone_number_us);
registry.register_as("is_phone", phone_number);
registry.register(Arc::new(IsEmailPredicate));
registry.register(Arc::new(IsUrlPredicate));
registry.register(Arc::new(IsUuidPredicate));
registry.register(Arc::new(IsIpPredicate));
registry.register(Arc::new(IsCidrPredicate));
registry.register(Arc::new(IsMacAddressPredicate));
registry.register(Arc::new(IsStreetAddressPredicate));
}
struct PhoneNumberUSPredicate;
impl NamedPredicate for PhoneNumberUSPredicate {
fn name(&self) -> &str {
"phone_number_us"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() || s.starts_with('+') {
return false;
}
validate_us_phone(s, args)
}
}
struct PhoneNumberPredicate;
impl NamedPredicate for PhoneNumberPredicate {
fn name(&self) -> &str {
"phone_number"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
if s.starts_with('+') {
return validate_international_phone(s);
}
validate_us_phone(s, args)
}
}
fn validate_us_phone(s: &str, args: &Value) -> bool {
let require_area_code = args
.get("require_area_code")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let mut digit_count = 0usize;
let mut d0 = b'0';
let mut d1 = b'0';
let mut d3 = b'0';
let mut d4 = b'0';
for &b in s.as_bytes() {
if !b.is_ascii_digit() {
continue;
}
match digit_count {
0 => d0 = b,
1 => d1 = b,
3 => d3 = b,
4 => d4 = b,
_ => {}
}
digit_count += 1;
if digit_count > 11 {
return false;
}
}
match digit_count {
10 => d0 != b'0' && d0 != b'1' && d3 != b'0' && d3 != b'1',
11 => d0 == b'1' && d1 != b'0' && d1 != b'1' && d4 != b'0' && d4 != b'1',
7 if !require_area_code => d0 != b'0' && d0 != b'1',
_ => false,
}
}
fn validate_international_phone(s: &str) -> bool {
let s = s.trim();
if !s.as_bytes().starts_with(b"+") {
return false;
}
let mut digits = 0usize;
for &b in s.as_bytes() {
if b.is_ascii_digit() {
digits += 1;
if digits > 15 {
return false;
}
}
}
digits >= 7
}
struct IsEmailPredicate;
impl NamedPredicate for IsEmailPredicate {
fn name(&self) -> &str {
"is_email"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
let bytes = s.as_bytes();
let Some(at) = memchr(b'@', bytes) else {
return false;
};
if memchr(b'@', &bytes[at + 1..]).is_some() {
return false;
};
let local = &bytes[..at];
let domain = &bytes[at + 1..];
if local.is_empty() || local.len() > 64 {
return false;
}
if domain.is_empty() || domain.len() > 253 {
return false;
}
if memchr(b'.', domain).is_none() {
return false;
}
if domain[0] == b'.' || domain[domain.len() - 1] == b'.' {
return false;
}
if domain[0] == b'-' || domain[domain.len() - 1] == b'-' {
return false;
}
let Some(last_dot) = domain.iter().rposition(|&b| b == b'.') else {
return false;
};
let tld = &domain[last_dot + 1..];
if tld.len() < 2 {
return false;
}
if !tld.iter().all(|b| b.is_ascii_alphabetic()) {
return false;
}
let valid_local = local.iter().all(|&b| {
b.is_ascii_alphanumeric()
|| b == b'.'
|| b == b'_'
|| b == b'%'
|| b == b'+'
|| b == b'-'
});
if !valid_local {
return false;
}
if local[0] == b'.' || local[local.len() - 1] == b'.' {
return false;
}
let valid_domain = domain
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-');
if !valid_domain {
return false;
}
let block_disposable = args
.get("block_disposable")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if block_disposable
&& DISPOSABLE_EMAIL_DOMAINS
.iter()
.any(|d| bytes_ends_with_ignore_ascii_case(domain, d.as_bytes()))
{
return false;
}
true
}
}
fn bytes_ends_with_ignore_ascii_case(bytes: &[u8], suffix: &[u8]) -> bool {
if bytes.len() < suffix.len() {
return false;
}
bytes[bytes.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
}
struct IsUrlPredicate;
impl NamedPredicate for IsUrlPredicate {
fn name(&self) -> &str {
"is_url"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
let require_https = args
.get("require_https")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let has_http = s.starts_with("http://");
let has_https = s.starts_with("https://");
if require_https && !has_https {
return false;
}
if !has_http && !has_https {
return false;
}
let rest = if has_https { &s[8..] } else { &s[7..] };
if rest.is_empty() {
return false;
}
let domain_part = rest.split('/').next().unwrap();
if domain_part.is_empty() {
return false;
}
let domain_without_port = domain_part.split(':').next().unwrap();
if domain_without_port.is_empty() {
return false;
}
domain_without_port
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
}
}
struct IsUuidPredicate;
impl NamedPredicate for IsUuidPredicate {
fn name(&self) -> &str {
"is_uuid"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim().to_lowercase(),
None => return false,
};
if s.len() != 36 {
return false;
}
let bytes = s.as_bytes();
if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
return false;
}
for (i, &b) in bytes.iter().enumerate() {
if i == 8 || i == 13 || i == 18 || i == 23 {
continue;
}
if !b.is_ascii_hexdigit() {
return false;
}
}
if let Some(version) = args.get("version").and_then(|v| v.as_u64()) {
let version_char = bytes[14];
let actual_version = if version_char.is_ascii_digit() {
(version_char - b'0') as u64
} else {
(version_char - b'a' + 10) as u64
};
if actual_version != version {
return false;
}
}
true
}
}
struct IsIpPredicate;
impl NamedPredicate for IsIpPredicate {
fn name(&self) -> &str {
"is_ip"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let version = args
.get("version")
.and_then(|v| v.as_str())
.map(|v| v.to_lowercase());
match version.as_deref() {
Some("v4") => s.parse::<Ipv4Addr>().is_ok(),
Some("v6") => s.parse::<Ipv6Addr>().is_ok(),
_ => s.parse::<IpAddr>().is_ok(),
}
}
}
struct IsCidrPredicate;
impl NamedPredicate for IsCidrPredicate {
fn name(&self) -> &str {
"is_cidr"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let (ip_part, prefix_part) = match s.split_once('/') {
Some(parts) => parts,
None => return false,
};
let prefix = match prefix_part.parse::<u8>() {
Ok(p) => p,
Err(_) => return false,
};
let version = args
.get("version")
.and_then(|v| v.as_str())
.map(|v| v.to_lowercase());
match version.as_deref() {
Some("v4") => ip_part.parse::<Ipv4Addr>().is_ok() && prefix <= 32,
Some("v6") => ip_part.parse::<Ipv6Addr>().is_ok() && prefix <= 128,
_ => {
if ip_part.parse::<Ipv4Addr>().is_ok() {
prefix <= 32
} else if ip_part.parse::<Ipv6Addr>().is_ok() {
prefix <= 128
} else {
false
}
}
}
}
}
struct IsMacAddressPredicate;
impl NamedPredicate for IsMacAddressPredicate {
fn name(&self) -> &str {
"is_mac_address"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.contains(':') || s.contains('-') {
let delim = if s.contains(':') { ':' } else { '-' };
let parts: Vec<&str> = s.split(delim).collect();
return parts.len() == 6
&& parts
.iter()
.all(|p| p.len() == 2 && p.chars().all(|c| c.is_ascii_hexdigit()));
}
if s.contains('.') {
let parts: Vec<&str> = s.split('.').collect();
return parts.len() == 3
&& parts
.iter()
.all(|p| p.len() == 4 && p.chars().all(|c| c.is_ascii_hexdigit()));
}
false
}
}
struct IsStreetAddressPredicate;
impl NamedPredicate for IsStreetAddressPredicate {
fn name(&self) -> &str {
"is_street_address"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let bytes = s.as_bytes();
if bytes.len() < 5 {
return false;
}
if !bytes[0].is_ascii_digit() {
return false;
}
let mut has_letter = false;
for &b in bytes {
if b.is_ascii_alphabetic() {
has_letter = true;
break;
}
}
has_letter
}
}
const DISPOSABLE_EMAIL_DOMAINS: &[&str] = &[
"mailinator.com",
"guerrillamail.com",
"10minutemail.com",
"tempmail.com",
"throwaway.email",
"yopmail.com",
"fakeinbox.com",
"trashmail.com",
];
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_phone_number_us() {
let pred = PhoneNumberUSPredicate;
assert!(pred.evaluate(&json!("(555) 234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("555-234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("555.234.5678"), &json!(null)));
assert!(pred.evaluate(&json!("5552345678"), &json!(null)));
assert!(pred.evaluate(&json!("1-555-234-5678"), &json!(null)));
assert!(!pred.evaluate(&json!("+1 555-234-5678"), &json!(null)));
assert!(!pred.evaluate(&json!("+62 34234233"), &json!(null)));
assert!(!pred.evaluate(&json!("055-234-5678"), &json!(null)));
assert!(!pred.evaluate(&json!("155-234-5678"), &json!(null)));
assert!(!pred.evaluate(&json!("555-023-4567"), &json!(null)));
assert!(!pred.evaluate(&json!("555-123-4567"), &json!(null)));
assert!(!pred.evaluate(&json!("123"), &json!(null)));
assert!(!pred.evaluate(&json!("234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("234-5678"), &json!({"require_area_code": false})));
}
#[test]
fn test_phone_number_general() {
let pred = PhoneNumberPredicate;
assert!(pred.evaluate(&json!("(555) 234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("555-234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("+1 555-234-5678"), &json!(null)));
assert!(pred.evaluate(&json!("+62 34234233"), &json!(null)));
assert!(pred.evaluate(&json!("+44 20 7946 0958"), &json!(null)));
assert!(!pred.evaluate(&json!("123"), &json!(null)));
assert!(!pred.evaluate(&json!("not-a-phone"), &json!(null)));
}
#[test]
fn test_is_phone_backwards_compat() {
let registry = PredicateRegistry::with_builtins();
assert!(registry.get("is_phone").is_some());
assert!(registry.get("phone_number").is_some());
assert!(registry.get("phone_number_us").is_some());
let is_phone = registry.get("is_phone").unwrap();
assert!(is_phone.evaluate(&json!("(555) 234-5678"), &json!(null)));
assert!(is_phone.evaluate(&json!("+1 555-234-5678"), &json!(null)));
assert!(is_phone.evaluate(&json!("+62 34234233"), &json!(null)));
}
#[test]
fn test_is_email() {
let pred = IsEmailPredicate;
assert!(pred.evaluate(&json!("user@example.com"), &json!(null)));
assert!(pred.evaluate(&json!("user.name@example.com"), &json!(null)));
assert!(pred.evaluate(&json!("user+tag@example.com"), &json!(null)));
assert!(pred.evaluate(&json!("user@sub.example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("user"), &json!(null)));
assert!(!pred.evaluate(&json!("user@"), &json!(null)));
assert!(!pred.evaluate(&json!("@example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("user@@example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("user@example"), &json!(null))); assert!(!pred.evaluate(&json!(".user@example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("user.@example.com"), &json!(null)));
assert!(!pred.evaluate(
&json!("test@mailinator.com"),
&json!({"block_disposable": true})
));
assert!(!pred.evaluate(
&json!("test@MAILINATOR.COM"),
&json!({"block_disposable": true})
));
assert!(pred.evaluate(
&json!("test@mailinator.com"),
&json!({"block_disposable": false})
));
}
#[test]
fn test_is_url() {
let pred = IsUrlPredicate;
assert!(pred.evaluate(&json!("http://example.com"), &json!(null)));
assert!(pred.evaluate(&json!("https://example.com"), &json!(null)));
assert!(pred.evaluate(&json!("https://example.com/path"), &json!(null)));
assert!(pred.evaluate(&json!("https://sub.example.com"), &json!(null)));
assert!(!pred.evaluate(
&json!("http://example.com"),
&json!({"require_https": true})
));
assert!(pred.evaluate(
&json!("https://example.com"),
&json!({"require_https": true})
));
assert!(!pred.evaluate(&json!("example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("ftp://example.com"), &json!(null)));
assert!(!pred.evaluate(&json!("http://"), &json!(null)));
}
#[test]
fn test_is_uuid() {
let pred = IsUuidPredicate;
assert!(pred.evaluate(&json!("550e8400-e29b-41d4-a716-446655440000"), &json!(null)));
assert!(pred.evaluate(&json!("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), &json!(null)));
assert!(pred.evaluate(&json!("f47ac10b-58cc-4372-a567-0e02b2c3d479"), &json!(null)));
assert!(pred.evaluate(&json!("550E8400-E29B-41D4-A716-446655440000"), &json!(null)));
assert!(pred.evaluate(
&json!("f47ac10b-58cc-4372-a567-0e02b2c3d479"),
&json!({"version": 4})
));
assert!(!pred.evaluate(
&json!("6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
&json!({"version": 4})
));
assert!(!pred.evaluate(&json!("550e8400-e29b-41d4-a716"), &json!(null)));
assert!(!pred.evaluate(&json!("550e8400e29b41d4a716446655440000"), &json!(null)));
assert!(!pred.evaluate(&json!("550e8400-e29b-41d4-a716-44665544000g"), &json!(null)));
assert!(!pred.evaluate(&json!(12345), &json!(null)));
}
#[test]
fn test_is_ip() {
let pred = IsIpPredicate;
assert!(pred.evaluate(&json!("192.168.1.1"), &json!(null)));
assert!(pred.evaluate(&json!("2001:db8::1"), &json!(null)));
assert!(!pred.evaluate(&json!("999.999.999.999"), &json!(null)));
assert!(pred.evaluate(&json!("10.0.0.1"), &json!({"version": "v4"})));
assert!(!pred.evaluate(&json!("2001:db8::1"), &json!({"version": "v4"})));
}
#[test]
fn test_is_cidr() {
let pred = IsCidrPredicate;
assert!(pred.evaluate(&json!("192.168.1.0/24"), &json!(null)));
assert!(pred.evaluate(&json!("2001:db8::/32"), &json!(null)));
assert!(!pred.evaluate(&json!("192.168.1.1"), &json!(null)));
assert!(!pred.evaluate(&json!("192.168.1.0/33"), &json!(null)));
}
#[test]
fn test_is_mac_address() {
let pred = IsMacAddressPredicate;
assert!(pred.evaluate(&json!("aa:bb:cc:dd:ee:ff"), &json!(null)));
assert!(pred.evaluate(&json!("AA-BB-CC-DD-EE-FF"), &json!(null)));
assert!(pred.evaluate(&json!("aabb.ccdd.eeff"), &json!(null)));
assert!(!pred.evaluate(&json!("aa:bb:cc:dd:ee"), &json!(null)));
assert!(!pred.evaluate(&json!("not-a-mac"), &json!(null)));
}
}