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
8mod data;
9
10use std::fmt;
11
12#[doc(inline)]
13pub use data::Code;
14
15use crate::{
16    json,
17    warning::{self, GatherWarnings as _, IntoCaveat as _},
18    Verdict,
19};
20
21const RESERVED_PREFIX: u8 = b'x';
22const ALPHA_2_LEN: usize = 2;
23const ALPHA_3_LEN: usize = 3;
24
25#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
26pub enum Warning {
27    /// Neither the timezone or country field require char escape codes.
28    ContainsEscapeCodes,
29
30    /// The field at the path could not be decoded.
31    Decode(json::decode::Warning),
32
33    /// The `country` is not a valid `ISO 3166-1` country code because it's not uppercase.
34    PreferUpperCase,
35
36    /// The `country` is not a valid `ISO 3166-1` country code.
37    InvalidCode,
38
39    /// The JSON value given is not a string.
40    InvalidType { type_found: json::ValueKind },
41
42    /// The `country` is not a valid `ISO 3166-1` country code because it's not 2 or 3 chars in length.
43    InvalidLength,
44
45    /// The `country` is not a valid `ISO 3166-1` country code because it's all codes beginning with 'X' are reserved.
46    InvalidReserved,
47}
48
49impl Warning {
50    fn invalid_type(elem: &json::Element<'_>) -> Self {
51        Self::InvalidType {
52            type_found: elem.value().kind(),
53        }
54    }
55}
56
57impl fmt::Display for Warning {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::ContainsEscapeCodes => f.write_str("The value contains escape codes but it does not need them"),
61            Self::Decode(warning) => fmt::Display::fmt(warning, f),
62            Self::PreferUpperCase => f.write_str("The country-code follows the ISO 3166-1 standard which states: the chars should be uppercase."),
63            Self::InvalidCode => f.write_str("The country-code is not a valid ISO 3166-1 code."),
64            Self::InvalidType { type_found } => {
65                write!(f, "The value should be a string but is `{type_found}`")
66            }
67            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."),
68            Self::InvalidReserved => f.write_str("The country-code follows the ISO 3166-1 standard which states: all codes beginning with 'X' are reserved."),
69        }
70    }
71}
72
73impl crate::Warning for Warning {
74    fn id(&self) -> warning::Id {
75        match self {
76            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
77            Self::Decode(kind) => kind.id(),
78            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
79            Self::InvalidCode => warning::Id::from_static("invalid_code"),
80            Self::InvalidType { type_found } => {
81                warning::Id::from_string(format!("invalid_type({type_found})"))
82            }
83            Self::InvalidLength => warning::Id::from_static("invalid_length"),
84            Self::InvalidReserved => warning::Id::from_static("invalid_reserved"),
85        }
86    }
87}
88
89/// An alpha-2 or alpha-3 `Code`.
90///
91/// The caller can decide if they want to warn or fail if the wrong variant is parsed.
92#[derive(Debug)]
93pub(crate) enum CodeSet {
94    /// An alpha-2 country code was parsed.
95    Alpha2(Code),
96
97    /// An alpha-3 country code was parsed.
98    Alpha3(Code),
99}
100
101impl From<json::decode::Warning> for Warning {
102    fn from(warn_kind: json::decode::Warning) -> Self {
103        Self::Decode(warn_kind)
104    }
105}
106
107impl json::FromJson<'_> for CodeSet {
108    type Warning = Warning;
109
110    fn from_json(elem: &json::Element<'_>) -> Verdict<CodeSet, Self::Warning> {
111        let mut warnings = warning::Set::new();
112        let value = elem.as_value();
113
114        let Some(s) = value.to_raw_str() else {
115            return warnings.bail(elem, Warning::invalid_type(elem));
116        };
117
118        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
119
120        let s = match pending_str {
121            json::PendingStr::NoEscapes(s) => s,
122            json::PendingStr::HasEscapes(_) => {
123                return warnings.bail(elem, Warning::ContainsEscapeCodes);
124            }
125        };
126
127        let bytes = s.as_bytes();
128
129        if let [a, b, c] = bytes {
130            let triplet: [u8; ALPHA_3_LEN] = [
131                a.to_ascii_uppercase(),
132                b.to_ascii_uppercase(),
133                c.to_ascii_uppercase(),
134            ];
135
136            if triplet != bytes {
137                warnings.insert(elem, Warning::PreferUpperCase);
138            }
139
140            if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
141                warnings.insert(elem, Warning::InvalidReserved);
142            }
143
144            let Some(code) = Code::from_alpha_3(triplet) else {
145                return warnings.bail(elem, Warning::InvalidCode);
146            };
147
148            Ok(CodeSet::Alpha3(code).into_caveat(warnings))
149        } else if let [a, b] = bytes {
150            let pair: [u8; ALPHA_2_LEN] = [a.to_ascii_uppercase(), b.to_ascii_uppercase()];
151
152            if pair != bytes {
153                warnings.insert(elem, Warning::PreferUpperCase);
154            }
155
156            if a.eq_ignore_ascii_case(&RESERVED_PREFIX) {
157                warnings.insert(elem, Warning::InvalidReserved);
158            }
159
160            let Some(code) = Code::from_alpha_2(pair) else {
161                return warnings.bail(elem, Warning::InvalidCode);
162            };
163
164            Ok(CodeSet::Alpha2(code).into_caveat(warnings))
165        } else {
166            warnings.bail(elem, Warning::InvalidLength)
167        }
168    }
169}
170
171impl fmt::Display for Code {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.write_str(self.into_alpha_2_str())
174    }
175}
176
177/// Macro to specify a list of valid `ISO 3166-1` `alpha-2` and `alpha-3` country codes strings.
178macro_rules! country_codes {
179    [$(($name:ident, $alpha2:literal, $alpha3:literal)),*] => {
180        /// An `ISO 3166-1` `alpha-2` country code.
181        ///
182        /// The impl is designed to be converted from `json::RawValue`.
183        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
184        pub enum Code {
185            $($name),*
186        }
187
188        impl Code {
189            /// Try creating a `Code` from two upper ASCII bytes.
190            pub(super) const fn from_alpha_2(code: [u8; 2]) -> Option<Self> {
191                match &code {
192                    $($alpha2 => Some(Self::$name),)*
193                    _ => None
194                }
195            }
196
197            /// Try creating a `Code` from three upper ASCII bytes.
198            pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
199                match &code {
200                    $($alpha3 => Some(Self::$name),)*
201                    _ => None
202                }
203            }
204
205            /// Return enum as two byte uppercase `&str`.
206            pub fn into_alpha_2_str(self) -> &'static str {
207                let bytes = match self {
208                    $(Self::$name => $alpha2),*
209                };
210                std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
211            }
212
213            /// Return enum as three byte uppercase `&str`.
214            pub fn into_alpha_3_str(self) -> &'static str {
215                let bytes = match self {
216                    $(Self::$name => $alpha3),*
217                };
218                std::str::from_utf8(bytes).expect("The country code bytes are known to be valid UTF8 as they are embedded into the binary")
219            }
220        }
221    };
222}
223
224pub(crate) use country_codes;