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, 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.to_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
146impl fmt::Display for Code {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.write_str(self.into_str())
149    }
150}
151
152/// Macro to specify a list of valid ISO 4217 alpha-3 currency codes.
153macro_rules! currency_codes {
154    [$(($name:ident, $alph3:literal, $symbol:literal)),*] => {
155        /// An ISO 4217 currency code.
156        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash,  PartialOrd, Ord)]
157        pub enum Code {
158            $($name),*
159        }
160
161        impl Code {
162            /// Try creating a `Code` from three upper ASCII bytes.
163            const fn from_alpha_3(code: [u8; 3]) -> Option<Self> {
164                match &code {
165                    $($alph3 => Some(Self::$name),)*
166                    _ => None
167                }
168            }
169
170            /// Return enum as three byte uppercase &str.
171            pub fn into_str(self) -> &'static str {
172                let bytes = match self {
173                    $(Self::$name => $alph3),*
174                };
175                std::str::from_utf8(bytes).expect("The currency code bytes are known to be valid UTF8 as they are embedded into the binary")
176            }
177
178            /// Return a str symbol of the [Code]
179            pub fn into_symbol(self) -> &'static str {
180                match self {
181                    $(Self::$name => $symbol),*
182                }
183            }
184        }
185    };
186}
187
188currency_codes![
189    (Aed, b"AED", "D"),
190    (Afn, b"AFN", "Afs"),
191    (All, b"ALL", "L"),
192    (Amd, b"AMD", "֏"),
193    (Ang, b"ANG", "NAƒ"),
194    (Aoa, b"AOA", "Kz"),
195    (Ars, b"ARS", "$"),
196    (Aud, b"AUD", "$"),
197    (Awg, b"AWG", "Aƒ"),
198    (Azn, b"AZN", "₼"),
199    (Bam, b"BAM", "KM"),
200    (Bbd, b"BBD", "$"),
201    (Bdt, b"BDT", "৳"),
202    (Bgn, b"BGN", "лв"),
203    (Bhd, b"BHD", "BD"),
204    (Bif, b"BIF", "FBu"),
205    (Bmd, b"BMD", "$"),
206    (Bnd, b"BND", "$"),
207    (Bob, b"BOB", "Bs"),
208    (Bov, b"BOV", "BOV"),
209    (Brl, b"BRL", "$"),
210    (Bsd, b"BSD", "$"),
211    (Btn, b"BTN", "Nu."),
212    (Bwp, b"BWP", "p"),
213    (Byn, b"BYN", "BYN"),
214    (Bzd, b"BZD", "$"),
215    (Cad, b"CAD", "$"),
216    (Cdf, b"CDF", "FC"),
217    (Che, b"CHE", "CHE"),
218    (Chf, b"CHF", "CHF"),
219    (Chw, b"CHW", "CHW"),
220    (Clf, b"CLF", "UF"),
221    (Clp, b"CLP", "$"),
222    (Cny, b"CNY", "¥"),
223    (Cop, b"COP", "$"),
224    (Cou, b"COU", "COU"),
225    (Crc, b"CRC", "₡"),
226    (Cuc, b"CUC", "$"),
227    (Cup, b"CUP", "$"),
228    (Cve, b"CVE", "$"),
229    (Czk, b"CZK", "Kč"),
230    (Djf, b"DJF", "Fdj"),
231    (Dkk, b"DKK", "kr."),
232    (Dop, b"DOP", "$"),
233    (Dzd, b"DZD", "DA"),
234    (Egp, b"EGP", "£"),
235    (Ern, b"ERN", "Nfk"),
236    (Etb, b"ETB", "Br"),
237    (Eur, b"EUR", "€"),
238    (Fjd, b"FJD", "$"),
239    (Fkp, b"FKP", "£"),
240    (Gbp, b"GBP", "£"),
241    (Gel, b"GEL", "₾"),
242    (Ghs, b"GHS", "GH₵"),
243    (Gip, b"GIP", "£"),
244    (Gmd, b"GMD", "D"),
245    (Gnf, b"GNF", "FG"),
246    (Gtq, b"GTQ", "Q"),
247    (Gyd, b"GYD", "$"),
248    (Hkd, b"HKD", "$"),
249    (Hnl, b"HNL", "L"),
250    (Hrk, b"HRK", "lp"),
251    (Htg, b"HTG", "G"),
252    (Huf, b"HUF", "Ft"),
253    (Idr, b"IDR", "Rp"),
254    (Ils, b"ILS", "₪"),
255    (Inr, b"INR", "₹"),
256    (Iqd, b"IQD", "IQD"),
257    (Irr, b"IRR", "Rl"),
258    (Isk, b"ISK", "kr"),
259    (Jmd, b"JMD", "$"),
260    (Jod, b"JOD", "د.أ"),
261    (Jpy, b"JPY", "¥"),
262    (Kes, b"KES", "KES"),
263    (Kgs, b"KGS", "⃀"),
264    (Khr, b"KHR", "៛"),
265    (Kmf, b"KMF", "KMF"),
266    (Kpw, b"KPW", "₩"),
267    (Krw, b"KRW", "₩"),
268    (Kwd, b"KWD", "KD"),
269    (Kyd, b"KYD", "$"),
270    (Kzt, b"KZT", "₸"),
271    (Lak, b"LAK", "₭"),
272    (Lbp, b"LBP", "LL"),
273    (Lkr, b"LKR", "₨"),
274    (Lrd, b"LRD", "$"),
275    (Lsl, b"LSL", "L"),
276    (Lyd, b"LYD", "LD"),
277    (Mad, b"MAD", "DH"),
278    (Mdl, b"MDL", "L"),
279    (Mga, b"MGA", "Ar"),
280    (Mkd, b"MKD", "den"),
281    (Mmk, b"MMK", "Ks."),
282    (Mnt, b"MNT", "₮"),
283    (Mop, b"MOP", "$"),
284    (Mru, b"MRU", "UM"),
285    (Mur, b"MUR", "₨"),
286    (Mvr, b"MVR", "Rf"),
287    (Mwk, b"MWK", "K"),
288    (Mxn, b"MXN", "$"),
289    (Mxv, b"MXV", "MXV"),
290    (Myr, b"MYR", "RM"),
291    (Mzn, b"MZN", "MT"),
292    (Nad, b"NAD", "$"),
293    (Ngn, b"NGN", "₦"),
294    (Nio, b"NIO", "C$"),
295    (Nok, b"NOK", "kr"),
296    (Npr, b"NPR", "रु"),
297    (Nzd, b"NZD", "$"),
298    (Omr, b"OMR", "OMR"),
299    (Pab, b"PAB", "฿"),
300    (Pen, b"PEN", "S/"),
301    (Pgk, b"PGK", "K"),
302    (Php, b"PHP", "₱"),
303    (Pkr, b"PKR", "Re"),
304    (Pln, b"PLN", "zł"),
305    (Pyg, b"PYG", "₲"),
306    (Qar, b"QAR", "QR"),
307    (Ron, b"RON", "lei"),
308    (Rsd, b"RSD", "DIN"),
309    (Rub, b"RUB", "₽"),
310    (Rwf, b"RWF", "R₣"),
311    (Sar, b"SAR", "\u{20C1}"),
312    (Sbd, b"SBD", "$"),
313    (Scr, b"SCR", "R"),
314    (Sdg, b"SDG", "PT"),
315    (Sek, b"SEK", "kr"),
316    (Sgd, b"SGD", "$"),
317    (Shp, b"SHP", "£"),
318    (Sle, b"SLE", "Le"),
319    (Sll, b"SLL", "Le"),
320    (Sos, b"SOS", "Sh.So."),
321    (Srd, b"SRD", "$"),
322    (Ssp, b"SSP", "£"),
323    (Stn, b"STN", "Db"),
324    (Svc, b"SVC", "₡"),
325    (Syp, b"SYP", "LS"),
326    (Szl, b"SZL", "L"),
327    (Thb, b"THB", "฿"),
328    (Tjs, b"TJS", "SM"),
329    (Tmt, b"TMT", "m"),
330    (Tnd, b"TND", "DT"),
331    (Top, b"TOP", "$"),
332    (Try, b"TRY", "₺"),
333    (Ttd, b"TTD", "$"),
334    (Twd, b"TWD", "$"),
335    (Tzs, b"TZS", "TZS"),
336    (Uah, b"UAH", "₴"),
337    (Ugx, b"UGX", "UGX"),
338    (Usd, b"USD", "$"),
339    (Usn, b"USN", "USN"),
340    (Uyi, b"UYI", "UYI"),
341    (Uyu, b"UYU", "$"),
342    (Uyw, b"UYW", "UYW"),
343    (Uzs, b"UZS", "UZS"),
344    (Ved, b"VED", "Bs.D"),
345    (Ves, b"VES", "Bs.S"),
346    (Vnd, b"VND", "₫"),
347    (Vuv, b"VUV", "Vt"),
348    (Wst, b"WST", "$"),
349    (Xaf, b"XAF", "F.CFA"),
350    (Xag, b"XAG", "XAG"),
351    (Xau, b"XAU", "XAU"),
352    (Xba, b"XBA", "XBA"),
353    (Xbb, b"XBB", "XBB"),
354    (Xbc, b"XBC", "XBC"),
355    (Xbd, b"XBD", "XBD"),
356    (Xcd, b"XCD", "$"),
357    (Xdr, b"XDR", "SDR"),
358    (Xof, b"XOF", "F.CFA"),
359    (Xpd, b"XPD", "XPD"),
360    (Xpf, b"XPF", "F"),
361    (Xpt, b"XPT", "XPT"),
362    (Xsu, b"XSU", "sucre"),
363    (Xts, b"XTS", "XTS"),
364    (Xua, b"XUA", "XUA"),
365    (Xxx, b"XXX", "XXX"),
366    (Yer, b"YER", "﷼"),
367    (Zar, b"ZAR", "R"),
368    (Zmw, b"ZMW", "K"),
369    (Zwl, b"ZWL", "$")
370];