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