Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::validation_phone_e164_data::{PhoneCountry, PHONE_COUNTRIES}; ← this capability’s own data, compiled from data/phone-countries.json into the same file by fune build
/// 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 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<String>| 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(&phone_e164(args[0].as_str(), args[1].as_str()))
}