icydb_model/base/validator/intl/
phone.rs1use crate::{prelude::*, visitor::Validator};
8
9#[validator]
16pub struct E164PhoneNumber;
17
18impl Validator<str> for E164PhoneNumber {
19 fn validate(&self, s: &str, ctx: &mut dyn VisitorContext) {
20 let Some(digits) = s.strip_prefix('+') else {
21 ctx.issue("phone number must start with +");
22 return;
23 };
24
25 if digits.is_empty() || !digits.chars().all(|ch| ch.is_ascii_digit()) {
26 ctx.issue("phone number must contain only digits after +");
27 return;
28 }
29
30 if digits.starts_with('0') {
31 ctx.issue("phone number country code must not start with 0");
32 return;
33 }
34
35 let digit_count = digits.len();
36
37 if !(7..=15).contains(&digit_count) {
38 ctx.issue(format!(
39 "phone number has {digit_count} digits; expected 7 to 15"
40 ));
41 }
42 }
43}
44
45#[cfg(test)]
50mod tests {
51 use super::*;
52
53 struct TestCtx {
54 issues: crate::visitor::VisitorIssues,
55 }
56
57 impl TestCtx {
58 fn new() -> Self {
59 Self {
60 issues: crate::visitor::VisitorIssues::new(),
61 }
62 }
63
64 fn has_issues(&self) -> bool {
65 !self.issues.is_empty()
66 }
67 }
68
69 impl crate::visitor::VisitorContext for TestCtx {
70 fn add_issue(&mut self, issue: crate::visitor::Issue) {
71 self.issues.push(String::new(), issue);
72 }
73
74 fn add_issue_at(&mut self, _: crate::visitor::PathSegment, issue: crate::visitor::Issue) {
75 self.add_issue(issue);
76 }
77 }
78
79 #[test]
80 fn e164_phone_validator_accepts_canonical_phone_number() {
81 let validator = E164PhoneNumber;
82 let mut ctx = TestCtx::new();
83
84 validator.validate("+15551234567", &mut ctx);
85
86 assert!(!ctx.has_issues());
87 }
88
89 #[test]
90 fn e164_phone_validator_rejects_interleaved_non_digits() {
91 let validator = E164PhoneNumber;
92 let mut ctx = TestCtx::new();
93
94 validator.validate("+1 (555) 123-4567", &mut ctx);
95
96 assert!(ctx.has_issues());
97 }
98
99 #[test]
100 fn e164_phone_validator_rejects_zero_country_code_and_bad_lengths() {
101 for value in ["+05551234567", "+123456", "+1234567890123456"] {
102 let validator = E164PhoneNumber;
103 let mut ctx = TestCtx::new();
104
105 validator.validate(value, &mut ctx);
106
107 assert!(ctx.has_issues(), "{value} should be rejected");
108 }
109 }
110}