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,
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    /// Neither the timezone or country field require char escape codes.
32    ContainsEscapeCodes,
33
34    /// The field at the path could not be decoded.
35    Decode(json::decode::Warning),
36
37    /// The `country` is not a valid `ISO 3166-1` country code because it's not uppercase.
38    PreferUpperCase,
39
40    /// The `country` is not a valid `ISO 3166-1` country code.
41    InvalidCode,
42
43    /// The `country` is not a valid `ISO 3166-1` country code because it's not 2 or 3 chars in length.
44    InvalidLength,
45
46    /// The `country` is not a valid `ISO 3166-1` country code because it's all codes beginning with 'X' are reserved.
47    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/// An alpha-2 or alpha-3 `Code`.
77///
78/// The caller can decide if they want to warn or fail if the wrong variant is parsed.
79#[derive(Debug)]
80pub(crate) enum CodeSet {
81    /// An alpha-2 country code was parsed.
82    Alpha2(Code),
83
84    /// An alpha-3 country code was parsed.
85    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        // The schema confirmed the value is a string, so there is no kind check; its
102        // content is read directly.
103        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
165/// Macro to specify a list of valid `ISO 3166-1` `alpha-2` and `alpha-3` country codes strings.
166macro_rules! country_codes {
167    [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
168        /// An `ISO 3166-1` `alpha-2` country code.
169        ///
170        /// The impl is designed to be converted from `json::RawValue`.
171        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
172        pub enum Code {
173            $($name),*
174        }
175
176        impl Code {
177            /// Try creating a `Code` from two upper ASCII bytes.
178            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            /// Try creating a `Code` from three upper ASCII bytes.
186            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            /// Return enum as two byte uppercase `&str`.
194            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            /// Return enum as three byte uppercase `&str`.
202            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;