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