use phonenumber::{country, Mode, ParseError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Normalization {
E164(String),
InvalidNumber,
MissingRegion,
InvalidCountryCode,
TooShort,
TooLong,
NotANumber,
}
#[must_use]
pub fn normalize_number(raw: &str, fallback_region: Option<country::Id>) -> Normalization {
match phonenumber::parse(fallback_region, raw) {
Ok(number) => {
if !number.is_valid() {
return Normalization::InvalidNumber;
}
Normalization::E164(number.format().mode(Mode::E164).to_string())
}
Err(ParseError::InvalidCountryCode) => {
if looks_international(raw) {
Normalization::InvalidCountryCode
} else {
Normalization::MissingRegion
}
}
Err(ParseError::TooShortAfterIdd | ParseError::TooShortNsn) => Normalization::TooShort,
Err(ParseError::TooLong) => Normalization::TooLong,
Err(ParseError::NoNumber | ParseError::MalformedInteger(_)) => Normalization::NotANumber,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhoneField {
raw: String,
e164: Option<String>,
}
impl PhoneField {
#[must_use]
pub fn new(raw: &str, region: Option<country::Id>) -> Self {
let e164 = match normalize_number(raw, region) {
Normalization::E164(number) => Some(number),
_ => None,
};
Self { raw: raw.to_owned(), e164 }
}
#[must_use]
pub const fn from_parts(raw: String, e164: Option<String>) -> Self {
Self { raw, e164 }
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn e164(&self) -> Option<&str> {
self.e164.as_deref()
}
#[must_use]
pub fn canonical(&self) -> &str {
self.e164.as_deref().unwrap_or(&self.raw)
}
#[must_use]
pub fn display(&self) -> &str {
self.canonical()
}
}
fn looks_international(raw: &str) -> bool {
let value = raw.trim_start();
value.starts_with('+') || value.starts_with("00")
}