1use std::fmt;
3
4use num_derive::{FromPrimitive, ToPrimitive};
5use num_traits::{FromPrimitive as _, ToPrimitive as _};
6
7use crate::{
8 from_warning_all, into_caveat, json,
9 warning::{self, GatherWarnings as _},
10 IntoCaveat, Verdict,
11};
12
13#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
15pub enum Warning {
16 ContainsEscapeCodes,
18
19 Decode(json::decode::Warning),
21
22 PreferUpperCase,
24
25 InvalidCode,
27
28 InvalidType,
30
31 InvalidLength,
33
34 InvalidCodeXTS,
36
37 InvalidCodeXXX,
39}
40
41impl fmt::Display for Warning {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::ContainsEscapeCodes => write!(
45 f,
46 "The currency-code contains escape-codes but it does not need them.",
47 ),
48 Self::Decode(warning) => fmt::Display::fmt(warning, f),
49 Self::PreferUpperCase => write!(
50 f,
51 "The currency-code follows the ISO 4217 standard whch states: the chars should be uppercase.",
52 ),
53 Self::InvalidCode => {
54 write!(f, "The currency-code is not a valid ISO 4217 code.")
55 }
56 Self::InvalidType => write!(f, "The currency-code should be a string."),
57 Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard whch states: the code should be three chars."),
58 Self::InvalidCodeXTS => write!(
59 f,
60 "The currency-code is `XTS`. This is a code for testing only",
61 ),
62 Self::InvalidCodeXXX => write!(
63 f,
64 "The currency-code is `XXX`. This means there is no currency",
65 ),
66 }
67 }
68}
69
70impl crate::Warning for Warning {
71 fn id(&self) -> crate::SmartString {
72 match self {
73 Self::ContainsEscapeCodes => "contains_escape_codes".into(),
74 Self::Decode(kind) => kind.id(),
75 Self::PreferUpperCase => "prefer_upper_case".into(),
76 Self::InvalidCode => "invalid_code".into(),
77 Self::InvalidType => "invalid_type".into(),
78 Self::InvalidLength => "invalid_length".into(),
79 Self::InvalidCodeXTS => "invalid_code_xts".into(),
80 Self::InvalidCodeXXX => "invalid_code_xxx".into(),
81 }
82 }
83}
84
85from_warning_all!(json::decode::Warning => Warning::Decode);
86
87impl json::FromJson<'_, '_> for Code {
88 type Warning = Warning;
89
90 #[expect(
91 clippy::unwrap_used,
92 reason = "The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum."
93 )]
94 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
95 let mut warnings = warning::Set::new();
96 let value = elem.as_value();
97
98 let Some(s) = value.as_raw_str() else {
99 return warnings.bail(Warning::InvalidType, elem);
100 };
101
102 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
103
104 let s = match pending_str {
105 json::decode::PendingStr::NoEscapes(s) => s,
106 json::decode::PendingStr::HasEscapes(_) => {
107 return warnings.bail(Warning::ContainsEscapeCodes, elem);
108 }
109 };
110
111 let bytes = s.as_bytes();
112
113 let [a, b, c] = bytes else {
115 return warnings.bail(Warning::InvalidLength, elem);
116 };
117
118 let triplet: [u8; 3] = [
119 a.to_ascii_uppercase(),
120 b.to_ascii_uppercase(),
121 c.to_ascii_uppercase(),
122 ];
123
124 if triplet != bytes {
125 warnings.with_elem(Warning::PreferUpperCase, elem);
126 }
127
128 let Some(index) = CURRENCIES_ALPHA3_ARRAY
129 .iter()
130 .position(|code| code.as_bytes() == triplet)
131 else {
132 return warnings.bail(Warning::InvalidCode, elem);
133 };
134
135 let code = Code::from_usize(index).unwrap();
136
137 if matches!(code, Code::Xts) {
138 warnings.with_elem(Warning::InvalidCodeXTS, elem);
139 } else if matches!(code, Code::Xxx) {
140 warnings.with_elem(Warning::InvalidCodeXXX, elem);
141 }
142
143 Ok(code.into_caveat(warnings))
144 }
145}
146
147impl Code {
148 #[expect(
154 clippy::indexing_slicing,
155 reason = "The CURRENCIES_ALPHA3_ARRAY is not in sync with the Code enum"
156 )]
157 pub fn into_str(self) -> &'static str {
158 let index = self
159 .to_usize()
160 .expect("The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum");
161 CURRENCIES_ALPHA3_ARRAY[index]
162 }
163}
164
165into_caveat!(Code);
166
167impl fmt::Display for Code {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 f.write_str(self.into_str())
170 }
171}
172
173#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Ord, PartialEq, PartialOrd, ToPrimitive)]
175pub enum Code {
176 Aed,
177 Afn,
178 All,
179 Amd,
180 Ang,
181 Aoa,
182 Ars,
183 Aud,
184 Awg,
185 Azn,
186 Bam,
187 Bbd,
188 Bdt,
189 Bgn,
190 Bhd,
191 Bif,
192 Bmd,
193 Bnd,
194 Bob,
195 Bov,
196 Brl,
197 Bsd,
198 Btn,
199 Bwp,
200 Byn,
201 Bzd,
202 Cad,
203 Cdf,
204 Che,
205 Chf,
206 Chw,
207 Clf,
208 Clp,
209 Cny,
210 Cop,
211 Cou,
212 Crc,
213 Cuc,
214 Cup,
215 Cve,
216 Czk,
217 Djf,
218 Dkk,
219 Dop,
220 Dzd,
221 Egp,
222 Ern,
223 Etb,
224 Eur,
225 Fjd,
226 Fkp,
227 Gbp,
228 Gel,
229 Ghs,
230 Gip,
231 Gmd,
232 Gnf,
233 Gtq,
234 Gyd,
235 Hkd,
236 Hnl,
237 Hrk,
238 Htg,
239 Huf,
240 Idr,
241 Ils,
242 Inr,
243 Iqd,
244 Irr,
245 Isk,
246 Jmd,
247 Jod,
248 Jpy,
249 Kes,
250 Kgs,
251 Khr,
252 Kmf,
253 Kpw,
254 Krw,
255 Kwd,
256 Kyd,
257 Kzt,
258 Lak,
259 Lbp,
260 Lkr,
261 Lrd,
262 Lsl,
263 Lyd,
264 Mad,
265 Mdl,
266 Mga,
267 Mkd,
268 Mmk,
269 Mnt,
270 Mop,
271 Mru,
272 Mur,
273 Mvr,
274 Mwk,
275 Mxn,
276 Mxv,
277 Myr,
278 Mzn,
279 Nad,
280 Ngn,
281 Nio,
282 Nok,
283 Npr,
284 Nzd,
285 Omr,
286 Pab,
287 Pen,
288 Pgk,
289 Php,
290 Pkr,
291 Pln,
292 Pyg,
293 Qar,
294 Ron,
295 Rsd,
296 Rub,
297 Rwf,
298 Sar,
299 Sbd,
300 Scr,
301 Sdg,
302 Sek,
303 Sgd,
304 Shp,
305 Sle,
306 Sll,
307 Sos,
308 Srd,
309 Ssp,
310 Stn,
311 Svc,
312 Syp,
313 Szl,
314 Thb,
315 Tjs,
316 Tmt,
317 Tnd,
318 Top,
319 Try,
320 Ttd,
321 Twd,
322 Tzs,
323 Uah,
324 Ugx,
325 Usd,
326 Usn,
327 Uyi,
328 Uyu,
329 Uyw,
330 Uzs,
331 Ved,
332 Ves,
333 Vnd,
334 Vuv,
335 Wst,
336 Xaf,
337 Xag,
338 Xau,
339 Xba,
340 Xbb,
341 Xbc,
342 Xbd,
343 Xcd,
344 Xdr,
345 Xof,
346 Xpd,
347 Xpf,
348 Xpt,
349 Xsu,
350 Xts,
351 Xua,
352 Xxx,
353 Yer,
354 Zar,
355 Zmw,
356 Zwl,
357}
358
359pub(crate) const CURRENCIES_ALPHA3_ARRAY: [&str; 181] = [
361 "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT",
362 "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
363 "CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP",
364 "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP",
365 "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR",
366 "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW",
367 "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA",
368 "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD",
369 "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
370 "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE",
371 "SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP",
372 "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED",
373 "VES", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR",
374 "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL",
375];
376
377#[cfg(test)]
378mod test {
379 #![allow(
380 clippy::unwrap_in_result,
381 reason = "unwraps are allowed anywhere in tests"
382 )]
383 #![allow(
384 clippy::indexing_slicing,
385 reason = "unwraps are allowed anywhere in tests"
386 )]
387
388 use assert_matches::assert_matches;
389
390 use crate::{
391 json::{self, FromJson as _},
392 warning::test::VerdictTestExt,
393 Verdict,
394 };
395
396 use super::{Code, Warning};
397
398 #[test]
399 fn should_create_currency_without_issue() {
400 const JSON: &str = r#"{ "currency": "EUR" }"#;
401
402 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
403
404 assert_eq!(Code::Eur, code);
405 assert!(warnings.is_empty(), "{warnings:#?}");
406 }
407
408 #[test]
409 fn should_raise_currency_content_issue() {
410 const JSON: &str = r#"{ "currency": "VVV" }"#;
411
412 let error = parse_code_from_json(JSON).unwrap_only_error();
413
414 assert_matches!(error.into_warning(), Warning::InvalidCode);
415 }
416
417 #[test]
418 fn should_raise_currency_case_issue() {
419 const JSON: &str = r#"{ "currency": "eur" }"#;
420
421 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
422 let warnings = warnings.path_map();
423 let warnings = warnings["$.currency"];
424
425 assert_eq!(code, Code::Eur);
426 assert_matches!(warnings, [Warning::PreferUpperCase]);
427 }
428
429 #[test]
430 fn should_raise_currency_xts_issue() {
431 const JSON: &str = r#"{ "currency": "xts" }"#;
432
433 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
434 let warnings = warnings.path_map();
435 let warnings = warnings["$.currency"];
436
437 assert_eq!(code, Code::Xts);
438 assert_matches!(
439 warnings,
440 [Warning::PreferUpperCase, Warning::InvalidCodeXTS]
441 );
442 }
443
444 #[test]
445 fn should_raise_currency_xxx_issue() {
446 const JSON: &str = r#"{ "currency": "xxx" }"#;
447
448 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
449 let warnings = warnings.path_map();
450 let warnings = warnings["$.currency"];
451
452 assert_eq!(code, Code::Xxx);
453 assert_matches!(
454 warnings,
455 [Warning::PreferUpperCase, Warning::InvalidCodeXXX]
456 );
457 }
458
459 #[track_caller]
460 fn parse_code_from_json(json: &str) -> Verdict<Code, Warning> {
461 let json = json::parse(json).unwrap();
462 let currency_elem = json.find_field("currency").unwrap();
463 Code::from_json(currency_elem.element())
464 }
465}