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