Skip to main content

payrail_core/
phone.rs

1use crate::PaymentError;
2
3/// Validated E.164-style phone number.
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub struct PhoneNumber(String);
6
7impl PhoneNumber {
8    /// Creates a phone number from digits with an optional leading `+`.
9    ///
10    /// # Errors
11    ///
12    /// Returns an error when the phone number is outside E.164 length bounds or
13    /// contains unsupported characters.
14    pub fn new(value: impl AsRef<str>) -> Result<Self, PaymentError> {
15        let value = value.as_ref().trim();
16        let digits = value.strip_prefix('+').unwrap_or(value);
17        if !(8..=15).contains(&digits.len()) || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
18            return Err(PaymentError::InvalidPhoneNumber(value.to_owned()));
19        }
20
21        Ok(Self(format!("+{digits}")))
22    }
23
24    /// Creates a phone number without adding the display `+` prefix.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error when the number is invalid.
29    pub fn new_digits(value: impl AsRef<str>) -> Result<Self, PaymentError> {
30        Self::new(value)
31    }
32
33    /// Returns the normalized number with a leading `+`.
34    #[inline]
35    #[must_use]
36    pub fn as_e164(&self) -> &str {
37        &self.0
38    }
39
40    /// Returns the normalized digits without the leading `+`.
41    #[inline]
42    #[must_use]
43    pub fn digits(&self) -> &str {
44        &self.0[1..]
45    }
46}
47
48impl AsRef<str> for PhoneNumber {
49    #[inline]
50    fn as_ref(&self) -> &str {
51        self.as_e164()
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn new_normalizes_number() {
61        let phone = PhoneNumber::new("260971234567").expect("phone should be valid");
62        let phone_from_digits =
63            PhoneNumber::new_digits("260971234567").expect("phone should be valid");
64
65        assert_eq!(phone.as_e164(), "+260971234567");
66        assert_eq!(phone.digits(), "260971234567");
67        assert_eq!(phone_from_digits.as_ref(), "+260971234567");
68    }
69
70    #[test]
71    fn new_rejects_short_number() {
72        assert!(matches!(
73            PhoneNumber::new("123"),
74            Err(PaymentError::InvalidPhoneNumber(_))
75        ));
76    }
77}