Skip to main content

agentis_pay_shared/types/
phone.rs

1#[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}