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