use std::sync::LazyLock;
use regex::Regex;
use crate::validation::patterns;
static EMAIL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(patterns::EMAIL).expect("email regex is valid"));
static PHONE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(patterns::PHONE_LENIENT).expect("phone regex is valid"));
static VIN_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[A-HJ-NPR-Z0-9]{17}$").expect("VIN regex is valid"));
static COUNTRY_CODE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[A-Z]{2}$").expect("country code regex is valid"));
pub struct EmailValidator;
impl EmailValidator {
pub fn validate(value: &str) -> bool {
!value.is_empty() && value.len() <= 254 && EMAIL_REGEX.is_match(value)
}
pub const fn error_message() -> &'static str {
"Invalid email format"
}
}
pub struct PhoneNumberValidator;
impl PhoneNumberValidator {
pub fn validate(value: &str) -> bool {
!value.is_empty() && value.len() <= 20 && PHONE_REGEX.is_match(value)
}
pub const fn error_message() -> &'static str {
"Invalid phone number format"
}
}
pub struct VinValidator;
impl VinValidator {
pub fn validate(value: &str) -> bool {
if value.len() != 17 {
return false;
}
let value_upper = value.to_uppercase();
VIN_REGEX.is_match(&value_upper)
}
pub const fn error_message() -> &'static str {
"Invalid VIN format (must be 17 alphanumeric characters, excluding I, O, Q)"
}
}
pub struct CountryCodeValidator {
valid_codes: std::collections::HashSet<&'static str>,
}
impl CountryCodeValidator {
pub fn new() -> Self {
let mut codes = std::collections::HashSet::new();
codes.insert("AD");
codes.insert("AE");
codes.insert("AF");
codes.insert("AG");
codes.insert("AI");
codes.insert("AL");
codes.insert("AM");
codes.insert("AO");
codes.insert("AQ");
codes.insert("AR");
codes.insert("AS");
codes.insert("AT");
codes.insert("AU");
codes.insert("AW");
codes.insert("AX");
codes.insert("AZ");
codes.insert("BA");
codes.insert("BB");
codes.insert("BD");
codes.insert("BE");
codes.insert("BF");
codes.insert("BG");
codes.insert("BH");
codes.insert("BI");
codes.insert("BJ");
codes.insert("BL");
codes.insert("BM");
codes.insert("BN");
codes.insert("BO");
codes.insert("BQ");
codes.insert("BR");
codes.insert("BS");
codes.insert("BT");
codes.insert("BV");
codes.insert("BW");
codes.insert("BY");
codes.insert("BZ");
codes.insert("CA");
codes.insert("CC");
codes.insert("CD");
codes.insert("CF");
codes.insert("CG");
codes.insert("CH");
codes.insert("CI");
codes.insert("CK");
codes.insert("CL");
codes.insert("CM");
codes.insert("CN");
codes.insert("CO");
codes.insert("CR");
codes.insert("CU");
codes.insert("CV");
codes.insert("CW");
codes.insert("CX");
codes.insert("CY");
codes.insert("CZ");
codes.insert("DE");
codes.insert("DJ");
codes.insert("DK");
codes.insert("DM");
codes.insert("DO");
codes.insert("DZ");
codes.insert("EC");
codes.insert("EE");
codes.insert("EG");
codes.insert("EH");
codes.insert("ER");
codes.insert("ES");
codes.insert("ET");
codes.insert("FI");
codes.insert("FJ");
codes.insert("FK");
codes.insert("FM");
codes.insert("FO");
codes.insert("FR");
codes.insert("GA");
codes.insert("GB");
codes.insert("GD");
codes.insert("GE");
codes.insert("GF");
codes.insert("GG");
codes.insert("GH");
codes.insert("GI");
codes.insert("GL");
codes.insert("GM");
codes.insert("GN");
codes.insert("GP");
codes.insert("GQ");
codes.insert("GR");
codes.insert("GS");
codes.insert("GT");
codes.insert("GU");
codes.insert("GW");
codes.insert("GY");
codes.insert("HK");
codes.insert("HM");
codes.insert("HN");
codes.insert("HR");
codes.insert("HT");
codes.insert("HU");
codes.insert("ID");
codes.insert("IE");
codes.insert("IL");
codes.insert("IM");
codes.insert("IN");
codes.insert("IO");
codes.insert("IQ");
codes.insert("IR");
codes.insert("IS");
codes.insert("IT");
codes.insert("JE");
codes.insert("JM");
codes.insert("JO");
codes.insert("JP");
codes.insert("KE");
codes.insert("KG");
codes.insert("KH");
codes.insert("KI");
codes.insert("KM");
codes.insert("KN");
codes.insert("KP");
codes.insert("KR");
codes.insert("KW");
codes.insert("KY");
codes.insert("KZ");
codes.insert("LA");
codes.insert("LB");
codes.insert("LC");
codes.insert("LI");
codes.insert("LK");
codes.insert("LR");
codes.insert("LS");
codes.insert("LT");
codes.insert("LU");
codes.insert("LV");
codes.insert("LY");
codes.insert("MA");
codes.insert("MC");
codes.insert("MD");
codes.insert("ME");
codes.insert("MF");
codes.insert("MG");
codes.insert("MH");
codes.insert("MK");
codes.insert("ML");
codes.insert("MM");
codes.insert("MN");
codes.insert("MO");
codes.insert("MP");
codes.insert("MQ");
codes.insert("MR");
codes.insert("MS");
codes.insert("MT");
codes.insert("MU");
codes.insert("MV");
codes.insert("MW");
codes.insert("MX");
codes.insert("MY");
codes.insert("MZ");
codes.insert("NA");
codes.insert("NC");
codes.insert("NE");
codes.insert("NF");
codes.insert("NG");
codes.insert("NI");
codes.insert("NL");
codes.insert("NO");
codes.insert("NP");
codes.insert("NR");
codes.insert("NU");
codes.insert("NZ");
codes.insert("OM");
codes.insert("PA");
codes.insert("PE");
codes.insert("PF");
codes.insert("PG");
codes.insert("PH");
codes.insert("PK");
codes.insert("PL");
codes.insert("PM");
codes.insert("PN");
codes.insert("PR");
codes.insert("PS");
codes.insert("PT");
codes.insert("PW");
codes.insert("PY");
codes.insert("QA");
codes.insert("RE");
codes.insert("RO");
codes.insert("RS");
codes.insert("RU");
codes.insert("RW");
codes.insert("SA");
codes.insert("SB");
codes.insert("SC");
codes.insert("SD");
codes.insert("SE");
codes.insert("SG");
codes.insert("SH");
codes.insert("SI");
codes.insert("SJ");
codes.insert("SK");
codes.insert("SL");
codes.insert("SM");
codes.insert("SN");
codes.insert("SO");
codes.insert("SR");
codes.insert("SS");
codes.insert("ST");
codes.insert("SV");
codes.insert("SX");
codes.insert("SY");
codes.insert("SZ");
codes.insert("TC");
codes.insert("TD");
codes.insert("TF");
codes.insert("TG");
codes.insert("TH");
codes.insert("TJ");
codes.insert("TK");
codes.insert("TL");
codes.insert("TM");
codes.insert("TN");
codes.insert("TO");
codes.insert("TR");
codes.insert("TT");
codes.insert("TV");
codes.insert("TW");
codes.insert("TZ");
codes.insert("UA");
codes.insert("UG");
codes.insert("UM");
codes.insert("US");
codes.insert("UY");
codes.insert("UZ");
codes.insert("VA");
codes.insert("VC");
codes.insert("VE");
codes.insert("VG");
codes.insert("VI");
codes.insert("VN");
codes.insert("VU");
codes.insert("WF");
codes.insert("WS");
codes.insert("YE");
codes.insert("YT");
codes.insert("ZA");
codes.insert("ZM");
codes.insert("ZW");
Self { valid_codes: codes }
}
pub fn validate(&self, value: &str) -> bool {
let value_upper = value.to_uppercase();
COUNTRY_CODE_REGEX.is_match(&value_upper) && self.valid_codes.contains(value_upper.as_str())
}
pub const fn error_message() -> &'static str {
"Invalid country code (must be ISO 3166-1 alpha-2)"
}
}
impl Default for CountryCodeValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_email_valid() {
assert!(EmailValidator::validate("user@example.com"));
assert!(EmailValidator::validate("john.doe@company.co.uk"));
}
#[test]
fn test_email_invalid() {
assert!(!EmailValidator::validate("invalid.email"));
assert!(!EmailValidator::validate("user@"));
assert!(!EmailValidator::validate("@example.com"));
assert!(!EmailValidator::validate("user@localhost"));
assert!(!EmailValidator::validate("user@example"));
}
#[test]
fn test_email_empty() {
assert!(!EmailValidator::validate(""));
}
#[test]
fn test_phone_valid_plus_format() {
assert!(PhoneNumberValidator::validate("+1234567890"));
assert!(PhoneNumberValidator::validate("+33612345678"));
}
#[test]
fn test_phone_valid_no_plus() {
assert!(PhoneNumberValidator::validate("1234567890"));
}
#[test]
fn test_phone_invalid() {
assert!(!PhoneNumberValidator::validate("+0123456789")); assert!(!PhoneNumberValidator::validate(""));
}
#[test]
fn test_vin_valid() {
assert!(VinValidator::validate("3G1FB1E30D1109186"));
assert!(VinValidator::validate("JH2RC5004LM200591"));
}
#[test]
fn test_vin_valid_lowercase() {
assert!(VinValidator::validate("3g1fb1e30d1109186"));
}
#[test]
fn test_vin_invalid_length() {
assert!(!VinValidator::validate("3G1FB1E30D110918"));
assert!(!VinValidator::validate("3G1FB1E30D11091861"));
}
#[test]
fn test_vin_invalid_chars() {
assert!(!VinValidator::validate("3G1FB1E30D110918I")); assert!(!VinValidator::validate("3G1FB1E30D110918O")); assert!(!VinValidator::validate("3G1FB1E30D110918Q")); }
#[test]
fn test_vin_empty_rejected_by_length_guard() {
assert!(!VinValidator::validate(""), "empty string rejected before regex");
}
#[test]
fn test_vin_16_chars_rejected_by_length_guard() {
assert!(!VinValidator::validate("3G1FB1E30D110918"), "16-char VIN rejected");
}
#[test]
fn test_vin_18_chars_rejected_by_length_guard() {
assert!(!VinValidator::validate("3G1FB1E30D11091862"), "18-char VIN rejected");
}
#[test]
fn test_vin_very_long_string_rejected_by_length_guard() {
let long_input = "A".repeat(100);
assert!(!VinValidator::validate(&long_input), "100-char string rejected");
}
#[test]
fn test_country_code_valid() {
let validator = CountryCodeValidator::new();
assert!(validator.validate("US"));
assert!(validator.validate("GB"));
assert!(validator.validate("DE"));
assert!(validator.validate("FR"));
}
#[test]
fn test_country_code_lowercase() {
let validator = CountryCodeValidator::new();
assert!(validator.validate("us"));
assert!(validator.validate("gb"));
}
#[test]
fn test_country_code_invalid() {
let validator = CountryCodeValidator::new();
assert!(!validator.validate("XX"));
assert!(!validator.validate("USA"));
assert!(!validator.validate("U"));
}
}