use crate::error::{DomainError, DomainErrorKind};
use phonenumber::{Mode, parse};
use stillwater::refined::{Predicate, Refined};
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidPhoneNumber;
impl Predicate<String> for ValidPhoneNumber {
type Error = DomainError;
fn check(value: &String) -> Result<(), Self::Error> {
if value.is_empty() {
return Err(DomainError {
format_name: "phone number",
value: value.clone(),
reason: DomainErrorKind::Empty,
example: "+14155551234",
});
}
let parsed = parse(None, value).map_err(|_| DomainError {
format_name: "phone number",
value: value.clone(),
reason: DomainErrorKind::InvalidFormat {
expected: "E.164 format (+[country][number])",
},
example: "+14155551234",
})?;
if phonenumber::is_valid(&parsed) {
Ok(())
} else {
Err(DomainError {
format_name: "phone number",
value: value.clone(),
reason: DomainErrorKind::InvalidFormat {
expected: "valid phone number for region",
},
example: "+14155551234",
})
}
}
fn description() -> &'static str {
"E.164 phone number"
}
}
pub type PhoneNumber = Refined<String, ValidPhoneNumber>;
pub trait PhoneNumberExt {
fn to_e164(&self) -> String;
fn country_code(&self) -> u16;
}
impl PhoneNumberExt for PhoneNumber {
fn to_e164(&self) -> String {
let parsed = parse(None, self.get()).expect("already validated");
parsed.format().mode(Mode::E164).to_string()
}
fn country_code(&self) -> u16 {
let parsed = parse(None, self.get()).expect("already validated");
parsed.code().value()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_us_e164() {
assert!(PhoneNumber::new("+14155551234".to_string()).is_ok());
}
#[test]
fn valid_us_formatted() {
assert!(PhoneNumber::new("+1 (415) 555-1234".to_string()).is_ok());
}
#[test]
fn valid_uk_e164() {
assert!(PhoneNumber::new("+442071234567".to_string()).is_ok());
}
#[test]
fn valid_uk_formatted() {
assert!(PhoneNumber::new("+44 20 7123 4567".to_string()).is_ok());
}
#[test]
fn valid_france() {
assert!(PhoneNumber::new("+33123456789".to_string()).is_ok());
}
#[test]
fn valid_germany() {
assert!(PhoneNumber::new("+4930123456".to_string()).is_ok());
}
#[test]
fn valid_japan() {
assert!(PhoneNumber::new("+81312345678".to_string()).is_ok());
}
#[test]
fn invalid_empty() {
let result = PhoneNumber::new(String::new());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err.reason, DomainErrorKind::Empty));
}
#[test]
fn invalid_no_country_code() {
assert!(PhoneNumber::new("4155551234".to_string()).is_err());
}
#[test]
fn invalid_too_short() {
assert!(PhoneNumber::new("+1234".to_string()).is_err());
}
#[test]
fn invalid_too_long() {
assert!(PhoneNumber::new("+12345678901234567890".to_string()).is_err());
}
#[test]
fn invalid_letters_only() {
assert!(PhoneNumber::new("ABCDEFGHIJ".to_string()).is_err());
}
#[test]
fn invalid_random_text() {
assert!(PhoneNumber::new("not a phone number".to_string()).is_err());
}
#[test]
fn to_e164_strips_formatting() {
let phone = PhoneNumber::new("+1 (415) 555-1234".to_string()).unwrap();
assert_eq!(phone.to_e164(), "+14155551234");
}
#[test]
fn to_e164_preserves_country_code() {
let phone = PhoneNumber::new("+44 20 7123 4567".to_string()).unwrap();
assert_eq!(phone.to_e164(), "+442071234567");
}
#[test]
fn to_e164_idempotent() {
let phone = PhoneNumber::new("+14155551234".to_string()).unwrap();
assert_eq!(phone.to_e164(), "+14155551234");
}
#[test]
fn country_code_us() {
let phone = PhoneNumber::new("+14155551234".to_string()).unwrap();
assert_eq!(phone.country_code(), 1);
}
#[test]
fn country_code_uk() {
let phone = PhoneNumber::new("+442071234567".to_string()).unwrap();
assert_eq!(phone.country_code(), 44);
}
#[test]
fn country_code_france() {
let phone = PhoneNumber::new("+33123456789".to_string()).unwrap();
assert_eq!(phone.country_code(), 33);
}
#[test]
fn error_includes_format_name() {
let result = PhoneNumber::new("invalid".to_string());
let err = result.unwrap_err();
assert_eq!(err.format_name, "phone number");
}
#[test]
fn error_includes_example() {
let result = PhoneNumber::new("invalid".to_string());
let err = result.unwrap_err();
assert_eq!(err.example, "+14155551234");
}
#[test]
fn error_display_is_readable() {
let result = PhoneNumber::new("invalid".to_string());
let err = result.unwrap_err();
let display = err.to_string();
assert!(display.contains("phone number"));
assert!(display.contains("+14155551234"));
}
#[test]
fn description_returns_expected() {
assert_eq!(ValidPhoneNumber::description(), "E.164 phone number");
}
}