1use crate::error::{Severity, ValidationError};
10use crate::rules::Rule;
11
12static VALID_COUNTRIES: &[&str] = &[
14 "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
15 "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS",
16 "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
17 "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE",
18 "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF",
19 "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
20 "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
21 "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
22 "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
23 "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA",
24 "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG",
25 "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
26 "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS",
27 "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
28 "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
29 "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
30 "XK", ];
33
34pub struct CountryCodeRule;
54
55impl Rule for CountryCodeRule {
56 fn id(&self) -> &'static str {
57 "COUNTRY_CHECK"
58 }
59
60 fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
61 if VALID_COUNTRIES.contains(&value) {
62 return vec![];
63 }
64
65 let msg = if value.is_empty() {
66 "Country code must not be empty".to_owned()
67 } else if value.len() != 2 {
68 format!(
69 "Country code must be exactly 2 characters, got {}: `{value}`",
70 value.len()
71 )
72 } else if !value.chars().all(|c| c.is_ascii_alphabetic()) {
73 format!("Country code must be 2 alphabetic characters, got: `{value}`")
74 } else if !value.chars().all(|c| c.is_ascii_uppercase()) {
75 format!("Country code must be uppercase, got: `{value}`")
76 } else {
77 format!("Unrecognised ISO 3166-1 alpha-2 country code: `{value}`")
78 };
79
80 vec![ValidationError::new(
81 path,
82 Severity::Error,
83 "COUNTRY_CHECK",
84 msg,
85 )]
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::rules::Rule;
93
94 const VALID_CODES: &[&str] = &[
95 "US", "GB", "DE", "FR", "JP", "CN", "AU", "CA", "IN", "BR", "MX", "RU", "KR", "ZA", "SG",
96 "HK", "NL", "SE", "NO", "DK", "FI", "PL", "CZ", "HU", "RO", "UA", "TR", "SA", "AE", "QA",
97 "KW", "BH", "OM", "EG", "NG", "KE", "GH", "TZ", "MY", "ID", "TH", "PH", "VN", "PK", "BD",
98 "NZ", "CH", "AT", "BE", "IE", "PT", "ES", "IT", "GR", "IL", "AR", "CL", "CO", "PE", "VE",
99 "XK", ];
101
102 const INVALID_CODES: &[&str] = &[
103 "us", "Gb", "USA", "U", "12", "U1", "", "XX", "AA", "QQ", ];
114
115 #[test]
116 fn valid_country_codes_pass() {
117 let rule = CountryCodeRule;
118 for code in VALID_CODES {
119 let errors = rule.validate(code, "/test");
120 assert!(
121 errors.is_empty(),
122 "Expected no errors for valid code `{code}`, got: {errors:?}"
123 );
124 }
125 }
126
127 #[test]
128 fn invalid_country_codes_fail() {
129 let rule = CountryCodeRule;
130 for code in INVALID_CODES {
131 let errors = rule.validate(code, "/test");
132 assert!(
133 !errors.is_empty(),
134 "Expected errors for invalid code `{code}`"
135 );
136 }
137 }
138
139 #[test]
140 fn error_has_correct_rule_id_and_path() {
141 let rule = CountryCodeRule;
142 let errors = rule.validate("XX", "/Document/Ctry");
143 assert_eq!(errors.len(), 1);
144 assert_eq!(errors[0].rule_id, "COUNTRY_CHECK");
145 assert_eq!(errors[0].path, "/Document/Ctry");
146 assert_eq!(errors[0].severity, Severity::Error);
147 }
148
149 #[test]
150 fn rule_id_is_country_check() {
151 assert_eq!(CountryCodeRule.id(), "COUNTRY_CHECK");
152 }
153
154 #[test]
155 fn empty_code_rejected() {
156 let rule = CountryCodeRule;
157 let errors = rule.validate("", "/test");
158 assert!(!errors.is_empty());
159 assert!(errors[0].message.contains("empty"));
160 }
161
162 #[test]
163 fn three_char_code_rejected_with_length_message() {
164 let rule = CountryCodeRule;
165 let errors = rule.validate("USA", "/test");
166 assert!(!errors.is_empty());
167 assert!(
168 errors[0].message.contains("2 characters") || errors[0].message.contains("exactly"),
169 "Expected length message, got: {}",
170 errors[0].message
171 );
172 }
173
174 #[test]
175 fn lowercase_code_rejected() {
176 let rule = CountryCodeRule;
177 let errors = rule.validate("us", "/test");
178 assert!(!errors.is_empty());
179 }
180}