agentis_pay_shared/types/
phone.rs1#[derive(Debug, Clone, Copy)]
2pub struct Phone(arrayvec::ArrayString<14>);
3
4impl Phone {
5 #[must_use]
6 pub fn country_code_and_rest(&self) -> (&str, &str) {
7 (&self.0[..3], &self.0[3..])
8 }
9
10 #[must_use]
11 pub fn format_short(&self) -> String {
12 format!("({}) {}-{}", &self.0[3..5], &self.0[5..10], &self.0[10..])
13 }
14
15 #[must_use]
16 pub fn format_masked(&self) -> String {
17 let (_, _, number) = (&self.0[..3], &self.0[3..5], &self.0[5..]);
18
19 format!("(**) *****-{}", &number[number.len() - 4..])
20 }
21}
22
23impl std::ops::Deref for Phone {
24 type Target = str;
25
26 fn deref(&self) -> &Self::Target {
27 &self.0
28 }
29}
30
31impl std::str::FromStr for Phone {
32 type Err = &'static str;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 let normalized = normalize_phone(s)?;
36 arrayvec::ArrayString::from(normalized.as_str())
37 .map_err(|_| "Phone has wrong length")
38 .and_then(|s| {
39 REGEX
40 .is_match(&s)
41 .then_some(Phone(s))
42 .ok_or("Phone has wrong format")
43 })
44 }
45}
46
47fn normalize_phone(raw: &str) -> Result<String, &'static str> {
48 let mut normalized = String::with_capacity(raw.len());
49
50 for ch in raw.trim().chars() {
51 if ch.is_ascii_digit() {
52 normalized.push(ch);
53 } else if ch == '+' {
54 if normalized.is_empty() {
55 normalized.push(ch);
56 } else {
57 return Err("Phone has wrong format");
58 }
59 } else if matches!(ch, ' ' | '(' | ')' | '-') {
60 continue;
61 } else {
62 return Err("Phone has wrong format");
63 }
64 }
65
66 if normalized.is_empty() {
67 return Err("Phone has wrong length");
68 }
69
70 Ok(normalized)
71}
72
73static REGEX: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
74 regex::Regex::new(
75 r"(?x)
76 ^ # from the start
77 \+55 # country code from brazil
78 [1-9]{2} # two non-zero digits for area code
79 9[1-9]{1} # one 9 followed by non-zero digit for mobile number
80 [0-9]{7} # seven more digits for good measure
81 $ # the end
82 ",
83 )
84 .expect("Phone regex couldn't be compiled")
85});
86
87#[cfg(test)]
88mod tests {
89 #[test]
90 fn number_parsing() {
91 assert!("+5531983136171".parse::<super::Phone>().is_ok());
92 assert!("+5511969695920".parse::<super::Phone>().is_ok());
93 assert!("+55 (11) 96969-5920".parse::<super::Phone>().is_ok());
94
95 assert!("+553183136171".parse::<super::Phone>().is_err());
96 assert!("+551169695920".parse::<super::Phone>().is_err());
97 assert!("+550169695920".parse::<super::Phone>().is_err());
98 assert!("+551069695920".parse::<super::Phone>().is_err());
99 assert!("+5511696959201".parse::<super::Phone>().is_err());
100 assert!("+5501696959201".parse::<super::Phone>().is_err());
101 assert!("+5510696959201".parse::<super::Phone>().is_err());
102 assert!("5511969695920".parse::<super::Phone>().is_err());
103 assert!("+55 test".parse::<super::Phone>().is_err());
104 }
105
106 #[test]
107 fn deref_to_raw() -> Result<(), &'static str> {
108 let raw = "+55 (31) 98313-6171";
109 assert_eq!(&raw.parse::<super::Phone>()? as &str, "+5531983136171");
110 Ok(())
111 }
112
113 #[test]
114 fn without_country_code() -> Result<(), &'static str> {
115 assert_eq!(
116 "+5531983136171"
117 .parse::<super::Phone>()?
118 .country_code_and_rest(),
119 ("+55", "31983136171")
120 );
121 Ok(())
122 }
123
124 #[test]
125 fn format_short() -> Result<(), &'static str> {
126 let formatted = "+5531983136171".parse::<super::Phone>()?.format_short();
127 assert_eq!(&formatted, "(31) 98313-6171");
128
129 let formatted = "+5511969695920".parse::<super::Phone>()?.format_short();
130
131 assert_eq!(&formatted, "(11) 96969-5920");
132 Ok(())
133 }
134
135 #[test]
136 fn format_masked() -> Result<(), &'static str> {
137 let phone = "+5531983136171".parse::<super::Phone>()?;
138 assert_eq!(phone.format_masked(), "(**) *****-6171");
139
140 let phone = "+5511969695920".parse::<super::Phone>()?;
141 assert_eq!(phone.format_masked(), "(**) *****-5920");
142
143 let phone = "+5521987654321".parse::<super::Phone>()?;
144 assert_eq!(phone.format_masked(), "(**) *****-4321");
145 Ok(())
146 }
147}