robinpath_modules/modules/
phone_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 rp.register_builtin("phone.parse", |args, _| {
5 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6 let digits = normalize_phone(&raw);
7 let mut obj = indexmap::IndexMap::new();
8 obj.insert("raw".to_string(), Value::String(raw));
9 obj.insert("digits".to_string(), Value::String(digits.clone()));
10 obj.insert("isValid".to_string(), Value::Bool(digits.len() >= 7 && digits.len() <= 15));
11 Ok(Value::Object(obj))
12 });
13
14 rp.register_builtin("phone.format", |args, _| {
15 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
16 let digits = normalize_phone(&raw);
17 if digits.len() == 10 {
18 Ok(Value::String(format!(
19 "({}) {}-{}",
20 &digits[0..3],
21 &digits[3..6],
22 &digits[6..10]
23 )))
24 } else if digits.len() == 11 && digits.starts_with('1') {
25 Ok(Value::String(format!(
26 "+1 ({}) {}-{}",
27 &digits[1..4],
28 &digits[4..7],
29 &digits[7..11]
30 )))
31 } else {
32 Ok(Value::String(raw))
33 }
34 });
35
36 rp.register_builtin("phone.formatE164", |args, _| {
37 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
38 let country = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "US".to_string());
39 let digits = normalize_phone(&raw);
40 let dial_code = get_dial_code(&country);
41 if digits.starts_with(&dial_code) {
42 Ok(Value::String(format!("+{}", digits)))
43 } else {
44 Ok(Value::String(format!("+{}{}", dial_code, digits)))
45 }
46 });
47
48 rp.register_builtin("phone.formatInternational", |args, _| {
49 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
50 let country = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "US".to_string());
51 let digits = normalize_phone(&raw);
52 let dial_code = get_dial_code(&country);
53 let national = if digits.starts_with(&dial_code) {
54 &digits[dial_code.len()..]
55 } else {
56 &digits
57 };
58 Ok(Value::String(format!("+{} {}", dial_code, national)))
59 });
60
61 rp.register_builtin("phone.validate", |args, _| {
62 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
63 let digits = normalize_phone(&raw);
64 Ok(Value::Bool(digits.len() >= 7 && digits.len() <= 15))
65 });
66
67 rp.register_builtin("phone.getCountry", |args, _| {
68 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
69 let digits = normalize_phone(&raw);
70 for entry in COUNTRY_CODES {
71 if digits.starts_with(entry.dial_code) {
72 return Ok(Value::String(entry.code.to_string()));
73 }
74 }
75 Ok(Value::Null)
76 });
77
78 rp.register_builtin("phone.getType", |args, _| {
79 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
80 let digits = normalize_phone(&raw);
81 let phone_type = if digits.len() == 10 || digits.len() == 11 {
83 "mobile"
84 } else {
85 "unknown"
86 };
87 Ok(Value::String(phone_type.to_string()))
88 });
89
90 rp.register_builtin("phone.normalize", |args, _| {
91 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
92 Ok(Value::String(normalize_phone(&raw)))
93 });
94
95 rp.register_builtin("phone.mask", |args, _| {
96 let raw = args.first().map(|v| v.to_display_string()).unwrap_or_default();
97 let digits = normalize_phone(&raw);
98 if digits.len() >= 4 {
99 let visible = &digits[digits.len() - 4..];
100 let masked = "*".repeat(digits.len() - 4);
101 Ok(Value::String(format!("{}{}", masked, visible)))
102 } else {
103 Ok(Value::String("****".to_string()))
104 }
105 });
106
107 rp.register_builtin("phone.dialCode", |args, _| {
108 let country = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "US".to_string());
109 Ok(Value::String(format!("+{}", get_dial_code(&country))))
110 });
111
112 rp.register_builtin("phone.countryInfo", |args, _| {
113 let code = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "US".to_string());
114 let upper = code.to_uppercase();
115 if let Some(entry) = COUNTRY_CODES.iter().find(|c| c.code == upper) {
116 let mut obj = indexmap::IndexMap::new();
117 obj.insert("code".to_string(), Value::String(entry.code.to_string()));
118 obj.insert("name".to_string(), Value::String(entry.name.to_string()));
119 obj.insert("dialCode".to_string(), Value::String(format!("+{}", entry.dial_code)));
120 Ok(Value::Object(obj))
121 } else {
122 Ok(Value::Null)
123 }
124 });
125
126 rp.register_builtin("phone.listCountries", |_args, _| {
127 let countries: Vec<Value> = COUNTRY_CODES
128 .iter()
129 .map(|c| Value::String(c.code.to_string()))
130 .collect();
131 Ok(Value::Array(countries))
132 });
133
134 rp.register_builtin("phone.compare", |args, _| {
135 let a = args.first().map(|v| v.to_display_string()).unwrap_or_default();
136 let b = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
137 Ok(Value::Bool(normalize_phone(&a) == normalize_phone(&b)))
138 });
139}
140
141fn normalize_phone(raw: &str) -> String {
142 raw.chars().filter(|c| c.is_ascii_digit()).collect()
143}
144
145fn get_dial_code(country: &str) -> String {
146 let upper = country.to_uppercase();
147 COUNTRY_CODES
148 .iter()
149 .find(|c| c.code == upper)
150 .map(|c| c.dial_code.to_string())
151 .unwrap_or_else(|| "1".to_string())
152}
153
154struct CountryEntry {
155 code: &'static str,
156 name: &'static str,
157 dial_code: &'static str,
158}
159
160const COUNTRY_CODES: &[CountryEntry] = &[
161 CountryEntry { code: "US", name: "United States", dial_code: "1" },
162 CountryEntry { code: "CA", name: "Canada", dial_code: "1" },
163 CountryEntry { code: "GB", name: "United Kingdom", dial_code: "44" },
164 CountryEntry { code: "DE", name: "Germany", dial_code: "49" },
165 CountryEntry { code: "FR", name: "France", dial_code: "33" },
166 CountryEntry { code: "IT", name: "Italy", dial_code: "39" },
167 CountryEntry { code: "ES", name: "Spain", dial_code: "34" },
168 CountryEntry { code: "JP", name: "Japan", dial_code: "81" },
169 CountryEntry { code: "CN", name: "China", dial_code: "86" },
170 CountryEntry { code: "KR", name: "South Korea", dial_code: "82" },
171 CountryEntry { code: "IN", name: "India", dial_code: "91" },
172 CountryEntry { code: "AU", name: "Australia", dial_code: "61" },
173 CountryEntry { code: "NZ", name: "New Zealand", dial_code: "64" },
174 CountryEntry { code: "BR", name: "Brazil", dial_code: "55" },
175 CountryEntry { code: "MX", name: "Mexico", dial_code: "52" },
176 CountryEntry { code: "RU", name: "Russia", dial_code: "7" },
177 CountryEntry { code: "SE", name: "Sweden", dial_code: "46" },
178 CountryEntry { code: "NO", name: "Norway", dial_code: "47" },
179 CountryEntry { code: "DK", name: "Denmark", dial_code: "45" },
180 CountryEntry { code: "FI", name: "Finland", dial_code: "358" },
181 CountryEntry { code: "NL", name: "Netherlands", dial_code: "31" },
182 CountryEntry { code: "CH", name: "Switzerland", dial_code: "41" },
183 CountryEntry { code: "PL", name: "Poland", dial_code: "48" },
184 CountryEntry { code: "TR", name: "Turkey", dial_code: "90" },
185 CountryEntry { code: "ZA", name: "South Africa", dial_code: "27" },
186 CountryEntry { code: "SG", name: "Singapore", dial_code: "65" },
187 CountryEntry { code: "HK", name: "Hong Kong", dial_code: "852" },
188 CountryEntry { code: "TH", name: "Thailand", dial_code: "66" },
189 CountryEntry { code: "AR", name: "Argentina", dial_code: "54" },
190 CountryEntry { code: "EG", name: "Egypt", dial_code: "20" },
191];