1#[cfg(test)]
6pub(crate) mod test;
7
8mod data;
9
10use std::fmt;
11
12#[doc(inline)]
13pub use data::Code;
14
15use crate::{
16 json,
17 warning::{self, GatherWarnings as _, IntoCaveat as _},
18 Verdict,
19};
20
21const RESERVED_PREFIX: u8 = b'x';
22const ALPHA_2_LEN: usize = 2;
23const ALPHA_3_LEN: usize = 3;
24
25#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
26pub enum Warning {
27 ContainsEscapeCodes,
29
30 Decode(json::decode::Warning),
32
33 PreferUpperCase,
35
36 InvalidCode,
38
39 InvalidType { type_found: json::ValueKind },
41
42 InvalidLength,
44
45 InvalidReserved,
47}
48
49impl Warning {
50 fn invalid_type(elem: &json::Element<'_>) -> Self {
51 Self::InvalidType {
52 type_found: elem.value().kind(),
53 }
54 }
55}
56
57impl fmt::Display for Warning {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
61 Self::Decode(warning) => fmt::Display::fmt(warning, f),
62 Self::PreferUpperCase => f.write_str("The country-code follows the ISO 3166-1 standard which states: the chars should be uppercase."),
63 Self::InvalidCode => f.write_str("The country-code is not a valid ISO 3166-1 code."),
64 Self::InvalidType { type_found } => {
65 write!(f, "The value should be a string but is `{type_found}`")
66 }
67 Self::InvalidLength => f.write_str("The country-code follows the ISO 3166-1 which states that the code should be 2 or 3 chars in length."),
68 Self::InvalidReserved => f.write_str("The country-code follows the ISO 3166-1 standard which states: all codes beginning with 'X' are reserved."),
69 }
70 }
71}
72
73impl crate::Warning for Warning {
74 fn id(&self) -> warning::Id {
75 match self {
76 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
77 Self::Decode(kind) => kind.id(),
78 Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
79 Self::InvalidCode => warning::Id::from_static("invalid_code"),
80 Self::InvalidType { type_found } => {
81 warning::Id::from_string(format!("invalid_type({type_found})"))
82 }
83 Self::InvalidLength => warning::Id::from_static("invalid_length"),
84 Self::InvalidReserved => warning::Id::from_static("invalid_reserved"),
85 }
86 }
87}
88
89#[derive(Debug)]
93pub(crate) enum CodeSet {
94 Alpha2(Code),
96
97 Alpha3(Code),
99}
100
101impl From<json::decode::Warning> for Warning {
102 fn from(warn_kind: json::decode::Warning) -> Self {
103 Self::Decode(warn_kind)
104 }
105}
106
107impl json::FromJson<'_> for CodeSet {
108 type Warning = Warning;
109
110 fn from_json(elem: &json::Element<'_>) -> Verdict<CodeSet, Self::Warning> {
111 let mut warnings = warning::Set::new();
112 let value = elem.as_value();
113
114 let Some(s) = value.to_raw_str() else {
115 return warnings.bail(elem, Warning::invalid_type(elem));
116 };
117
118 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
119
120 let s = match pending_str {
121 json::PendingStr::NoEscapes(s) => s,
122 json::PendingStr::HasEscapes(_) => {
123 return warnings.bail(elem, Warning::ContainsEscapeCodes);
124 }
125 };
126
127 let bytes = s.as_bytes();
128
129 if let [a, b, c] = bytes {
130 let triplet: [u8; ALPHA_3_LEN] = [
131 a.to_ascii_uppercase(),
132 b.to_ascii_uppercase(),
133 c.to_ascii_uppercase(),
134 ];
135
136 if triplet != bytes {
137 warnings.insert(elem, Warning::PreferUpperCase);
138 }
139
140 if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
141 warnings.insert(elem, Warning::InvalidReserved);
142 }
143
144 let Some(code) = Code::from_alpha_3(triplet) else {
145 return warnings.bail(elem, Warning::InvalidCode);
146 };
147
148 Ok(CodeSet::Alpha3(code).into_caveat(warnings))
149 } else if let [a, b] = bytes {
150 let pair: [u8; ALPHA_2_LEN] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];
151
152 if pair != bytes {
153 warnings.insert(elem, Warning::PreferUpperCase);
154 }
155
156 if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
157 warnings.insert(elem, Warning::InvalidReserved);
158 }
159
160 let Some(code) = Code::from_alpha_2(pair) else {
161 return warnings.bail(elem, Warning::InvalidCode);
162 };
163
164 Ok(CodeSet::Alpha2(code).into_caveat(warnings))
165 } else {
166 warnings.bail(elem, Warning::InvalidLength)
167 }
168 }
169}
170
171impl fmt::Display for Code {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.write_str(self.into_alpha_2_str())
174 }
175}
176
177macro_rules! country_codes {
179 [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
180 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
184 pub enum Code {
185 $($name),*
186 }
187
188 impl Code {
189 pub(super) const fn from_alpha_2(code: [u8; 2]) -> Option<Self> {
191 match &code {
192 $($alpha2 => Some(Self::$name),)*
193 _ => None
194 }
195 }
196
197 pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
199 match &code {
200 $($alpha3 => Some(Self::$name),)*
201 _ => None
202 }
203 }
204
205 pub fn into_alpha_2_str(self) -> &'static str {
207 let bytes = match self {
208 $(Self::$name => $alpha2),*
209 };
210 std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
211 }
212
213 pub fn into_alpha_3_str(self) -> &'static str {
215 let bytes = match self {
216 $(Self::$name => $alpha3),*
217 };
218 std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
219 }
220 }
221 };
222}
223
224pub(crate) use country_codes;