1use crate::error::{Severity, ValidationError};
8use crate::rules::Rule;
9
10static VALID_CURRENCIES: &[&str] = &[
15 "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT",
17 "BGN", "BHD", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD",
18 "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUP", "CVE", "CZK",
19 "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS",
20 "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IQD",
21 "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD",
22 "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT",
23 "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK",
24 "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD",
25 "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP",
26 "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS",
27 "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED", "VES", "VND", "VUV", "WST",
28 "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT",
29 "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWG",
30 "HRK", "SLL", "STD", "VEF", "MRO", "BYR",
32];
33
34pub struct CurrencyRule;
54
55impl Rule for CurrencyRule {
56 fn id(&self) -> &'static str {
57 "CURRENCY_CHECK"
58 }
59
60 fn validate(&self, value: &str, path: &str) -> Vec<ValidationError> {
61 if VALID_CURRENCIES.contains(&value) {
62 return vec![];
63 }
64
65 let msg = if value.is_empty() {
66 "Currency code must not be empty".to_owned()
67 } else if value.len() != 3 {
68 format!(
69 "Currency code must be exactly 3 characters, got {}: `{value}`",
70 value.len()
71 )
72 } else if !value.chars().all(|c| c.is_ascii_alphabetic()) {
73 format!("Currency code must be 3 alphabetic characters, got: `{value}`")
74 } else if !value.chars().all(|c| c.is_ascii_uppercase()) {
75 format!("Currency code must be uppercase, got: `{value}`")
76 } else {
77 format!("Unrecognised ISO 4217 currency code: `{value}`")
78 };
79
80 vec![ValidationError::new(
81 path,
82 Severity::Error,
83 "CURRENCY_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] = &[
96 "USD", "EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "CNY", "INR", "BRL", "MXN", "SGD",
97 "HKD", "KRW", "ZAR", "SEK", "NOK", "DKK", "PLN", "CZK", "HUF", "TRY", "THB", "MYR", "IDR",
98 "PHP", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "EGP", "ILS", "TWD", "ARS", "CLP", "COP",
99 "PEN", "NGN", "KES", "GHS", "TZS", "RUB", "UAH", "RON", "BGN", "ISK",
100 "XAU", "XAG", "XDR", "XXX", "XAF", "XOF", "HRK",
103 ];
104
105 const INVALID_CODES: &[&str] = &[
106 "usd", "Usd", "US", "USDX", "123", "U1D", "", "ZZZ", "ABC", ];
116
117 #[test]
118 fn valid_currency_codes_pass() {
119 let rule = CurrencyRule;
120 for code in VALID_CODES {
121 let errors = rule.validate(code, "/test");
122 assert!(
123 errors.is_empty(),
124 "Expected no errors for valid code `{code}`, got: {errors:?}"
125 );
126 }
127 }
128
129 #[test]
130 fn invalid_currency_codes_fail() {
131 let rule = CurrencyRule;
132 for code in INVALID_CODES {
133 let errors = rule.validate(code, "/test");
134 assert!(
135 !errors.is_empty(),
136 "Expected errors for invalid code `{code}`"
137 );
138 }
139 }
140
141 #[test]
142 fn error_has_correct_rule_id_and_path() {
143 let rule = CurrencyRule;
144 let errors = rule.validate("ABC", "/Document/Ccy");
145 assert_eq!(errors.len(), 1);
146 assert_eq!(errors[0].rule_id, "CURRENCY_CHECK");
147 assert_eq!(errors[0].path, "/Document/Ccy");
148 assert_eq!(errors[0].severity, Severity::Error);
149 }
150
151 #[test]
152 fn rule_id_is_currency_check() {
153 assert_eq!(CurrencyRule.id(), "CURRENCY_CHECK");
154 }
155
156 #[test]
157 fn lowercase_code_rejected_with_descriptive_message() {
158 let rule = CurrencyRule;
159 let errors = rule.validate("usd", "/test");
160 assert!(!errors.is_empty());
161 assert!(
162 errors[0].message.contains("uppercase") || errors[0].message.contains("Unrecognised"),
163 "Expected message about case, got: {}",
164 errors[0].message
165 );
166 }
167
168 #[test]
169 fn two_char_code_rejected_with_length_message() {
170 let rule = CurrencyRule;
171 let errors = rule.validate("US", "/test");
172 assert!(!errors.is_empty());
173 assert!(
174 errors[0].message.contains("3 characters") || errors[0].message.contains("exactly"),
175 "Expected length message, got: {}",
176 errors[0].message
177 );
178 }
179
180 #[test]
181 fn empty_code_rejected() {
182 let rule = CurrencyRule;
183 let errors = rule.validate("", "/test");
184 assert!(!errors.is_empty());
185 assert!(errors[0].message.contains("empty"));
186 }
187
188 #[test]
189 fn numeric_code_rejected() {
190 let rule = CurrencyRule;
191 let errors = rule.validate("123", "/test");
192 assert!(!errors.is_empty());
193 }
194}