1use std::{borrow::Cow, 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 WarningKind {
16 ContainsEscapeCodes,
18
19 Decode(json::decode::WarningKind),
21
22 PreferUpperCase,
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 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 warning::Kind for WarningKind {
71 fn id(&self) -> Cow<'static, str> {
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::WarningKind => WarningKind::Decode);
86
87impl json::FromJson<'_, '_> for Code {
88 type WarningKind = WarningKind;
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::WarningKind> {
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(WarningKind::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(WarningKind::ContainsEscapeCodes, elem);
108 }
109 };
110
111 let bytes = s.as_bytes();
112
113 let [a, b, c] = bytes else {
115 return warnings.bail(WarningKind::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(WarningKind::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(WarningKind::InvalidCode, elem);
133 };
134
135 let code = Code::from_usize(index).unwrap();
136
137 if matches!(code, Code::Xts) {
138 warnings.with_elem(WarningKind::InvalidCodeXTS, elem);
139 } else if matches!(code, Code::Xxx) {
140 warnings.with_elem(WarningKind::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
384 use assert_matches::assert_matches;
385
386 use crate::{
387 json::{self, FromJson as _},
388 Verdict,
389 };
390
391 use super::{Code, WarningKind};
392
393 #[test]
394 fn should_create_currency_without_issue() {
395 const JSON: &str = r#"{ "currency": "EUR" }"#;
396
397 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
398
399 assert_eq!(Code::Eur, code);
400 assert_matches!(*warnings, []);
401 }
402
403 #[test]
404 fn should_raise_currency_content_issue() {
405 const JSON: &str = r#"{ "currency": "VVV" }"#;
406
407 let warnings = parse_code_from_json(JSON).unwrap_err().into_kind_vec();
408
409 assert_matches!(*warnings, [WarningKind::InvalidCode]);
410 }
411
412 #[test]
413 fn should_raise_currency_case_issue() {
414 const JSON: &str = r#"{ "currency": "eur" }"#;
415
416 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
417 let warnings = warnings.into_kind_vec();
418
419 assert_eq!(code, Code::Eur);
420 assert_matches!(*warnings, [WarningKind::PreferUpperCase]);
421 }
422
423 #[test]
424 fn should_raise_currency_xts_issue() {
425 const JSON: &str = r#"{ "currency": "xts" }"#;
426
427 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
428 let warnings = warnings.into_kind_vec();
429
430 assert_eq!(code, Code::Xts);
431 assert_matches!(
432 *warnings,
433 [WarningKind::PreferUpperCase, WarningKind::InvalidCodeXTS]
434 );
435 }
436
437 #[test]
438 fn should_raise_currency_xxx_issue() {
439 const JSON: &str = r#"{ "currency": "xxx" }"#;
440
441 let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
442 let warnings = warnings.into_kind_vec();
443
444 assert_eq!(code, Code::Xxx);
445 assert_matches!(
446 *warnings,
447 [WarningKind::PreferUpperCase, WarningKind::InvalidCodeXXX]
448 );
449 }
450
451 #[track_caller]
452 fn parse_code_from_json(json: &str) -> Verdict<Code, WarningKind> {
453 let json = json::parse(json).unwrap();
454 let currency_elem = json.find_field("currency").unwrap();
455 Code::from_json(currency_elem.element())
456 }
457}