Skip to main content

ocpi_tariffs/
currency.rs

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