use std::{borrow::Cow, fmt};
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::{FromPrimitive as _, ToPrimitive as _};
use crate::{
into_caveat, json,
warning::{self, GatherWarnings as _},
IntoCaveat, Verdict,
};
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
Decode(json::decode::WarningKind),
InvalidCase,
InvalidCode,
InvalidType,
InvalidLength,
InvalidCodeXTS,
InvalidCodeXXX,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => write!(
f,
"The currency field contains escape codes but it does not need them",
),
WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
WarningKind::InvalidCase => write!(
f,
"The currency field is lowercase but it should be uppercase",
),
WarningKind::InvalidCode => {
write!(f, "The currency field content is not a valid ISO 4217 code")
}
WarningKind::InvalidType => write!(f, "The currency field should be a string"),
WarningKind::InvalidLength => write!(f, "The currency field should be three chars"),
WarningKind::InvalidCodeXTS => write!(
f,
"The currency field contains `XTS`. This is a code for testing only",
),
WarningKind::InvalidCodeXXX => write!(
f,
"The currency field contains `XXX`. This means there is no currency",
),
}
}
}
impl warning::Kind for WarningKind {
fn id(&self) -> Cow<'static, str> {
match self {
WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
WarningKind::InvalidCase => "invalid_case".into(),
WarningKind::InvalidCode => "invalid_code".into(),
WarningKind::InvalidType => "invalid_type".into(),
WarningKind::InvalidLength => "invalid_length".into(),
WarningKind::InvalidCodeXTS => "invalid_code_xts".into(),
WarningKind::InvalidCodeXXX => "invalid_code_xxx".into(),
}
}
}
impl From<json::decode::WarningKind> for WarningKind {
fn from(warn_kind: json::decode::WarningKind) -> Self {
Self::Decode(warn_kind)
}
}
impl Code {
#[expect(
clippy::unwrap_in_result,
reason = "The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum."
)]
#[expect(
clippy::unwrap_used,
reason = "The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum."
)]
#[expect(
clippy::missing_panics_doc,
reason = "The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum."
)]
pub fn from_json(elem: &json::Element<'_>) -> Verdict<Code, WarningKind> {
let mut warnings = warning::Set::new();
let value = elem.as_value();
let Some(s) = value.as_raw_str() else {
warnings.with_elem(WarningKind::InvalidType, elem);
return Err(warnings);
};
let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
let s = match pending_str {
json::decode::PendingStr::NoEscapes(s) => s,
json::decode::PendingStr::HasEscapes(_) => {
warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
return Err(warnings);
}
};
let bytes = s.as_bytes();
let [a, b, c] = bytes else {
warnings.with_elem(WarningKind::InvalidLength, elem);
return Err(warnings);
};
let triplet: [u8; 3] = [
a.to_ascii_uppercase(),
b.to_ascii_uppercase(),
c.to_ascii_uppercase(),
];
if triplet != bytes {
warnings.with_elem(WarningKind::InvalidCase, elem);
}
let Some(index) = CURRENCIES_ALPHA3_ARRAY
.iter()
.position(|code| code.as_bytes() == triplet)
else {
warnings.with_elem(WarningKind::InvalidCode, elem);
return Err(warnings);
};
let code = Code::from_usize(index).unwrap();
if matches!(code, Code::Xts) {
warnings.with_elem(WarningKind::InvalidCodeXTS, elem);
} else if matches!(code, Code::Xxx) {
warnings.with_elem(WarningKind::InvalidCodeXXX, elem);
}
Ok(code.into_caveat(warnings))
}
#[expect(
clippy::indexing_slicing,
reason = "The CURRENCIES_ALPHA3_ARRAY is not in sync with the Code enum"
)]
pub fn into_str(self) -> &'static str {
let index = self
.to_usize()
.expect("The CURRENCIES_ALPHA3_ARRAY is in sync with the Code enum");
CURRENCIES_ALPHA3_ARRAY[index]
}
}
into_caveat!(Code);
impl fmt::Display for Code {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.into_str())
}
}
#[derive(
Clone, Copy, Debug, Eq, FromPrimitive, Ord, PartialEq, PartialOrd, serde::Serialize, ToPrimitive,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum Code {
Aed,
Afn,
All,
Amd,
Ang,
Aoa,
Ars,
Aud,
Awg,
Azn,
Bam,
Bbd,
Bdt,
Bgn,
Bhd,
Bif,
Bmd,
Bnd,
Bob,
Bov,
Brl,
Bsd,
Btn,
Bwp,
Byn,
Bzd,
Cad,
Cdf,
Che,
Chf,
Chw,
Clf,
Clp,
Cny,
Cop,
Cou,
Crc,
Cuc,
Cup,
Cve,
Czk,
Djf,
Dkk,
Dop,
Dzd,
Egp,
Ern,
Etb,
Eur,
Fjd,
Fkp,
Gbp,
Gel,
Ghs,
Gip,
Gmd,
Gnf,
Gtq,
Gyd,
Hkd,
Hnl,
Hrk,
Htg,
Huf,
Idr,
Ils,
Inr,
Iqd,
Irr,
Isk,
Jmd,
Jod,
Jpy,
Kes,
Kgs,
Khr,
Kmf,
Kpw,
Krw,
Kwd,
Kyd,
Kzt,
Lak,
Lbp,
Lkr,
Lrd,
Lsl,
Lyd,
Mad,
Mdl,
Mga,
Mkd,
Mmk,
Mnt,
Mop,
Mru,
Mur,
Mvr,
Mwk,
Mxn,
Mxv,
Myr,
Mzn,
Nad,
Ngn,
Nio,
Nok,
Npr,
Nzd,
Omr,
Pab,
Pen,
Pgk,
Php,
Pkr,
Pln,
Pyg,
Qar,
Ron,
Rsd,
Rub,
Rwf,
Sar,
Sbd,
Scr,
Sdg,
Sek,
Sgd,
Shp,
Sle,
Sll,
Sos,
Srd,
Ssp,
Stn,
Svc,
Syp,
Szl,
Thb,
Tjs,
Tmt,
Tnd,
Top,
Try,
Ttd,
Twd,
Tzs,
Uah,
Ugx,
Usd,
Usn,
Uyi,
Uyu,
Uyw,
Uzs,
Ved,
Ves,
Vnd,
Vuv,
Wst,
Xaf,
Xag,
Xau,
Xba,
Xbb,
Xbc,
Xbd,
Xcd,
Xdr,
Xof,
Xpd,
Xpf,
Xpt,
Xsu,
Xts,
Xua,
Xxx,
Yer,
Zar,
Zmw,
Zwl,
}
pub(crate) const CURRENCIES_ALPHA3_ARRAY: [&str; 181] = [
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT",
"BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
"CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP",
"CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP",
"GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR",
"ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW",
"KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA",
"MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN", "NAD",
"NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
"QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE",
"SLL", "SOS", "SRD", "SSP", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP",
"TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS", "VED",
"VES", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR",
"XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX", "YER", "ZAR", "ZMW", "ZWL",
];
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
use crate::{json, Verdict};
use super::{Code, WarningKind};
#[test]
fn should_create_currency_without_issue() {
const JSON: &str = r#"{ "currency": "EUR" }"#;
let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
assert_eq!(Code::Eur, code);
assert_matches!(*warnings, []);
}
#[test]
fn should_raise_currency_content_issue() {
const JSON: &str = r#"{ "currency": "VVV" }"#;
let warnings = parse_code_from_json(JSON).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidCode]);
}
#[test]
fn should_raise_currency_case_issue() {
const JSON: &str = r#"{ "currency": "eur" }"#;
let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
assert_eq!(code, Code::Eur);
assert_matches!(*warnings, [WarningKind::InvalidCase]);
}
#[test]
fn should_raise_currency_xts_issue() {
const JSON: &str = r#"{ "currency": "xts" }"#;
let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
assert_eq!(code, Code::Xts);
assert_matches!(
*warnings,
[WarningKind::InvalidCase, WarningKind::InvalidCodeXTS]
);
}
#[test]
fn should_raise_currency_xxx_issue() {
const JSON: &str = r#"{ "currency": "xxx" }"#;
let (code, warnings) = parse_code_from_json(JSON).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
assert_eq!(code, Code::Xxx);
assert_matches!(
*warnings,
[WarningKind::InvalidCase, WarningKind::InvalidCodeXXX]
);
}
#[track_caller]
fn parse_code_from_json(json: &str) -> Verdict<Code, WarningKind> {
let json = json::parse(json).unwrap();
let currency_elem = json.find_field("currency").unwrap();
Code::from_json(currency_elem.element())
}
}