use std::{borrow::Cow, fmt};
use crate::{
into_caveat, json,
warning::{self, GatherWarnings as _},
IntoCaveat, Verdict,
};
const RESERVED_PREFIX: u8 = b'x';
const ALPHA_2_LEN: usize = 2;
const ALPHA_3_LEN: usize = 3;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum WarningKind {
ContainsEscapeCodes,
Decode(json::decode::WarningKind),
InvalidCase,
InvalidCode,
InvalidType,
InvalidLength,
InvalidReserved,
}
impl fmt::Display for WarningKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WarningKind::ContainsEscapeCodes => write!(f, "The value contains escape codes but it does not need them"),
WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
WarningKind::InvalidCase => write!(f, "The value is not a valid ISO 3166-1 country code because it's not uppercase."),
WarningKind::InvalidCode => write!(f, "The value field is not a valid ISO 3166 currency code."),
WarningKind::InvalidType => write!(f, "The value should be a string."),
WarningKind::InvalidLength => write!(f, "The value is not a valid ISO 3166-1 country code because it's not 2 or 3 chars in length."),
WarningKind::InvalidReserved => write!(f, "The value is not a valid ISO 3166-1 alpha-2 country code because it's all codes beginning with 'X' are reserved."),
}
}
}
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::InvalidReserved => "invalid_reserved".into(),
}
}
}
#[derive(Debug)]
pub(crate) enum CodeSet {
Alpha2(Code),
Alpha3(Code),
}
into_caveat!(CodeSet);
into_caveat!(Code);
impl From<json::decode::WarningKind> for WarningKind {
fn from(warn_kind: json::decode::WarningKind) -> Self {
Self::Decode(warn_kind)
}
}
impl json::FromJson<'_, '_> for CodeSet {
type WarningKind = WarningKind;
fn from_json(elem: &json::Element<'_>) -> Verdict<CodeSet, Self::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();
if let [a, b, c] = bytes {
let triplet: [u8; ALPHA_3_LEN] = [
a.to_ascii_uppercase(),
b.to_ascii_uppercase(),
c.to_ascii_uppercase(),
];
if triplet != bytes {
warnings.with_elem(WarningKind::InvalidCase, elem);
}
if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
warnings.with_elem(WarningKind::InvalidReserved, elem);
}
let Some(code) = Code::from_alpha_3(triplet) else {
warnings.with_elem(WarningKind::InvalidCode, elem);
return Err(warnings);
};
Ok(CodeSet::Alpha3(code).into_caveat(warnings))
} else if let [a, b] = bytes {
let pair: [u8; ALPHA_2_LEN] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];
if pair != bytes {
warnings.with_elem(WarningKind::InvalidCase, elem);
}
if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
warnings.with_elem(WarningKind::InvalidReserved, elem);
}
let Some(code) = Code::from_alpha_2(pair) else {
warnings.with_elem(WarningKind::InvalidCode, elem);
return Err(warnings);
};
Ok(CodeSet::Alpha2(code).into_caveat(warnings))
} else {
warnings.with_elem(WarningKind::InvalidLength, elem);
Err(warnings)
}
}
}
impl Code {
pub(crate) fn into_str(self) -> &'static str {
self.into_alpha_2_raw()
}
}
impl From<Code> for &'static json::RawValue {
fn from(code: Code) -> Self {
serde_json::from_str(code.into_alpha_2_raw())
.expect("All country codes are valid JSON strings")
}
}
impl fmt::Display for Code {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.into_str())
}
}
macro_rules! country_codes {
[$(($name:ident, $alph2:literal, $alph3:literal)),*] => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
pub enum Code {
$($name),*
}
impl Code {
const fn from_alpha_2(code: [u8; 2]) -> Option<Self> {
match &code {
$($alph2 => Some(Self::$name),)*
_ => None
}
}
const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
match &code {
$($alph3 => Some(Self::$name),)*
_ => None
}
}
fn into_alpha_2_raw(self) -> &'static str {
let bytes = match self {
$(Self::$name => $alph2),*
};
std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
}
}
};
}
country_codes![
(Ad, b"AD", b"AND"),
(Ae, b"AE", b"ARE"),
(Af, b"AF", b"AFG"),
(Ag, b"AG", b"ATG"),
(Ai, b"AI", b"AIA"),
(Al, b"AL", b"ALB"),
(Am, b"AM", b"ARM"),
(Ao, b"AO", b"AGO"),
(Aq, b"AQ", b"ATA"),
(Ar, b"AR", b"ARG"),
(As, b"AS", b"ASM"),
(At, b"AT", b"AUT"),
(Au, b"AU", b"AUS"),
(Aw, b"AW", b"ABW"),
(Ax, b"AX", b"ALA"),
(Az, b"AZ", b"AZE"),
(Ba, b"BA", b"BIH"),
(Bb, b"BB", b"BRB"),
(Bd, b"BD", b"BGD"),
(Be, b"BE", b"BEL"),
(Bf, b"BF", b"BFA"),
(Bg, b"BG", b"BGR"),
(Bh, b"BH", b"BHR"),
(Bi, b"BI", b"BDI"),
(Bj, b"BJ", b"BEN"),
(Bl, b"BL", b"BLM"),
(Bm, b"BM", b"BMU"),
(Bn, b"BN", b"BRN"),
(Bo, b"BO", b"BOL"),
(Bq, b"BQ", b"BES"),
(Br, b"BR", b"BRA"),
(Bs, b"BS", b"BHS"),
(Bt, b"BT", b"BTN"),
(Bv, b"BV", b"BVT"),
(Bw, b"BW", b"BWA"),
(By, b"BY", b"BLR"),
(Bz, b"BZ", b"BLZ"),
(Ca, b"CA", b"CAN"),
(Cc, b"CC", b"CCK"),
(Cd, b"CD", b"COD"),
(Cf, b"CF", b"CAF"),
(Cg, b"CG", b"COG"),
(Ch, b"CH", b"CHE"),
(Ci, b"CI", b"CIV"),
(Ck, b"CK", b"COK"),
(Cl, b"CL", b"CHL"),
(Cm, b"CM", b"CMR"),
(Cn, b"CN", b"CHN"),
(Co, b"CO", b"COL"),
(Cr, b"CR", b"CRI"),
(Cu, b"CU", b"CUB"),
(Cv, b"CV", b"CPV"),
(Cw, b"CW", b"CUW"),
(Cx, b"CX", b"CXR"),
(Cy, b"CY", b"CYP"),
(Cz, b"CZ", b"CZE"),
(De, b"DE", b"DEU"),
(Dj, b"DJ", b"DJI"),
(Dk, b"DK", b"DNK"),
(Dm, b"DM", b"DMA"),
(Do, b"DO", b"DOM"),
(Dz, b"DZ", b"DZA"),
(Ec, b"EC", b"ECU"),
(Ee, b"EE", b"EST"),
(Eg, b"EG", b"EGY"),
(Eh, b"EH", b"ESH"),
(Er, b"ER", b"ERI"),
(Es, b"ES", b"ESP"),
(Et, b"ET", b"ETH"),
(Fi, b"FI", b"FIN"),
(Fj, b"FJ", b"FJI"),
(Fk, b"FK", b"FLK"),
(Fm, b"FM", b"FSM"),
(Fo, b"FO", b"FRO"),
(Fr, b"FR", b"FRA"),
(Ga, b"GA", b"GAB"),
(Gb, b"GB", b"GBR"),
(Gd, b"GD", b"GRD"),
(Ge, b"GE", b"GEO"),
(Gf, b"GF", b"GUF"),
(Gg, b"GG", b"GGY"),
(Gh, b"GH", b"GHA"),
(Gi, b"GI", b"GIB"),
(Gl, b"GL", b"GRL"),
(Gm, b"GM", b"GMB"),
(Gn, b"GN", b"GIN"),
(Gp, b"GP", b"GLP"),
(Gq, b"GQ", b"GNQ"),
(Gr, b"GR", b"GRC"),
(Gs, b"GS", b"SGS"),
(Gt, b"GT", b"GTM"),
(Gu, b"GU", b"GUM"),
(Gw, b"GW", b"GNB"),
(Gy, b"GY", b"GUY"),
(Hk, b"HK", b"HKG"),
(Hm, b"HM", b"HMD"),
(Hn, b"HN", b"HND"),
(Hr, b"HR", b"HRV"),
(Ht, b"HT", b"HTI"),
(Hu, b"HU", b"HUN"),
(Id, b"ID", b"IDN"),
(Ie, b"IE", b"IRL"),
(Il, b"IL", b"ISR"),
(Im, b"IM", b"IMN"),
(In, b"IN", b"IND"),
(Io, b"IO", b"IOT"),
(Iq, b"IQ", b"IRQ"),
(Ir, b"IR", b"IRN"),
(Is, b"IS", b"ISL"),
(It, b"IT", b"ITA"),
(Je, b"JE", b"JEY"),
(Jm, b"JM", b"JAM"),
(Jo, b"JO", b"JOR"),
(Jp, b"JP", b"JPN"),
(Ke, b"KE", b"KEN"),
(Kg, b"KG", b"KGZ"),
(Kh, b"KH", b"KHM"),
(Ki, b"KI", b"KIR"),
(Km, b"KM", b"COM"),
(Kn, b"KN", b"KNA"),
(Kp, b"KP", b"PRK"),
(Kr, b"KR", b"KOR"),
(Kw, b"KW", b"KWT"),
(Ky, b"KY", b"CYM"),
(Kz, b"KZ", b"KAZ"),
(La, b"LA", b"LAO"),
(Lb, b"LB", b"LBN"),
(Lc, b"LC", b"LCA"),
(Li, b"LI", b"LIE"),
(Lk, b"LK", b"LKA"),
(Lr, b"LR", b"LBR"),
(Ls, b"LS", b"LSO"),
(Lt, b"LT", b"LTU"),
(Lu, b"LU", b"LUX"),
(Lv, b"LV", b"LVA"),
(Ly, b"LY", b"LBY"),
(Ma, b"MA", b"MAR"),
(Mc, b"MC", b"MCO"),
(Md, b"MD", b"MDA"),
(Me, b"ME", b"MNE"),
(Mf, b"MF", b"MAF"),
(Mg, b"MG", b"MDG"),
(Mh, b"MH", b"MHL"),
(Mk, b"MK", b"MKD"),
(Ml, b"ML", b"MLI"),
(Mm, b"MM", b"MMR"),
(Mn, b"MN", b"MNG"),
(Mo, b"MO", b"MAC"),
(Mp, b"MP", b"MNP"),
(Mq, b"MQ", b"MTQ"),
(Mr, b"MR", b"MRT"),
(Ms, b"MS", b"MSR"),
(Mt, b"MT", b"MLT"),
(Mu, b"MU", b"MUS"),
(Mv, b"MV", b"MDV"),
(Mw, b"MW", b"MWI"),
(Mx, b"MX", b"MEX"),
(My, b"MY", b"MYS"),
(Mz, b"MZ", b"MOZ"),
(Na, b"NA", b"NAM"),
(Nc, b"NC", b"NCL"),
(Ne, b"NE", b"NER"),
(Nf, b"NF", b"NFK"),
(Ng, b"NG", b"NGA"),
(Ni, b"NI", b"NIC"),
(Nl, b"NL", b"NLD"),
(No, b"NO", b"NOR"),
(Np, b"NP", b"NPL"),
(Nr, b"NR", b"NRU"),
(Nu, b"NU", b"NIU"),
(Nz, b"NZ", b"NZL"),
(Om, b"OM", b"OMN"),
(Pa, b"PA", b"PAN"),
(Pe, b"PE", b"PER"),
(Pf, b"PF", b"PYF"),
(Pg, b"PG", b"PNG"),
(Ph, b"PH", b"PHL"),
(Pk, b"PK", b"PAK"),
(Pl, b"PL", b"POL"),
(Pm, b"PM", b"SPM"),
(Pn, b"PN", b"PCN"),
(Pr, b"PR", b"PRI"),
(Ps, b"PS", b"PSE"),
(Pt, b"PT", b"PRT"),
(Pw, b"PW", b"PLW"),
(Py, b"PY", b"PRY"),
(Qa, b"QA", b"QAT"),
(Re, b"RE", b"REU"),
(Ro, b"RO", b"ROU"),
(Rs, b"RS", b"SRB"),
(Ru, b"RU", b"RUS"),
(Rw, b"RW", b"RWA"),
(Sa, b"SA", b"SAU"),
(Sb, b"SB", b"SLB"),
(Sc, b"SC", b"SYC"),
(Sd, b"SD", b"SDN"),
(Se, b"SE", b"SWE"),
(Sg, b"SG", b"SGP"),
(Sh, b"SH", b"SHN"),
(Si, b"SI", b"SVN"),
(Sj, b"SJ", b"SJM"),
(Sk, b"SK", b"SVK"),
(Sl, b"SL", b"SLE"),
(Sm, b"SM", b"SMR"),
(Sn, b"SN", b"SEN"),
(So, b"SO", b"SOM"),
(Sr, b"SR", b"SUR"),
(Ss, b"SS", b"SSD"),
(St, b"ST", b"STP"),
(Sv, b"SV", b"SLV"),
(Sx, b"SX", b"SXM"),
(Sy, b"SY", b"SYR"),
(Sz, b"SZ", b"SWZ"),
(Tc, b"TC", b"TCA"),
(Td, b"TD", b"TCD"),
(Tf, b"TF", b"ATF"),
(Tg, b"TG", b"TGO"),
(Th, b"TH", b"THA"),
(Tj, b"TJ", b"TJK"),
(Tk, b"TK", b"TKL"),
(Tl, b"TL", b"TLS"),
(Tm, b"TM", b"TKM"),
(Tn, b"TN", b"TUN"),
(To, b"TO", b"TON"),
(Tr, b"TR", b"TUR"),
(Tt, b"TT", b"TTO"),
(Tv, b"TV", b"TUV"),
(Tw, b"TW", b"TWN"),
(Tz, b"TZ", b"TZA"),
(Ua, b"UA", b"UKR"),
(Ug, b"UG", b"UGA"),
(Um, b"UM", b"UMI"),
(Us, b"US", b"USA"),
(Uy, b"UY", b"URY"),
(Uz, b"UZ", b"UZB"),
(Va, b"VA", b"VAT"),
(Vc, b"VC", b"VCT"),
(Ve, b"VE", b"VEN"),
(Vg, b"VG", b"VGB"),
(Vi, b"VI", b"VIR"),
(Vn, b"VN", b"VNM"),
(Vu, b"VU", b"VUT"),
(Wf, b"WF", b"WLF"),
(Ws, b"WS", b"WSM"),
(Ye, b"YE", b"YEM"),
(Yt, b"YT", b"MYT"),
(Za, b"ZA", b"ZAF"),
(Zm, b"ZM", b"ZMB"),
(Zw, b"ZW", b"ZWE")
];
#[cfg(test)]
mod test {
use assert_matches::assert_matches;
use crate::{
json::{self, FromJson},
Verdict,
};
use super::{Code, CodeSet, WarningKind};
#[test]
fn alpha2_country_code_matches() {
let code = Code::from_alpha_2(*b"NL").unwrap();
assert_eq!(Code::Nl, code);
assert_eq!("NL", code.into_str());
}
#[test]
fn alpha3_country_code_matches() {
let code = Code::from_alpha_3(*b"NLD").unwrap();
assert_eq!(Code::Nl, code);
}
#[test]
fn should_create_country3_without_issue() {
const JSON: &str = r#"{ "country": "NLD" }"#;
let (code_set, warnings) = code_set_from_json(JSON).unwrap().into_parts();
let code = assert_matches!(code_set, CodeSet::Alpha3(code) => code);
assert_eq!(code, Code::Nl);
assert_matches!(*warnings, []);
}
#[test]
fn should_create_country2_without_issue() {
const JSON: &str = r#"{ "country": "NL" }"#;
let (code_set, warnings) = code_set_from_json(JSON).unwrap().into_parts();
let code = assert_matches!(code_set, CodeSet::Alpha2(code) => code);
assert_eq!(code, Code::Nl);
assert_matches!(*warnings, []);
}
#[test]
fn should_raise_country_content_issue() {
{
const JSON: &str = r#"{ "country": "VV" }"#;
let warnings = code_set_from_json(JSON).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidCode]);
}
{
const JSON: &str = r#"{ "country": "VVV" }"#;
let warnings = code_set_from_json(JSON).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidCode]);
}
}
#[test]
fn should_parse_invalid_case() {
{
const JSON: &str = r#"{ "country": "nl" }"#;
let (code_set, warnings) = code_set_from_json(JSON).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
let code = assert_matches!(code_set, CodeSet::Alpha2(code) => code);
assert_eq!(code, Code::Nl);
assert_matches!(*warnings, [WarningKind::InvalidCase]);
}
{
const JSON: &str = r#"{ "country": "nld" }"#;
let (code_set, warnings) = code_set_from_json(JSON).unwrap().into_parts();
let warnings = warnings.into_kind_vec();
let code = assert_matches!(code_set, CodeSet::Alpha3(code) => code);
assert_eq!(code, Code::Nl);
assert_matches!(*warnings, [WarningKind::InvalidCase]);
}
}
#[test]
fn should_raise_country_length_issue() {
const JSON: &str = r#"{ "country": "IRELAND" }"#;
let warnings = code_set_from_json(JSON).unwrap_err().into_kind_vec();
assert_matches!(*warnings, [WarningKind::InvalidLength]);
}
#[track_caller]
fn code_set_from_json(json: &str) -> Verdict<CodeSet, WarningKind> {
let json = json::parse(json).unwrap();
let country_elem = json.find_field("country").unwrap();
CodeSet::from_json(country_elem.element())
}
}