use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sofie_core::{Cast, ColumnReader, ColumnSpec, Error, Result, Show, Shown, SqlValue};
use sofie_core::deserialize_parsed;
use crate::refusal::refusal;
#[rustfmt::skip]
const CODES: [([u8; 3], u8); 165] = [
(*b"AED", 2), (*b"AFN", 2), (*b"ALL", 2), (*b"AMD", 2), (*b"AOA", 2), (*b"ARS", 2),
(*b"AUD", 2), (*b"AWG", 2), (*b"AZN", 2), (*b"BAM", 2), (*b"BBD", 2), (*b"BDT", 2),
(*b"BHD", 3), (*b"BIF", 0), (*b"BMD", 2), (*b"BND", 2), (*b"BOB", 2), (*b"BOV", 2),
(*b"BRL", 2), (*b"BSD", 2), (*b"BTN", 2), (*b"BWP", 2), (*b"BYN", 2), (*b"BZD", 2),
(*b"CAD", 2), (*b"CDF", 2), (*b"CHE", 2), (*b"CHF", 2), (*b"CHW", 2), (*b"CLF", 4),
(*b"CLP", 0), (*b"CNY", 2), (*b"COP", 2), (*b"COU", 2), (*b"CRC", 2), (*b"CUP", 2),
(*b"CVE", 2), (*b"CZK", 2), (*b"DJF", 0), (*b"DKK", 2), (*b"DOP", 2), (*b"DZD", 2),
(*b"EGP", 2), (*b"ERN", 2), (*b"ETB", 2), (*b"EUR", 2), (*b"FJD", 2), (*b"FKP", 2),
(*b"GBP", 2), (*b"GEL", 2), (*b"GHS", 2), (*b"GIP", 2), (*b"GMD", 2), (*b"GNF", 0),
(*b"GTQ", 2), (*b"GYD", 2), (*b"HKD", 2), (*b"HNL", 2), (*b"HTG", 2), (*b"HUF", 2),
(*b"IDR", 2), (*b"ILS", 2), (*b"INR", 2), (*b"IQD", 3), (*b"IRR", 2), (*b"ISK", 0),
(*b"JMD", 2), (*b"JOD", 3), (*b"JPY", 0), (*b"KES", 2), (*b"KGS", 2), (*b"KHR", 2),
(*b"KMF", 0), (*b"KPW", 2), (*b"KRW", 0), (*b"KWD", 3), (*b"KYD", 2), (*b"KZT", 2),
(*b"LAK", 2), (*b"LBP", 2), (*b"LKR", 2), (*b"LRD", 2), (*b"LSL", 2), (*b"LYD", 3),
(*b"MAD", 2), (*b"MDL", 2), (*b"MGA", 2), (*b"MKD", 2), (*b"MMK", 2), (*b"MNT", 2),
(*b"MOP", 2), (*b"MRU", 2), (*b"MUR", 2), (*b"MVR", 2), (*b"MWK", 2), (*b"MXN", 2),
(*b"MXV", 2), (*b"MYR", 2), (*b"MZN", 2), (*b"NAD", 2), (*b"NGN", 2), (*b"NIO", 2),
(*b"NOK", 2), (*b"NPR", 2), (*b"NZD", 2), (*b"OMR", 3), (*b"PAB", 2), (*b"PEN", 2),
(*b"PGK", 2), (*b"PHP", 2), (*b"PKR", 2), (*b"PLN", 2), (*b"PYG", 0), (*b"QAR", 2),
(*b"RON", 2), (*b"RSD", 2), (*b"RUB", 2), (*b"RWF", 0), (*b"SAR", 2), (*b"SBD", 2),
(*b"SCR", 2), (*b"SDG", 2), (*b"SEK", 2), (*b"SGD", 2), (*b"SHP", 2), (*b"SLE", 2),
(*b"SOS", 2), (*b"SRD", 2), (*b"SSP", 2), (*b"STN", 2), (*b"SVC", 2), (*b"SYP", 2),
(*b"SZL", 2), (*b"THB", 2), (*b"TJS", 2), (*b"TMT", 2), (*b"TND", 3), (*b"TOP", 2),
(*b"TRY", 2), (*b"TTD", 2), (*b"TWD", 2), (*b"TZS", 2), (*b"UAH", 2), (*b"UGX", 0),
(*b"USD", 2), (*b"USN", 2), (*b"UYI", 0), (*b"UYU", 2), (*b"UYW", 4), (*b"UZS", 2),
(*b"VED", 2), (*b"VES", 2), (*b"VND", 0), (*b"VUV", 0), (*b"WST", 2), (*b"XAD", 2),
(*b"XAF", 0), (*b"XCD", 2), (*b"XCG", 2), (*b"XOF", 0), (*b"XPF", 0), (*b"YER", 2),
(*b"ZAR", 2), (*b"ZMW", 2), (*b"ZWG", 2),
];
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Currency {
code: [u8; 3],
minor_digits: u8,
}
impl Currency {
pub fn parse(input: &str) -> Result<Self> {
let index = CODES
.binary_search_by(|(code, _)| code.as_slice().cmp(input.as_bytes()))
.map_err(|_| invalid_currency())?;
let (code, minor_digits) = CODES[index];
Ok(Self { code, minor_digits })
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.code).expect("a currency code is ASCII letters")
}
#[must_use]
pub fn minor_digits(&self) -> u8 {
self.minor_digits
}
pub fn all() -> impl Iterator<Item = Self> {
CODES
.iter()
.map(|&(code, minor_digits)| Self { code, minor_digits })
}
}
fn invalid_currency() -> Error {
refusal(
"currency",
"validation.currency.invalid",
"must be an ISO 4217 code in use, in uppercase, such as EUR",
)
}
impl fmt::Display for Currency {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl fmt::Debug for Currency {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "Currency({})", self.as_str())
}
}
impl Show for Currency {
fn show(&self) -> Shown<'_> {
Shown::Text(self.as_str())
}
}
impl Cast for Currency {
const COLUMNS: &'static [ColumnSpec] = String::COLUMNS;
fn encode(&self, out: &mut Vec<SqlValue>) {
out.push(SqlValue::Text(self.as_str().to_owned()));
}
fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
Self::parse(row.text()?)
.map_err(|error| Error::internal("stored currency is not an ISO 4217 code", error))
}
}
impl Serialize for Currency {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Currency {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserialize_parsed(deserializer, "an ISO 4217 currency code", Self::parse)
}
}
#[cfg(test)]
mod tests {
use serde::de::{self, Visitor};
use sofie_core::RowReader;
use super::*;
use crate::testing::{assert_refused, round_trip};
#[test]
fn accepts_codes_in_use() {
for code in ["EUR", "USD", "TRY", "JPY"] {
assert_eq!(Currency::parse(code).unwrap().as_str(), code);
}
}
#[test]
fn rejects_anything_else() {
assert_refused(
Currency::parse,
&[
"", "EU", "EURO", "eur", "Eur", "E1R", " EUR", "ÉUR", "€", "QQQ", "XXX", "XAU",
"DEM", "BGN",
],
);
}
#[test]
fn knows_the_digits_of_its_minor_unit() {
for (code, digits) in [
("EUR", 2),
("USD", 2),
("JPY", 0),
("XPF", 0),
("KWD", 3),
("CLF", 4),
("UYW", 4),
("UYI", 0),
] {
assert_eq!(
Currency::parse(code).unwrap().minor_digits(),
digits,
"{code}"
);
}
}
#[test]
fn displays_as_its_code() {
assert_eq!(Currency::parse("GBP").unwrap().to_string(), "GBP");
}
#[test]
fn round_trips_through_one_text_column() {
let currency = Currency::parse("EUR").unwrap();
assert_eq!(round_trip(¤cy), vec![SqlValue::Text("EUR".into())]);
}
#[test]
fn an_invalid_stored_code_is_an_internal_error() {
let values = [SqlValue::Text("euro".into())];
let mut row = RowReader::new(&values);
assert!(matches!(
Currency::decode(&mut row),
Err(Error::Internal { .. })
));
}
#[test]
fn round_trips_through_json_as_a_string() {
let currency = Currency::parse("USD").unwrap();
let json = serde_json::to_string(¤cy).unwrap();
assert_eq!(json, "\"USD\"");
assert_eq!(serde_json::from_str::<Currency>(&json).unwrap(), currency);
}
#[test]
fn json_deserialization_validates_the_code() {
assert!(serde_json::from_str::<Currency>("\"usd\"").is_err());
}
struct BorrowedOnly<'de>(&'de str);
impl<'de> Deserializer<'de> for BorrowedOnly<'de> {
type Error = serde::de::value::Error;
fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value, Self::Error> {
Err(de::Error::custom("only a borrowed str is offered"))
}
fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_borrowed_str(self.0)
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char string bytes byte_buf
option unit unit_struct newtype_struct seq tuple tuple_struct map struct enum
identifier ignored_any
}
}
#[test]
fn deserializes_from_a_borrowed_str() {
let currency = Currency::deserialize(BorrowedOnly("JPY")).unwrap();
assert_eq!(currency, Currency::parse("JPY").unwrap());
assert!(Currency::deserialize(BorrowedOnly("jpy")).is_err());
}
#[test]
fn every_listed_code_reads_back_as_written() {
for (code, digits) in CODES {
let code = std::str::from_utf8(&code).unwrap();
let currency = Currency::parse(code).unwrap();
assert_eq!(currency.as_str(), code);
assert_eq!(currency.minor_digits(), digits, "{code}");
}
}
#[test]
fn the_listed_codes_are_sorted_uppercase_letters() {
assert!(CODES.is_sorted_by(|(first, _), (second, _)| first < second));
assert!(
CODES
.iter()
.all(|(code, _)| code.iter().all(u8::is_ascii_uppercase))
);
}
}