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