Skip to main content

ocpi_tariffs/
country.rs

1//! An `ISO 3166-1` country code.
2//!
3//! Use `CodeSet` to parse a `Code` from JSON.
4
5#[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    /// Neither the timezone or country field require char escape codes.
31    ContainsEscapeCodes,
32
33    /// The field at the path could not be decoded.
34    Decode(json::decode::Warning),
35
36    /// The `country` is not a valid `ISO 3166-1` country code because it's not uppercase.
37    PreferUpperCase,
38
39    /// The `country` is not a valid `ISO 3166-1` country code.
40    InvalidCode,
41
42    /// The JSON value given is not a string.
43    InvalidType { type_found: json::ValueKind },
44
45    /// The `country` is not a valid `ISO 3166-1` country code because it's not 2 or 3 chars in length.
46    InvalidLength,
47
48    /// The `country` is not a valid `ISO 3166-1` country code because it's all codes beginning with 'X' are reserved.
49    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/// An alpha-2 or alpha-3 `Code`.
93///
94/// The caller can decide if they want to warn or fail if the wrong variant is parsed.
95#[derive(Debug)]
96pub(crate) enum CodeSet {
97    /// An alpha-2 country code was parsed.
98    Alpha2(Code),
99
100    /// An alpha-3 country code was parsed.
101    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        // The schema confirmed the value is a string, so there is no kind check; its
182        // content is read directly.
183        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
245/// Macro to specify a list of valid `ISO 3166-1` `alpha-2` and `alpha-3` country codes strings.
246macro_rules! country_codes {
247    [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
248        /// An `ISO 3166-1` `alpha-2` country code.
249        ///
250        /// The impl is designed to be converted from `json::RawValue`.
251        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
252        pub enum Code {
253            $($name),*
254        }
255
256        impl Code {
257            /// Try creating a `Code` from two upper ASCII bytes.
258            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            /// Try creating a `Code` from three upper ASCII bytes.
266            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            /// Return enum as two byte uppercase `&str`.
274            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            /// Return enum as three byte uppercase `&str`.
282            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;