1extern crate alloc;
4
5use alloc::string::{String, ToString};
6use core::fmt;
7use core::ops::Deref;
8use core::str::FromStr;
9
10use crate::error::GeoError;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "serde", serde(transparent))]
24pub struct CountryCode(String);
25
26impl CountryCode {
27 pub fn parse(s: &str) -> Result<Self, GeoError> {
33 validate_country(s)
34 }
35
36 pub fn new(s: String) -> Result<Self, GeoError> {
42 validate_country(&s)
43 }
44
45 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50
51 #[must_use]
53 pub fn into_inner(self) -> String {
54 self.0
55 }
56
57 #[must_use]
61 pub fn country_name(&self) -> &str {
62 match self.0.as_str() {
63 "AD" => "Andorra",
64 "AE" => "United Arab Emirates",
65 "AF" => "Afghanistan",
66 "AG" => "Antigua and Barbuda",
67 "AI" => "Anguilla",
68 "AL" => "Albania",
69 "AM" => "Armenia",
70 "AO" => "Angola",
71 "AQ" => "Antarctica",
72 "AR" => "Argentina",
73 "AS" => "American Samoa",
74 "AT" => "Austria",
75 "AU" => "Australia",
76 "AW" => "Aruba",
77 "AX" => "Åland Islands",
78 "AZ" => "Azerbaijan",
79 "BA" => "Bosnia and Herzegovina",
80 "BB" => "Barbados",
81 "BD" => "Bangladesh",
82 "BE" => "Belgium",
83 "BF" => "Burkina Faso",
84 "BG" => "Bulgaria",
85 "BH" => "Bahrain",
86 "BI" => "Burundi",
87 "BJ" => "Benin",
88 "BM" => "Bermuda",
89 "BN" => "Brunei",
90 "BO" => "Bolivia",
91 "BQ" => "Bonaire, Sint Eustatius and Saba",
92 "BR" => "Brazil",
93 "BS" => "Bahamas",
94 "BT" => "Bhutan",
95 "BV" => "Bouvet Island",
96 "BW" => "Botswana",
97 "BY" => "Belarus",
98 "BZ" => "Belize",
99 "CA" => "Canada",
100 "CC" => "Cocos (Keeling) Islands",
101 "CD" => "Congo, Democratic Republic of the",
102 "CF" => "Central African Republic",
103 "CG" => "Congo",
104 "CH" => "Switzerland",
105 "CI" => "Côte d'Ivoire",
106 "CK" => "Cook Islands",
107 "CL" => "Chile",
108 "CM" => "Cameroon",
109 "CN" => "China",
110 "CO" => "Colombia",
111 "CR" => "Costa Rica",
112 "CU" => "Cuba",
113 "CV" => "Cabo Verde",
114 "CW" => "Curaçao",
115 "CX" => "Christmas Island",
116 "CY" => "Cyprus",
117 "CZ" => "Czechia",
118 "DE" => "Germany",
119 "DJ" => "Djibouti",
120 "DK" => "Denmark",
121 "DM" => "Dominica",
122 "DO" => "Dominican Republic",
123 "DZ" => "Algeria",
124 "EC" => "Ecuador",
125 "EE" => "Estonia",
126 "EG" => "Egypt",
127 "EH" => "Western Sahara",
128 "ER" => "Eritrea",
129 "ES" => "Spain",
130 "ET" => "Ethiopia",
131 "FI" => "Finland",
132 "FJ" => "Fiji",
133 "FK" => "Falkland Islands (Malvinas)",
134 "FM" => "Micronesia (Federated States of)",
135 "FO" => "Faroe Islands",
136 "FR" => "France",
137 "GA" => "Gabon",
138 "GB" => "United Kingdom",
139 "GD" => "Grenada",
140 "GE" => "Georgia",
141 "GF" => "French Guiana",
142 "GG" => "Guernsey",
143 "GH" => "Ghana",
144 "GI" => "Gibraltar",
145 "GL" => "Greenland",
146 "GM" => "Gambia",
147 "GN" => "Guinea",
148 "GP" => "Guadeloupe",
149 "GQ" => "Equatorial Guinea",
150 "GR" => "Greece",
151 "GS" => "South Georgia and the South Sandwich Islands",
152 "GT" => "Guatemala",
153 "GU" => "Guam",
154 "GW" => "Guinea-Bissau",
155 "GY" => "Guyana",
156 "HK" => "Hong Kong",
157 "HM" => "Heard Island and McDonald Islands",
158 "HN" => "Honduras",
159 "HR" => "Croatia",
160 "HT" => "Haiti",
161 "HU" => "Hungary",
162 "ID" => "Indonesia",
163 "IE" => "Ireland",
164 "IL" => "Israel",
165 "IM" => "Isle of Man",
166 "IN" => "India",
167 "IO" => "British Indian Ocean Territory",
168 "IQ" => "Iraq",
169 "IR" => "Iran",
170 "IS" => "Iceland",
171 "IT" => "Italy",
172 "JE" => "Jersey",
173 "JM" => "Jamaica",
174 "JO" => "Jordan",
175 "JP" => "Japan",
176 "KE" => "Kenya",
177 "KG" => "Kyrgyzstan",
178 "KH" => "Cambodia",
179 "KI" => "Kiribati",
180 "KM" => "Comoros",
181 "KN" => "Saint Kitts and Nevis",
182 "KP" => "Korea (Democratic People's Republic of)",
183 "KR" => "Korea, Republic of",
184 "KW" => "Kuwait",
185 "KY" => "Cayman Islands",
186 "KZ" => "Kazakhstan",
187 "LA" => "Lao People's Democratic Republic",
188 "LB" => "Lebanon",
189 "LC" => "Saint Lucia",
190 "LI" => "Liechtenstein",
191 "LK" => "Sri Lanka",
192 "LR" => "Liberia",
193 "LS" => "Lesotho",
194 "LT" => "Lithuania",
195 "LU" => "Luxembourg",
196 "LV" => "Latvia",
197 "LY" => "Libya",
198 "MA" => "Morocco",
199 "MC" => "Monaco",
200 "MD" => "Moldova",
201 "ME" => "Montenegro",
202 "MF" => "Saint Martin (French part)",
203 "MG" => "Madagascar",
204 "MH" => "Marshall Islands",
205 "MK" => "North Macedonia",
206 "ML" => "Mali",
207 "MM" => "Myanmar",
208 "MN" => "Mongolia",
209 "MO" => "Macao",
210 "MP" => "Northern Mariana Islands",
211 "MQ" => "Martinique",
212 "MR" => "Mauritania",
213 "MS" => "Montserrat",
214 "MT" => "Malta",
215 "MU" => "Mauritius",
216 "MV" => "Maldives",
217 "MW" => "Malawi",
218 "MX" => "Mexico",
219 "MY" => "Malaysia",
220 "MZ" => "Mozambique",
221 "NA" => "Namibia",
222 "NC" => "New Caledonia",
223 "NE" => "Niger",
224 "NF" => "Norfolk Island",
225 "NG" => "Nigeria",
226 "NI" => "Nicaragua",
227 "NL" => "Netherlands",
228 "NO" => "Norway",
229 "NP" => "Nepal",
230 "NR" => "Nauru",
231 "NU" => "Niue",
232 "NZ" => "New Zealand",
233 "OM" => "Oman",
234 "PA" => "Panama",
235 "PE" => "Peru",
236 "PF" => "French Polynesia",
237 "PG" => "Papua New Guinea",
238 "PH" => "Philippines",
239 "PK" => "Pakistan",
240 "PL" => "Poland",
241 "PM" => "Saint Pierre and Miquelon",
242 "PN" => "Pitcairn",
243 "PR" => "Puerto Rico",
244 "PS" => "Palestine, State of",
245 "PT" => "Portugal",
246 "PW" => "Palau",
247 "PY" => "Paraguay",
248 "QA" => "Qatar",
249 "RE" => "Réunion",
250 "RO" => "Romania",
251 "RS" => "Serbia",
252 "RU" => "Russian Federation",
253 "RW" => "Rwanda",
254 "SA" => "Saudi Arabia",
255 "SB" => "Solomon Islands",
256 "SC" => "Seychelles",
257 "SD" => "Sudan",
258 "SE" => "Sweden",
259 "SG" => "Singapore",
260 "SH" => "Saint Helena, Ascension and Tristan da Cunha",
261 "SI" => "Slovenia",
262 "SJ" => "Svalbard and Jan Mayen",
263 "SK" => "Slovakia",
264 "SL" => "Sierra Leone",
265 "SM" => "San Marino",
266 "SN" => "Senegal",
267 "SO" => "Somalia",
268 "SR" => "Suriname",
269 "SS" => "South Sudan",
270 "ST" => "Sao Tome and Principe",
271 "SV" => "El Salvador",
272 "SX" => "Sint Maarten (Dutch part)",
273 "SY" => "Syrian Arab Republic",
274 "SZ" => "Eswatini",
275 "TC" => "Turks and Caicos Islands",
276 "TD" => "Chad",
277 "TF" => "French Southern Territories",
278 "TG" => "Togo",
279 "TH" => "Thailand",
280 "TJ" => "Tajikistan",
281 "TK" => "Tokelau",
282 "TL" => "Timor-Leste",
283 "TM" => "Turkmenistan",
284 "TN" => "Tunisia",
285 "TO" => "Tonga",
286 "TR" => "Turkey",
287 "TT" => "Trinidad and Tobago",
288 "TV" => "Tuvalu",
289 "TW" => "Taiwan",
290 "TZ" => "Tanzania",
291 "UA" => "Ukraine",
292 "UG" => "Uganda",
293 "UM" => "United States Minor Outlying Islands",
294 "US" => "United States of America",
295 "UY" => "Uruguay",
296 "UZ" => "Uzbekistan",
297 "VA" => "Holy See",
298 "VC" => "Saint Vincent and the Grenadines",
299 "VE" => "Venezuela",
300 "VG" => "Virgin Islands (British)",
301 "VI" => "Virgin Islands (U.S.)",
302 "VN" => "Viet Nam",
303 "VU" => "Vanuatu",
304 "WF" => "Wallis and Futuna",
305 "WS" => "Samoa",
306 "YE" => "Yemen",
307 "YT" => "Mayotte",
308 "ZA" => "South Africa",
309 "ZM" => "Zambia",
310 "ZW" => "Zimbabwe",
311 _ => "Unknown",
312 }
313 }
314}
315
316fn validate_country(input: &str) -> Result<CountryCode, GeoError> {
317 if input.is_empty() {
318 return Err(GeoError::InvalidCountry("country code is empty".to_string()));
319 }
320 if input.contains('\r') || input.contains('\n') || input.contains(' ') || input.contains('\t') {
321 return Err(GeoError::InvalidCountry(
322 "country code must not contain whitespace or control".to_string(),
323 ));
324 }
325
326 #[cfg(feature = "regex")]
327 {
328 #[cfg(feature = "std")]
329 {
330 use std::sync::OnceLock;
331 static RE: OnceLock<regex::Regex> = OnceLock::new();
332 let re = match RE.get() {
333 Some(r) => r,
334 None => {
335 let init = match regex::Regex::new(r"^[A-Z]{2}$") {
336 Ok(r) => r,
337 Err(_) => {
338 return Err(GeoError::InvalidCountry("internal regex error".to_string()))
339 }
340 };
341 let _ = RE.set(init);
342 match RE.get() {
343 Some(r) => r,
344 None => return Err(GeoError::InvalidCountry("internal regex error".to_string())),
345 }
346 }
347 };
348 if !re.is_match(input) {
349 return Err(GeoError::InvalidCountry(alloc::format!(
350 "country code '{}' must match ISO 3166-1 alpha-2 ^[A-Z]{{2}}$",
351 input
352 )));
353 }
354 }
355 #[cfg(not(feature = "std"))]
356 {
357 let re = match regex::Regex::new(r"^[A-Z]{2}$") {
358 Ok(r) => r,
359 Err(_) => return Err(GeoError::InvalidCountry("internal regex error".to_string())),
360 };
361 if !re.is_match(input) {
362 return Err(GeoError::InvalidCountry(alloc::format!(
363 "country code '{}' must match ISO 3166-1 alpha-2 ^[A-Z]{{2}}$",
364 input
365 )));
366 }
367 }
368 Ok(CountryCode(input.to_string()))
369 }
370
371 #[cfg(not(feature = "regex"))]
372 {
373 if input.len() != 2 {
374 return Err(GeoError::InvalidCountry(alloc::format!(
375 "country code '{}' must be exactly 2 characters",
376 input
377 )));
378 }
379 for ch in input.chars() {
380 if !ch.is_ascii_uppercase() {
381 return Err(GeoError::InvalidCountry(alloc::format!(
382 "country code '{}' must be 2 uppercase A-Z letters",
383 input
384 )));
385 }
386 }
387 Ok(CountryCode(input.to_string()))
388 }
389}
390
391#[must_use]
393pub fn is_valid_country_code(s: &str) -> bool {
394 validate_country(s).is_ok()
395}
396
397impl Deref for CountryCode {
398 type Target = str;
399 fn deref(&self) -> &Self::Target {
400 &self.0
401 }
402}
403
404impl fmt::Display for CountryCode {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 f.write_str(&self.0)
407 }
408}
409
410impl TryFrom<String> for CountryCode {
411 type Error = GeoError;
412 fn try_from(value: String) -> Result<Self, Self::Error> {
413 CountryCode::new(value)
414 }
415}
416
417impl TryFrom<&str> for CountryCode {
418 type Error = GeoError;
419 fn try_from(value: &str) -> Result<Self, Self::Error> {
420 CountryCode::parse(value)
421 }
422}
423
424impl FromStr for CountryCode {
425 type Err = GeoError;
426 fn from_str(s: &str) -> Result<Self, Self::Err> {
427 CountryCode::parse(s)
428 }
429}
430
431impl AsRef<str> for CountryCode {
432 fn as_ref(&self) -> &str {
433 &self.0
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn valid_codes() {
443 assert!(CountryCode::parse("GB").is_ok());
444 assert_eq!(CountryCode::parse("GB").unwrap().country_name(), "United Kingdom");
445 assert!(CountryCode::parse("US").is_ok());
446 assert_eq!(CountryCode::parse("US").unwrap().country_name(), "United States of America");
447 assert!(CountryCode::parse("DE").is_ok());
448 assert_eq!(CountryCode::parse("FR").unwrap().country_name(), "France");
449 assert!(CountryCode::parse("JP").is_ok());
450 assert!(CountryCode::parse("ZZ").is_ok()); assert_eq!(CountryCode::parse("ZZ").unwrap().country_name(), "Unknown");
452 }
453
454 #[test]
455 fn invalid_lowercase() {
456 assert!(CountryCode::parse("gb").is_err());
457 assert!(CountryCode::parse("Gb").is_err());
458 }
459
460 #[test]
461 fn invalid_length() {
462 assert!(CountryCode::parse("").is_err());
463 assert!(CountryCode::parse("G").is_err());
464 assert!(CountryCode::parse("GBR").is_err());
465 assert!(CountryCode::parse("USA").is_err());
466 }
467
468 #[test]
469 fn invalid_chars() {
470 assert!(CountryCode::parse("12").is_err());
471 assert!(CountryCode::parse("G1").is_err());
472 assert!(CountryCode::parse("G ").is_err());
473 assert!(CountryCode::parse("G\n").is_err());
474 }
475
476 #[test]
477 fn is_valid_helper() {
478 assert!(is_valid_country_code("GB"));
479 assert!(!is_valid_country_code("gb"));
480 }
481}