use crate::error::{ValidationError, ValidationResult};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PhoneNumber {
pub value: String,
pub display: Option<String>,
#[serde(rename = "type")]
pub phone_type: Option<String>,
pub primary: Option<bool>,
}
impl PhoneNumber {
pub fn new(
value: String,
display: Option<String>,
phone_type: Option<String>,
primary: Option<bool>,
) -> ValidationResult<Self> {
Self::validate_phone_value(&value)?;
if let Some(ref d) = display {
Self::validate_display(d)?;
}
if let Some(ref pt) = phone_type {
Self::validate_phone_type(pt)?;
}
Ok(Self {
value,
display,
phone_type,
primary,
})
}
pub fn new_simple(value: String, phone_type: String) -> ValidationResult<Self> {
Self::new(value, None, Some(phone_type), None)
}
pub fn new_work(value: String) -> ValidationResult<Self> {
Self::new(value, None, Some("work".to_string()), None)
}
pub fn new_mobile(value: String) -> ValidationResult<Self> {
Self::new(value, None, Some("mobile".to_string()), None)
}
pub fn value(&self) -> &str {
&self.value
}
pub fn display(&self) -> Option<&str> {
self.display.as_deref()
}
pub fn phone_type(&self) -> Option<&str> {
self.phone_type.as_deref()
}
pub fn is_primary(&self) -> bool {
self.primary.unwrap_or(false)
}
pub fn display_value(&self) -> &str {
self.display.as_deref().unwrap_or(&self.value)
}
pub fn is_rfc3966_format(&self) -> bool {
self.value.starts_with("tel:")
}
pub fn to_rfc3966(&self) -> String {
if self.is_rfc3966_format() {
self.value.clone()
} else {
let cleaned = self
.value
.chars()
.filter(|c| c.is_ascii_digit() || *c == '+' || *c == '-')
.collect::<String>();
if cleaned.starts_with('+') {
format!("tel:{}", cleaned)
} else {
format!("tel:+{}", cleaned)
}
}
}
fn validate_phone_value(value: &str) -> ValidationResult<()> {
if value.trim().is_empty() {
return Err(ValidationError::custom(
"value: Phone number value cannot be empty",
));
}
if value.len() > 50 {
return Err(ValidationError::custom(
"value: Phone number exceeds maximum length of 50 characters",
));
}
if value.starts_with("tel:") {
let phone_part = &value[4..];
if phone_part.is_empty() {
return Err(ValidationError::custom(
"value: RFC 3966 format phone number cannot be empty after 'tel:' prefix",
));
}
}
if !value.chars().any(|c| c.is_ascii_digit()) {
return Err(ValidationError::custom(
"value: Phone number must contain at least one digit",
));
}
let has_invalid_chars = value.chars().any(|c| {
!c.is_ascii_digit()
&& c != '+'
&& c != '-'
&& c != '('
&& c != ')'
&& c != ' '
&& c != '.'
&& c != ':'
&& !c.is_ascii_alphabetic() });
if has_invalid_chars {
return Err(ValidationError::custom(
"value: Phone number contains invalid characters",
));
}
Ok(())
}
fn validate_display(display: &str) -> ValidationResult<()> {
if display.trim().is_empty() {
return Err(ValidationError::custom(
"display: Display name cannot be empty or contain only whitespace",
));
}
if display.len() > 256 {
return Err(ValidationError::custom(
"display: Display name exceeds maximum length of 256 characters",
));
}
Ok(())
}
fn validate_phone_type(phone_type: &str) -> ValidationResult<()> {
if phone_type.trim().is_empty() {
return Err(ValidationError::custom("type: Phone type cannot be empty"));
}
let valid_types = ["work", "home", "mobile", "fax", "pager", "other"];
if !valid_types.contains(&phone_type) {
return Err(ValidationError::custom(format!(
"type: '{}' is not a valid phone type. Valid types are: {:?}",
phone_type, valid_types
)));
}
Ok(())
}
}
impl fmt::Display for PhoneNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(phone_type) = &self.phone_type {
write!(f, "{} ({})", self.display_value(), phone_type)
} else {
write!(f, "{}", self.display_value())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_phone_number_full() {
let phone = PhoneNumber::new(
"+1-201-555-0123".to_string(),
Some("Work Phone".to_string()),
Some("work".to_string()),
Some(true),
);
assert!(phone.is_ok());
let phone = phone.unwrap();
assert_eq!(phone.value(), "+1-201-555-0123");
assert_eq!(phone.display(), Some("Work Phone"));
assert_eq!(phone.phone_type(), Some("work"));
assert!(phone.is_primary());
}
#[test]
fn test_valid_phone_number_simple() {
let phone = PhoneNumber::new_simple("555-123-4567".to_string(), "mobile".to_string());
assert!(phone.is_ok());
let phone = phone.unwrap();
assert_eq!(phone.value(), "555-123-4567");
assert_eq!(phone.phone_type(), Some("mobile"));
assert!(!phone.is_primary());
}
#[test]
fn test_valid_phone_number_work() {
let phone = PhoneNumber::new_work("+1-555-123-4567".to_string());
assert!(phone.is_ok());
let phone = phone.unwrap();
assert_eq!(phone.phone_type(), Some("work"));
assert_eq!(phone.value(), "+1-555-123-4567");
}
#[test]
fn test_valid_phone_number_mobile() {
let phone = PhoneNumber::new_mobile("(555) 123-4567".to_string());
assert!(phone.is_ok());
let phone = phone.unwrap();
assert_eq!(phone.phone_type(), Some("mobile"));
assert_eq!(phone.value(), "(555) 123-4567");
}
#[test]
fn test_empty_phone_value() {
let result = PhoneNumber::new("".to_string(), None, Some("work".to_string()), None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Phone number value cannot be empty")
);
}
#[test]
fn test_invalid_phone_type() {
let result = PhoneNumber::new(
"555-123-4567".to_string(),
None,
Some("business".to_string()), None,
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not a valid phone type")
);
}
#[test]
fn test_too_long_phone_value() {
let long_phone = "1".repeat(60);
let result = PhoneNumber::new_work(long_phone);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exceeds maximum length")
);
}
#[test]
fn test_phone_without_digits() {
let result = PhoneNumber::new_work("abc-def-ghij".to_string());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("must contain at least one digit")
);
}
#[test]
fn test_phone_with_invalid_characters() {
let result = PhoneNumber::new_work("555-123-4567#".to_string());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("contains invalid characters")
);
}
#[test]
fn test_empty_display() {
let result = PhoneNumber::new(
"555-123-4567".to_string(),
Some("".to_string()),
Some("work".to_string()),
None,
);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Display name cannot be empty")
);
}
#[test]
fn test_rfc3966_format() {
let phone = PhoneNumber::new_work("tel:+1-201-555-0123".to_string()).unwrap();
assert!(phone.is_rfc3966_format());
let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
assert!(!phone2.is_rfc3966_format());
}
#[test]
fn test_to_rfc3966() {
let phone = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
assert_eq!(phone.to_rfc3966(), "tel:+555-123-4567");
let phone2 = PhoneNumber::new_work("+1-555-123-4567".to_string()).unwrap();
assert_eq!(phone2.to_rfc3966(), "tel:+1-555-123-4567");
let phone3 = PhoneNumber::new_work("tel:+1-555-123-4567".to_string()).unwrap();
assert_eq!(phone3.to_rfc3966(), "tel:+1-555-123-4567");
}
#[test]
fn test_display_value() {
let phone = PhoneNumber::new(
"555-123-4567".to_string(),
Some("My Work Phone".to_string()),
Some("work".to_string()),
None,
)
.unwrap();
assert_eq!(phone.display_value(), "My Work Phone");
let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
assert_eq!(phone2.display_value(), "555-123-4567");
}
#[test]
fn test_display() {
let phone = PhoneNumber::new(
"555-123-4567".to_string(),
Some("My Work Phone".to_string()),
Some("work".to_string()),
None,
)
.unwrap();
assert_eq!(format!("{}", phone), "My Work Phone (work)");
let phone2 = PhoneNumber::new(
"555-123-4567".to_string(),
None,
Some("mobile".to_string()),
None,
)
.unwrap();
assert_eq!(format!("{}", phone2), "555-123-4567 (mobile)");
let phone3 = PhoneNumber::new("555-123-4567".to_string(), None, None, None).unwrap();
assert_eq!(format!("{}", phone3), "555-123-4567");
}
#[test]
fn test_serialization() {
let phone = PhoneNumber::new(
"+1-201-555-0123".to_string(),
Some("Work Phone".to_string()),
Some("work".to_string()),
Some(true),
)
.unwrap();
let json = serde_json::to_string(&phone).unwrap();
assert!(json.contains("\"value\":\"+1-201-555-0123\""));
assert!(json.contains("\"display\":\"Work Phone\""));
assert!(json.contains("\"type\":\"work\""));
assert!(json.contains("\"primary\":true"));
}
#[test]
fn test_deserialization() {
let json = r#"{
"value": "+1-201-555-0123",
"display": "Work Phone",
"type": "work",
"primary": true
}"#;
let phone: PhoneNumber = serde_json::from_str(json).unwrap();
assert_eq!(phone.value(), "+1-201-555-0123");
assert_eq!(phone.display(), Some("Work Phone"));
assert_eq!(phone.phone_type(), Some("work"));
assert!(phone.is_primary());
}
#[test]
fn test_equality() {
let phone1 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
let phone2 = PhoneNumber::new_work("555-123-4567".to_string()).unwrap();
let phone3 = PhoneNumber::new_mobile("555-123-4567".to_string()).unwrap();
assert_eq!(phone1, phone2);
assert_ne!(phone1, phone3);
}
#[test]
fn test_clone() {
let original = PhoneNumber::new(
"+1-555-123-4567".to_string(),
Some("Work Phone".to_string()),
Some("work".to_string()),
Some(true),
)
.unwrap();
let cloned = original.clone();
assert_eq!(original, cloned);
assert_eq!(cloned.value(), "+1-555-123-4567");
assert_eq!(cloned.phone_type(), Some("work"));
}
#[test]
fn test_valid_phone_types() {
for phone_type in ["work", "home", "mobile", "fax", "pager", "other"] {
let phone = PhoneNumber::new(
"555-123-4567".to_string(),
None,
Some(phone_type.to_string()),
None,
);
assert!(phone.is_ok(), "Phone type '{}' should be valid", phone_type);
}
}
#[test]
fn test_various_phone_formats() {
let formats = [
"555-123-4567",
"(555) 123-4567",
"+1-555-123-4567",
"tel:+1-555-123-4567",
"555.123.4567",
"1 555 123 4567",
];
for format in &formats {
let phone = PhoneNumber::new_work(format.to_string());
assert!(phone.is_ok(), "Phone format '{}' should be valid", format);
}
}
#[test]
fn test_invalid_rfc3966_format() {
let result = PhoneNumber::new_work("tel:".to_string());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("cannot be empty after 'tel:' prefix"));
let result2 = PhoneNumber::new(
"tel:+1-555-123-4567".to_string(),
None,
Some("work".to_string()),
None,
);
assert!(result2.is_ok());
}
}