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