Skip to main content

ocpi_tariffs/
currency.rs

1//! An ISO 4217 currency code.
2
3#[cfg(test)]
4pub(crate) mod test;
5
6#[cfg(test)]
7mod test_from_schema;
8
9mod data;
10
11use std::fmt;
12
13#[doc(inline)]
14pub use data::Code;
15
16use crate::{
17    from_warning_all, json, schema,
18    warning::{self, GatherWarnings as _},
19    FromSchema, IntoCaveat as _, Verdict,
20};
21
22/// The warnings that can happen when parsing or linting a currency code.
23#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
24pub enum Warning {
25    /// The currency field does not require char escape codes.
26    ContainsEscapeCodes,
27
28    /// The field at the path could not be decoded.
29    Decode(json::decode::Warning),
30
31    /// The `country` is not a valid `ISO 3166-1` country code because it's not uppercase.
32    PreferUpperCase,
33
34    /// The `currency` is not a valid `ISO 4217` currency code.
35    InvalidCode,
36
37    /// The JSON value given is not a string.
38    InvalidType { type_found: json::ValueKind },
39
40    /// The `currency` is not a valid `ISO 4217` currency code: it should be 3 chars.
41    InvalidLength,
42
43    /// The `currency` is not a valid `ISO 4217` currency code because it's a test code.
44    InvalidCodeXTS,
45
46    /// The `currency` is not a valid `ISO 4217` currency code because it's a code for `no-currency`.
47    InvalidCodeXXX,
48}
49
50impl Warning {
51    fn invalid_type(elem: &json::Element<'_>) -> Self {
52        Self::InvalidType {
53            type_found: elem.value().kind(),
54        }
55    }
56}
57
58impl fmt::Display for Warning {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::ContainsEscapeCodes => write!(
62                f,
63                "The currency-code contains escape-codes but it does not need them.",
64            ),
65            Self::Decode(warning) => fmt::Display::fmt(warning, f),
66            Self::PreferUpperCase => write!(
67                f,
68                "The currency-code follows the ISO 4217 standard which states: the chars should be uppercase.",
69            ),
70            Self::InvalidCode => {
71                write!(f, "The currency-code is not a valid ISO 4217 code.")
72            }
73            Self::InvalidType { .. } => write!(f, "The currency-code should be a string."),
74            Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard which states: the code should be three chars."),
75            Self::InvalidCodeXTS => write!(
76                f,
77                "The currency-code is `XTS`. This is a code for testing only",
78            ),
79            Self::InvalidCodeXXX => write!(
80                f,
81                "The currency-code is `XXX`. This means there is no currency",
82            ),
83        }
84    }
85}
86
87impl crate::Warning for Warning {
88    fn id(&self) -> warning::Id {
89        match self {
90            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
91            Self::Decode(kind) => kind.id(),
92            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
93            Self::InvalidCode => warning::Id::from_static("invalid_code"),
94            Self::InvalidType { type_found } => {
95                warning::Id::from_string(format!("invalid_type({type_found})"))
96            }
97            Self::InvalidLength => warning::Id::from_static("invalid_length"),
98            Self::InvalidCodeXTS => warning::Id::from_static("invalid_code_xts"),
99            Self::InvalidCodeXXX => warning::Id::from_static("invalid_code_xxx"),
100        }
101    }
102}
103
104from_warning_all!(json::decode::Warning => Warning::Decode);
105
106impl json::FromJson<'_> for Code {
107    type Warning = Warning;
108
109    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
110        let mut warnings = warning::Set::new();
111        let value = elem.as_value();
112
113        let Some(s) = value.to_raw_str() else {
114            return warnings.bail(elem, Warning::invalid_type(elem));
115        };
116
117        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
118
119        let s = match pending_str {
120            json::PendingStr::NoEscapes(s) => s,
121            json::PendingStr::HasEscapes(_) => {
122                return warnings.bail(elem, Warning::ContainsEscapeCodes);
123            }
124        };
125
126        let bytes = s.as_bytes();
127
128        // ISO 4217 is expected to be 3 chars enclosed in quotes.
129        let [a, b, c] = bytes else {
130            return warnings.bail(elem, Warning::InvalidLength);
131        };
132
133        let triplet: [u8; 3] = [
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        let Some(code) = Code::from_alpha_3(triplet) else {
144            return warnings.bail(elem, Warning::InvalidCode);
145        };
146
147        if matches!(code, Code::Xts) {
148            warnings.insert(elem, Warning::InvalidCodeXTS);
149        } else if matches!(code, Code::Xxx) {
150            warnings.insert(elem, Warning::InvalidCodeXXX);
151        }
152
153        Ok(code.into_caveat(warnings))
154    }
155}
156
157impl<'buf> FromSchema<'buf, schema::Str<'buf>> for Code {
158    type Warning = Warning;
159
160    fn from_schema(source: &schema::Str<'buf>) -> Verdict<Self, Self::Warning> {
161        let mut warnings = warning::Set::new();
162        let elem = source.element();
163
164        // The schema confirmed the value is a string, so there is no kind check; its
165        // content is read directly.
166        let pending_str = source
167            .value()
168            .has_escapes(elem)
169            .gather_warnings_into(&mut warnings);
170
171        let s = match pending_str {
172            json::PendingStr::NoEscapes(s) => s,
173            json::PendingStr::HasEscapes(_) => {
174                return warnings.bail(elem, Warning::ContainsEscapeCodes);
175            }
176        };
177
178        let bytes = s.as_bytes();
179
180        // ISO 4217 is expected to be 3 chars enclosed in quotes.
181        let [a, b, c] = bytes else {
182            return warnings.bail(elem, Warning::InvalidLength);
183        };
184
185        let triplet: [u8; 3] = [
186            a.to_ascii_uppercase(),
187            b.to_ascii_uppercase(),
188            c.to_ascii_uppercase(),
189        ];
190
191        if triplet != bytes {
192            warnings.insert(elem, Warning::PreferUpperCase);
193        }
194
195        let Some(code) = Code::from_alpha_3(triplet) else {
196            return warnings.bail(elem, Warning::InvalidCode);
197        };
198
199        if matches!(code, Code::Xts) {
200            warnings.insert(elem, Warning::InvalidCodeXTS);
201        } else if matches!(code, Code::Xxx) {
202            warnings.insert(elem, Warning::InvalidCodeXXX);
203        }
204
205        Ok(code.into_caveat(warnings))
206    }
207}
208
209impl fmt::Display for Code {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        f.write_str(self.into_str())
212    }
213}
214
215/// Macro to specify a list of valid ISO 4217 alpha-3 currency codes.
216macro_rules! currency_codes {
217    [$(($name:ident, $alpha3:literal, $symbol:literal)),*] => {
218        /// An ISO 4217 currency code.
219        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
220        pub enum Code {
221            $($name),*
222        }
223
224        impl Code {
225            /// Try creating a `Code` from three upper ASCII bytes.
226            pub(super) const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
227                match &code {
228                    $($alpha3 => Some(Self::$name),)*
229                    _ => None
230                }
231            }
232
233            /// Return enum as three byte uppercase &str.
234            pub fn into_str(self) -> &'static str {
235                let bytes = match self {
236                    $(Self::$name => $alpha3),*
237                };
238                std::str::from_utf8(bytes).expect("The currency code bytes are known to be valid UTF8 as they are embedded into the binary")
239            }
240
241            /// Return a str symbol of the [Code].
242            pub fn into_symbol(self) -> &'static str {
243                match self {
244                    $(Self::$name => $symbol),*
245                }
246            }
247        }
248    };
249}
250
251pub(crate) use currency_codes;