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,
20 schema::{self, HasElement as _},
21 warning::{self, GatherWarnings as _, IntoCaveat as _},
22 FromSchema, Verdict,
23};
24
25const RESERVED_PREFIX: u8 = b'x';
26const ALPHA_2_LEN: usize = 2;
27const ALPHA_3_LEN: usize = 3;
28
29#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
31pub enum Warning {
32 ContainsEscapeCodes,
34
35 Decode(json::decode::Warning),
37
38 IncorrectCase,
40
41 InvalidCode,
43
44 InvalidLength,
46
47 InvalidReserved,
49}
50
51impl fmt::Display for Warning {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
55 Self::Decode(warning) => fmt::Display::fmt(warning, f),
56 Self::IncorrectCase => f.write_str("The country-code follows the ISO 3166-1 standard which states: the chars should be uppercase."),
57 Self::InvalidCode => f.write_str("The country-code is not a valid ISO 3166-1 code."),
58 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."),
59 Self::InvalidReserved => f.write_str("The country-code follows the ISO 3166-1 standard which states: all codes beginning with 'X' are reserved."),
60 }
61 }
62}
63
64impl crate::Warning for Warning {
65 fn id(&self) -> warning::Id {
66 match self {
67 Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
68 Self::Decode(kind) => kind.id(),
69 Self::IncorrectCase => warning::Id::from_static("incorrect_case"),
70 Self::InvalidCode => warning::Id::from_static("invalid_code"),
71 Self::InvalidLength => warning::Id::from_static("invalid_length"),
72 Self::InvalidReserved => warning::Id::from_static("invalid_reserved"),
73 }
74 }
75}
76
77#[derive(Debug)]
81pub(crate) enum CodeSet {
82 Alpha2(Code),
84
85 Alpha3(Code),
87}
88
89impl From<json::decode::Warning> for Warning {
90 fn from(warn_kind: json::decode::Warning) -> Self {
91 Self::Decode(warn_kind)
92 }
93}
94
95impl<'buf> FromSchema<'buf, schema::Str<'buf>> for CodeSet {
96 type Warning = Warning;
97
98 fn from_schema(source: &schema::Str<'buf>) -> Verdict<CodeSet, Self::Warning> {
99 let mut warnings = warning::Set::new();
100 let elem = source.element();
101
102 let pending_str = source
105 .value()
106 .has_escapes(elem)
107 .gather_warnings_into(&mut warnings);
108
109 let s = match pending_str {
110 json::PendingStr::NoEscapes(s) => s,
111 json::PendingStr::HasEscapes(_) => {
112 return warnings.bail(elem, Warning::ContainsEscapeCodes);
113 }
114 };
115
116 let bytes = s.as_bytes();
117
118 if let [a, b, c] = bytes {
119 let triplet: [u8; ALPHA_3_LEN] = [
120 a.to_ascii_uppercase(),
121 b.to_ascii_uppercase(),
122 c.to_ascii_uppercase(),
123 ];
124
125 if triplet != bytes {
126 warnings.insert(elem, Warning::IncorrectCase);
127 }
128
129 if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
130 warnings.insert(elem, Warning::InvalidReserved);
131 }
132
133 let Some(code) = Code::from_alpha_3(triplet) else {
134 return warnings.bail(elem, Warning::InvalidCode);
135 };
136
137 Ok(CodeSet::Alpha3(code).into_caveat(warnings))
138 } else if let [a, b] = bytes {
139 let pair: [u8; ALPHA_2_LEN] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];
140
141 if pair != bytes {
142 warnings.insert(elem, Warning::IncorrectCase);
143 }
144
145 if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
146 warnings.insert(elem, Warning::InvalidReserved);
147 }
148
149 let Some(code) = Code::from_alpha_2(pair) else {
150 return warnings.bail(elem, Warning::InvalidCode);
151 };
152
153 Ok(CodeSet::Alpha2(code).into_caveat(warnings))
154 } else {
155 warnings.bail(elem, Warning::InvalidLength)
156 }
157 }
158}
159
160impl fmt::Display for Code {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.write_str(self.into_alpha_2_str())
163 }
164}
165
166macro_rules! country_codes {
168 [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
169 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
173 #[expect(missing_docs, reason = "Each variant should tecnically have a comment but each variant is simply the country code.")]
174 pub enum Code {
175 $($name),*
176 }
177
178 impl Code {
179 pub(super) const fn from_alpha_2(code: [u8; 2]) -> Option<Self> {
181 match &code {
182 $($alpha2 => Some(Self::$name),)*
183 _ => None
184 }
185 }
186
187 pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
189 match &code {
190 $($alpha3 => Some(Self::$name),)*
191 _ => None
192 }
193 }
194
195 pub fn into_alpha_2_str(self) -> &'static str {
197 let bytes = match self {
198 $(Self::$name => $alpha2),*
199 };
200 std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
201 }
202
203 pub fn into_alpha_3_str(self) -> &'static str {
205 let bytes = match self {
206 $(Self::$name => $alpha3),*
207 };
208 std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
209 }
210 }
211 };
212}
213
214pub(crate) use country_codes;