Skip to main content

ocpi_tariffs/
currency.rs

1//! An ISO 4217 currency code.
2#[cfg(test)]
3pub(crate) mod test;
4
5use std::fmt;
6
7use crate::{
8    from_warning_all, into_caveat, json,
9    warning::{self, GatherWarnings as _},
10    IntoCaveat, Verdict,
11};
12
13/// The warnings that can happen when parsing or linting a currency code.
14#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
15pub enum Warning {
16    /// The currency field does not require char escape codes.
17    ContainsEscapeCodes,
18
19    /// The field at the path could not be decoded.
20    Decode(json::decode::Warning),
21
22    /// The `country` is not a valid ISO 3166-1 country code because it's not uppercase.
23    PreferUpperCase,
24
25    /// The `currency` is not a valid ISO 4217 currency code.
26    InvalidCode,
27
28    /// The JSON value given is not a string.
29    InvalidType { type_found: json::ValueKind },
30
31    /// The `currency` is not a valid ISO 4217 currency code: it should be 3 chars.
32    InvalidLength,
33
34    /// The `currency` is not a valid ISO 4217 currency code because it's a test code.
35    InvalidCodeXTS,
36
37    /// The `currency` is not a valid ISO 4217 currency code because it's a code for `no-currency`.
38    InvalidCodeXXX,
39}
40
41impl Warning {
42    fn invalid_type(elem: &json::Element<'_>) -> Self {
43        Self::InvalidType {
44            type_found: elem.value().kind(),
45        }
46    }
47}
48
49impl fmt::Display for Warning {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::ContainsEscapeCodes => write!(
53                f,
54                "The currency-code contains escape-codes but it does not need them.",
55            ),
56            Self::Decode(warning) => fmt::Display::fmt(warning, f),
57            Self::PreferUpperCase => write!(
58                f,
59                "The currency-code follows the ISO 4217 standard whch states: the chars should be uppercase.",
60            ),
61            Self::InvalidCode => {
62                write!(f, "The currency-code is not a valid ISO 4217 code.")
63            }
64            Self::InvalidType { .. } => write!(f, "The currency-code should be a string."),
65            Self::InvalidLength => write!(f, "The currency-code follows the ISO 4217 standard whch states: the code should be three chars."),
66            Self::InvalidCodeXTS => write!(
67                f,
68                "The currency-code is `XTS`. This is a code for testing only",
69            ),
70            Self::InvalidCodeXXX => write!(
71                f,
72                "The currency-code is `XXX`. This means there is no currency",
73            ),
74        }
75    }
76}
77
78impl crate::Warning for Warning {
79    fn id(&self) -> warning::Id {
80        match self {
81            Self::ContainsEscapeCodes => warning::Id::from_static("contains_escape_codes"),
82            Self::Decode(kind) => kind.id(),
83            Self::PreferUpperCase => warning::Id::from_static("prefer_upper_case"),
84            Self::InvalidCode => warning::Id::from_static("invalid_code"),
85            Self::InvalidType { .. } => warning::Id::from_static("invalid_type"),
86            Self::InvalidLength => warning::Id::from_static("invalid_length"),
87            Self::InvalidCodeXTS => warning::Id::from_static("invalid_code_xts"),
88            Self::InvalidCodeXXX => warning::Id::from_static("invalid_code_xxx"),
89        }
90    }
91}
92
93from_warning_all!(json::decode::Warning => Warning::Decode);
94
95impl json::FromJson<'_, '_> for Code {
96    type Warning = Warning;
97
98    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
99        let mut warnings = warning::Set::new();
100        let value = elem.as_value();
101
102        let Some(s) = value.as_raw_str() else {
103            return warnings.bail(Warning::invalid_type(elem), elem);
104        };
105
106        let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
107
108        let s = match pending_str {
109            json::decode::PendingStr::NoEscapes(s) => s,
110            json::decode::PendingStr::HasEscapes(_) => {
111                return warnings.bail(Warning::ContainsEscapeCodes, elem);
112            }
113        };
114
115        let bytes = s.as_bytes();
116
117        // ISO 4217 is expected to be 3 chars enclosed in quotes.
118        let [a, b, c] = bytes else {
119            return warnings.bail(Warning::InvalidLength, elem);
120        };
121
122        let triplet: [u8; 3] = [
123            a.to_ascii_uppercase(),
124            b.to_ascii_uppercase(),
125            c.to_ascii_uppercase(),
126        ];
127
128        if triplet != bytes {
129            warnings.insert(Warning::PreferUpperCase, elem);
130        }
131
132        let Some(code) = Code::from_alpha_3(triplet) else {
133            return warnings.bail(Warning::InvalidCode, elem);
134        };
135
136        if matches!(code, Code::Xts) {
137            warnings.insert(Warning::InvalidCodeXTS, elem);
138        } else if matches!(code, Code::Xxx) {
139            warnings.insert(Warning::InvalidCodeXXX, elem);
140        }
141
142        Ok(code.into_caveat(warnings))
143    }
144}
145
146into_caveat!(Code);
147
148impl fmt::Display for Code {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.into_str())
151    }
152}
153
154/// Macro to specify a list of valid ISO 4217 alpha-3 currency codes.
155macro_rules! currency_codes {
156    [$(($name:ident, $alph3:literal, $symbol:literal)),*] => {
157        /// An ISO 4217 currency code.
158        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
159        pub enum Code {
160            $($name),*
161        }
162
163        impl Code {
164            /// Try creating a `Code` from three upper ASCII bytes.
165            const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
166                match &code {
167                    $($alph3 => Some(Self::$name),)*
168                    _ => None
169                }
170            }
171
172            /// Return enum as three byte uppercase &str.
173            pub fn into_str(self) -> &'static str {
174                let bytes = match self {
175                    $(Self::$name => $alph3),*
176                };
177                std::str::from_utf8(bytes).expect("The currency code bytes are known to be valid UTF8 as they are embedded into the binary")
178            }
179
180            /// Return a str symbol of the [Code]
181            pub fn into_symbol(self) -> &'static str {
182                match self {
183                    $(Self::$name => $symbol),*
184                }
185            }
186        }
187    };
188}
189
190currency_codes![
191    (Aed, b"AED", "D"),
192    (Afn, b"AFN", "Afs"),
193    (All, b"ALL", "L"),
194    (Amd, b"AMD", "֏"),
195    (Ang, b"ANG", "NAƒ"),
196    (Aoa, b"AOA", "Kz"),
197    (Ars, b"ARS", "$"),
198    (Aud, b"AUD", "$"),
199    (Awg, b"AWG", "Aƒ"),
200    (Azn, b"AZN", "₼"),
201    (Bam, b"BAM", "KM"),
202    (Bbd, b"BBD", "$"),
203    (Bdt, b"BDT", "৳"),
204    (Bgn, b"BGN", "лв"),
205    (Bhd, b"BHD", "BD"),
206    (Bif, b"BIF", "FBu"),
207    (Bmd, b"BMD", "$"),
208    (Bnd, b"BND", "$"),
209    (Bob, b"BOB", "Bs"),
210    (Bov, b"BOV", "BOV"),
211    (Brl, b"BRL", "$"),
212    (Bsd, b"BSD", "$"),
213    (Btn, b"BTN", "Nu."),
214    (Bwp, b"BWP", "p"),
215    (Byn, b"BYN", "BYN"),
216    (Bzd, b"BZD", "$"),
217    (Cad, b"CAD", "$"),
218    (Cdf, b"CDF", "FC"),
219    (Che, b"CHE", "CHE"),
220    (Chf, b"CHF", "CHF"),
221    (Chw, b"CHW", "CHW"),
222    (Clf, b"CLF", "UF"),
223    (Clp, b"CLP", "$"),
224    (Cny, b"CNY", "¥"),
225    (Cop, b"COP", "$"),
226    (Cou, b"COU", "COU"),
227    (Crc, b"CRC", "₡"),
228    (Cuc, b"CUC", "$"),
229    (Cup, b"CUP", "$"),
230    (Cve, b"CVE", "$"),
231    (Czk, b"CZK", "Kč"),
232    (Djf, b"DJF", "Fdj"),
233    (Dkk, b"DKK", "kr."),
234    (Dop, b"DOP", "$"),
235    (Dzd, b"DZD", "DA"),
236    (Egp, b"EGP", "£"),
237    (Ern, b"ERN", "Nfk"),
238    (Etb, b"ETB", "Br"),
239    (Eur, b"EUR", "€"),
240    (Fjd, b"FJD", "$"),
241    (Fkp, b"FKP", "£"),
242    (Gbp, b"GBP", "£"),
243    (Gel, b"GEL", "₾"),
244    (Ghs, b"GHS", "GH₵"),
245    (Gip, b"GIP", "£"),
246    (Gmd, b"GMD", "D"),
247    (Gnf, b"GNF", "FG"),
248    (Gtq, b"GTQ", "Q"),
249    (Gyd, b"GYD", "$"),
250    (Hkd, b"HKD", "$"),
251    (Hnl, b"HNL", "L"),
252    (Hrk, b"HRK", "lp"),
253    (Htg, b"HTG", "G"),
254    (Huf, b"HUF", "Ft"),
255    (Idr, b"IDR", "Rp"),
256    (Ils, b"ILS", "₪"),
257    (Inr, b"INR", "₹"),
258    (Iqd, b"IQD", "IQD"),
259    (Irr, b"IRR", "Rl"),
260    (Isk, b"ISK", "kr"),
261    (Jmd, b"JMD", "$"),
262    (Jod, b"JOD", "د.أ"),
263    (Jpy, b"JPY", "¥"),
264    (Kes, b"KES", "KES"),
265    (Kgs, b"KGS", "⃀"),
266    (Khr, b"KHR", "៛"),
267    (Kmf, b"KMF", "KMF"),
268    (Kpw, b"KPW", "₩"),
269    (Krw, b"KRW", "₩"),
270    (Kwd, b"KWD", "KD"),
271    (Kyd, b"KYD", "$"),
272    (Kzt, b"KZT", "₸"),
273    (Lak, b"LAK", "₭"),
274    (Lbp, b"LBP", "LL"),
275    (Lkr, b"LKR", "₨"),
276    (Lrd, b"LRD", "$"),
277    (Lsl, b"LSL", "L"),
278    (Lyd, b"LYD", "LD"),
279    (Mad, b"MAD", "DH"),
280    (Mdl, b"MDL", "L"),
281    (Mga, b"MGA", "Ar"),
282    (Mkd, b"MKD", "den"),
283    (Mmk, b"MMK", "Ks."),
284    (Mnt, b"MNT", "₮"),
285    (Mop, b"MOP", "$"),
286    (Mru, b"MRU", "UM"),
287    (Mur, b"MUR", "₨"),
288    (Mvr, b"MVR", "Rf"),
289    (Mwk, b"MWK", "K"),
290    (Mxn, b"MXN", "$"),
291    (Mxv, b"MXV", "MXV"),
292    (Myr, b"MYR", "RM"),
293    (Mzn, b"MZN", "MT"),
294    (Nad, b"NAD", "$"),
295    (Ngn, b"NGN", "₦"),
296    (Nio, b"NIO", "C$"),
297    (Nok, b"NOK", "kr"),
298    (Npr, b"NPR", "रु"),
299    (Nzd, b"NZD", "$"),
300    (Omr, b"OMR", "OMR"),
301    (Pab, b"PAB", "฿"),
302    (Pen, b"PEN", "S/"),
303    (Pgk, b"PGK", "K"),
304    (Php, b"PHP", "₱"),
305    (Pkr, b"PKR", "Re"),
306    (Pln, b"PLN", "zł"),
307    (Pyg, b"PYG", "₲"),
308    (Qar, b"QAR", "QR"),
309    (Ron, b"RON", "lei"),
310    (Rsd, b"RSD", "DIN"),
311    (Rub, b"RUB", "₽"),
312    (Rwf, b"RWF", "R₣"),
313    (Sar, b"SAR", "\u{20C1}"),
314    (Sbd, b"SBD", "$"),
315    (Scr, b"SCR", "R"),
316    (Sdg, b"SDG", "PT"),
317    (Sek, b"SEK", "kr"),
318    (Sgd, b"SGD", "$"),
319    (Shp, b"SHP", "£"),
320    (Sle, b"SLE", "Le"),
321    (Sll, b"SLL", "Le"),
322    (Sos, b"SOS", "Sh.So."),
323    (Srd, b"SRD", "$"),
324    (Ssp, b"SSP", "£"),
325    (Stn, b"STN", "Db"),
326    (Svc, b"SVC", "₡"),
327    (Syp, b"SYP", "LS"),
328    (Szl, b"SZL", "L"),
329    (Thb, b"THB", "฿"),
330    (Tjs, b"TJS", "SM"),
331    (Tmt, b"TMT", "m"),
332    (Tnd, b"TND", "DT"),
333    (Top, b"TOP", "$"),
334    (Try, b"TRY", "₺"),
335    (Ttd, b"TTD", "$"),
336    (Twd, b"TWD", "$"),
337    (Tzs, b"TZS", "TZS"),
338    (Uah, b"UAH", "₴"),
339    (Ugx, b"UGX", "UGX"),
340    (Usd, b"USD", "$"),
341    (Usn, b"USN", "USN"),
342    (Uyi, b"UYI", "UYI"),
343    (Uyu, b"UYU", "$"),
344    (Uyw, b"UYW", "UYW"),
345    (Uzs, b"UZS", "UZS"),
346    (Ved, b"VED", "Bs.D"),
347    (Ves, b"VES", "Bs.S"),
348    (Vnd, b"VND", "₫"),
349    (Vuv, b"VUV", "Vt"),
350    (Wst, b"WST", "$"),
351    (Xaf, b"XAF", "F.CFA"),
352    (Xag, b"XAG", "XAG"),
353    (Xau, b"XAU", "XAU"),
354    (Xba, b"XBA", "XBA"),
355    (Xbb, b"XBB", "XBB"),
356    (Xbc, b"XBC", "XBC"),
357    (Xbd, b"XBD", "XBD"),
358    (Xcd, b"XCD", "$"),
359    (Xdr, b"XDR", "SDR"),
360    (Xof, b"XOF", "F.CFA"),
361    (Xpd, b"XPD", "XPD"),
362    (Xpf, b"XPF", "F"),
363    (Xpt, b"XPT", "XPT"),
364    (Xsu, b"XSU", "sucre"),
365    (Xts, b"XTS", "XTS"),
366    (Xua, b"XUA", "XUA"),
367    (Xxx, b"XXX", "XXX"),
368    (Yer, b"YER", "﷼"),
369    (Zar, b"ZAR", "R"),
370    (Zmw, b"ZMW", "K"),
371    (Zwl, b"ZWL", "$")
372];