use super::funejson::Value; use super::validation_phone_e164_data::{PhoneCountry, PHONE_COUNTRIES}; /// Characters people put between digits. Brackets matter because of the UK /// habit of writing "+44 (0)20 ...". const SEPARATORS: &str = " -.()"; fn invalid(reason: &str) -> PhoneE164 { PhoneE164 { valid: false, normalised: None, calling_code: None, national_number: None, reason: Some(reason.to_string()), } } /// Normalise a phone number to E.164 for a default country. /// /// This is a plausibility check over a small table of countries, not a /// numbering-plan database: it knows each country's calling code, trunk /// prefix, international dialling prefix, national number lengths and the /// digits a national number may start with. It does not know which ranges /// are allocated, or whether a number is a mobile. /// /// # Panics /// Panics if `default_country` is neither "" nor a supported country. pub fn validate_phone_e164(value: &str, default_country: &str) -> PhoneE164 { // ASCII-only upper-casing, so every language folds identically. let code = default_country.to_ascii_uppercase(); let mut home: Option<&PhoneCountry> = None; if !code.is_empty() { home = PHONE_COUNTRIES.iter().find(|row| row.country == code); // A bad default country is the caller's bug, not the user's typo, so // it panics rather than answering. if home.is_none() { panic!("unsupported default country \"{}\"", default_country); } } let mut digits = String::new(); let mut plus = false; for ch in value.chars() { if SEPARATORS.contains(ch) { continue; } if ch == '+' { // A plus is only meaningful as the very first thing typed. if plus || !digits.is_empty() { return invalid("bad-format"); } plus = true; continue; } if !ch.is_ascii_digit() { return invalid("bad-character"); } digits.push(ch); } if digits.is_empty() { return invalid(if plus { "bad-format" } else { "empty" }); } if let Some(h) = home { if !plus && digits.starts_with(h.international_prefix) { // Dialled with the home country's international prefix: 00 44 ... // from the UK, 011 44 ... from North America, 0011 44 ... from // Australia. digits = digits[h.international_prefix.len()..].to_string(); plus = true; } } let target: &PhoneCountry; let mut national: &str; if plus { let mut found: Option<&PhoneCountry> = None; // Calling codes are prefix-free, so the first match is the only one. for size in 1..=3usize { let head = &digits[..size.min(digits.len())]; if let Some(h) = home { if h.calling_code == head { found = Some(h); break; } } found = PHONE_COUNTRIES.iter().find(|row| row.calling_code == head); if found.is_some() { break; } } target = match found { Some(t) => t, None => return invalid("unknown-country-code"), }; national = &digits[target.calling_code.len()..]; } else { target = match home { Some(h) => h, None => return invalid("no-country"), }; national = &digits; } // The trunk prefix is dialled only inside the country ("0" in the UK, "1" // in North America). It is dropped once, in national form and in the // "+44 (0)20" form alike; no national number starts with it anyway. if !target.trunk_prefix.is_empty() && national.starts_with(target.trunk_prefix) { national = &national[target.trunk_prefix.len()..]; } let length = national.len() as i64; if length < target.min_length || length > target.max_length { return invalid("bad-length"); } if !target.leading_digits.contains(&national[0..1]) { return invalid("bad-prefix"); } PhoneE164 { valid: true, normalised: Some(format!("+{}{}", target.calling_code, national)), calling_code: Some(target.calling_code.to_string()), national_number: Some(national.to_string()), reason: None, } } /// Object keys are camelCase to match the shared vectors, and so that a /// capability building on this one can reuse the same shape. pub fn phone_e164_to_value(result: &PhoneE164) -> Value { let text = |field: &Option| match field { Some(s) => Value::str(s), None => Value::Null, }; Value::obj(vec![ ("valid", Value::Bool(result.valid)), ("normalised", text(&result.normalised)), ("callingCode", text(&result.calling_code)), ("nationalNumber", text(&result.national_number)), ("reason", text(&result.reason)), ]) } pub fn fune_vector(args: &[Value]) -> Value { phone_e164_to_value(&validate_phone_e164(args[0].as_str(), args[1].as_str())) }