Skip to main content

ocpi_tariffs/
currency.rs

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