use std::{fmt, str::FromStr};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{self, MapAccess, Unexpected, Visitor},
ser::{SerializeMap, SerializeStruct},
};
use crate::{
Currency, Decimal, Exchange, ExchangeRate, IsoAlphabeticCode, IsoNumericCode, Money, MoneyBag,
RoundingMode,
};
const AMOUNT_FIELD: &str = "amount";
const CURRENCY_FIELD: &str = "currency";
const MONEY_FIELDS: &[&str] = &[AMOUNT_FIELD, CURRENCY_FIELD];
const FROM_FIELD: &str = "from";
const TO_FIELD: &str = "to";
const RATE_FIELD: &str = "rate";
const EXCHANGE_RATE_FIELDS: &[&str] = &[FROM_FIELD, TO_FIELD, RATE_FIELD];
const PAIR_SEPARATOR: char = '/';
struct Figure(Decimal);
impl Serialize for Figure {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self.0)
}
}
impl<'de> Deserialize<'de> for Figure {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(FigureVisitor)
}
}
struct FigureVisitor;
impl Visitor<'_> for FigureVisitor {
type Value = Figure;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal figure, written as text or as a number")
}
fn visit_str<E: de::Error>(self, figure: &str) -> Result<Self::Value, E> {
Decimal::from_str(figure)
.or_else(|_| Decimal::from_scientific(figure))
.map(Figure)
.map_err(|_| E::invalid_value(Unexpected::Str(figure), &self))
}
fn visit_u64<E: de::Error>(self, figure: u64) -> Result<Self::Value, E> {
Ok(Figure(Decimal::from(figure)))
}
fn visit_i64<E: de::Error>(self, figure: i64) -> Result<Self::Value, E> {
Ok(Figure(Decimal::from(figure)))
}
fn visit_f64<E: de::Error>(self, figure: f64) -> Result<Self::Value, E> {
Decimal::from_str(&figure.to_string())
.map(Figure)
.map_err(|_| E::invalid_value(Unexpected::Float(figure), &self))
}
}
impl Serialize for Money {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut money = serializer.serialize_struct("Money", MONEY_FIELDS.len())?;
money.serialize_field(AMOUNT_FIELD, &Figure(self.amount()))?;
money.serialize_field(CURRENCY_FIELD, &self.currency())?;
money.end()
}
}
impl<'de> Deserialize<'de> for Money {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct("Money", MONEY_FIELDS, MoneyVisitor)
}
}
struct MoneyVisitor;
impl<'de> Visitor<'de> for MoneyVisitor {
type Value = Money;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an amount and the currency it is denominated in")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut amount: Option<Figure> = None;
let mut currency: Option<Currency> = None;
while let Some(field) = map.next_key()? {
match field {
MoneyField::Amount if amount.is_some() => {
return Err(de::Error::duplicate_field(AMOUNT_FIELD));
}
MoneyField::Currency if currency.is_some() => {
return Err(de::Error::duplicate_field(CURRENCY_FIELD));
}
MoneyField::Amount => amount = Some(map.next_value()?),
MoneyField::Currency => currency = Some(map.next_value()?),
MoneyField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
let Figure(amount) = amount.ok_or_else(|| de::Error::missing_field(AMOUNT_FIELD))?;
let currency = currency.ok_or_else(|| de::Error::missing_field(CURRENCY_FIELD))?;
Ok(Money::from_decimal(amount, ¤cy))
}
}
enum MoneyField {
Amount,
Currency,
Other,
}
impl<'de> Deserialize<'de> for MoneyField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_identifier(MoneyFieldVisitor)
}
}
struct MoneyFieldVisitor;
impl Visitor<'_> for MoneyFieldVisitor {
type Value = MoneyField;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a field name")
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
Ok(match name {
AMOUNT_FIELD => MoneyField::Amount,
CURRENCY_FIELD => MoneyField::Currency,
_ => MoneyField::Other,
})
}
}
impl Serialize for MoneyBag {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut balances = serializer.serialize_map(Some(self.len()))?;
for money in self {
balances.serialize_entry(&money.currency(), &Figure(money.amount()))?;
}
balances.end()
}
}
impl<'de> Deserialize<'de> for MoneyBag {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(MoneyBagVisitor)
}
}
struct MoneyBagVisitor;
impl<'de> Visitor<'de> for MoneyBagVisitor {
type Value = MoneyBag;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a balance per currency, keyed by ISO 4217 alphabetic code")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut bag = MoneyBag::new();
while let Some((currency, Figure(amount))) = map.next_entry::<Currency, Figure>()? {
bag = bag
.checked_add(Money::from_decimal(amount, ¤cy))
.map_err(|_| {
de::Error::custom(format!(
"the {currency} balance totals past what a decimal can hold"
))
})?;
}
Ok(bag)
}
}
impl Serialize for ExchangeRate {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut quote = serializer.serialize_struct("ExchangeRate", EXCHANGE_RATE_FIELDS.len())?;
quote.serialize_field(FROM_FIELD, &self.from())?;
quote.serialize_field(TO_FIELD, &self.to())?;
quote.serialize_field(RATE_FIELD, &Figure(self.rate()))?;
quote.end()
}
}
impl<'de> Deserialize<'de> for ExchangeRate {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct("ExchangeRate", EXCHANGE_RATE_FIELDS, ExchangeRateVisitor)
}
}
struct ExchangeRateVisitor;
impl<'de> Visitor<'de> for ExchangeRateVisitor {
type Value = ExchangeRate;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a pair of currencies and the rate between them")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut from: Option<Currency> = None;
let mut to: Option<Currency> = None;
let mut rate: Option<Figure> = None;
while let Some(field) = map.next_key()? {
match field {
ExchangeRateField::From if from.is_some() => {
return Err(de::Error::duplicate_field(FROM_FIELD));
}
ExchangeRateField::To if to.is_some() => {
return Err(de::Error::duplicate_field(TO_FIELD));
}
ExchangeRateField::Rate if rate.is_some() => {
return Err(de::Error::duplicate_field(RATE_FIELD));
}
ExchangeRateField::From => from = Some(map.next_value()?),
ExchangeRateField::To => to = Some(map.next_value()?),
ExchangeRateField::Rate => rate = Some(map.next_value()?),
ExchangeRateField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
let from = from.ok_or_else(|| de::Error::missing_field(FROM_FIELD))?;
let to = to.ok_or_else(|| de::Error::missing_field(TO_FIELD))?;
let Figure(rate) = rate.ok_or_else(|| de::Error::missing_field(RATE_FIELD))?;
ExchangeRate::new(from, to, rate).map_err(de::Error::custom)
}
}
enum ExchangeRateField {
From,
To,
Rate,
Other,
}
impl<'de> Deserialize<'de> for ExchangeRateField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_identifier(ExchangeRateFieldVisitor)
}
}
struct ExchangeRateFieldVisitor;
impl Visitor<'_> for ExchangeRateFieldVisitor {
type Value = ExchangeRateField;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a field name")
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
Ok(match name {
FROM_FIELD => ExchangeRateField::From,
TO_FIELD => ExchangeRateField::To,
RATE_FIELD => ExchangeRateField::Rate,
_ => ExchangeRateField::Other,
})
}
}
struct Pair(Currency, Currency);
impl Serialize for Pair {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let Self(from, to) = self;
serializer.collect_str(&format_args!("{from}{PAIR_SEPARATOR}{to}"))
}
}
impl<'de> Deserialize<'de> for Pair {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(PairVisitor)
}
}
struct PairVisitor;
impl Visitor<'_> for PairVisitor {
type Value = Pair;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"two currencies ISO 4217 assigns, separated by {PAIR_SEPARATOR:?}"
)
}
fn visit_str<E: de::Error>(self, pair: &str) -> Result<Self::Value, E> {
pair.split_once(PAIR_SEPARATOR)
.and_then(|(from, to)| {
Some(Pair(
Currency::from_alphabetic_code(from)?,
Currency::from_alphabetic_code(to)?,
))
})
.ok_or_else(|| E::invalid_value(Unexpected::Str(pair), &self))
}
}
impl Serialize for Exchange {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut table = serializer.serialize_map(Some(self.quotes().len()))?;
for quote in self.quotes() {
table.serialize_entry(&Pair(quote.from(), quote.to()), &Figure(quote.rate()))?;
}
table.end()
}
}
impl<'de> Deserialize<'de> for Exchange {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(ExchangeVisitor)
}
}
struct ExchangeVisitor;
impl<'de> Visitor<'de> for ExchangeVisitor {
type Value = Exchange;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a rate per currency pair, keyed by the pair the rate spans")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut desk = Exchange::new();
while let Some((Pair(from, to), Figure(rate))) = map.next_entry::<Pair, Figure>()? {
let quote = ExchangeRate::new(from, to, rate).map_err(de::Error::custom)?;
desk.set_rate("e);
}
Ok(desk)
}
}
impl Serialize for Currency {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.alphabetic_code().as_str())
}
}
impl<'de> Deserialize<'de> for Currency {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(CurrencyVisitor)
}
}
struct CurrencyVisitor;
impl Visitor<'_> for CurrencyVisitor {
type Value = Currency;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("the alphabetic code of a currency ISO 4217 assigns")
}
fn visit_str<E: de::Error>(self, code: &str) -> Result<Self::Value, E> {
Currency::from_alphabetic_code(code)
.ok_or_else(|| E::invalid_value(Unexpected::Str(code), &self))
}
}
impl Serialize for IsoAlphabeticCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for IsoAlphabeticCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(IsoAlphabeticCodeVisitor)
}
}
struct IsoAlphabeticCodeVisitor;
impl Visitor<'_> for IsoAlphabeticCodeVisitor {
type Value = IsoAlphabeticCode;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("three capital letters, as ISO 4217 spells its codes")
}
fn visit_str<E: de::Error>(self, code: &str) -> Result<Self::Value, E> {
IsoAlphabeticCode::try_from(code)
.map_err(|_| E::invalid_value(Unexpected::Str(code), &self))
}
}
impl Serialize for IsoNumericCode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_u32(self.value())
}
}
impl<'de> Deserialize<'de> for IsoNumericCode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_u32(IsoNumericCodeVisitor)
}
}
struct IsoNumericCodeVisitor;
impl Visitor<'_> for IsoNumericCodeVisitor {
type Value = IsoNumericCode;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("an integer of at most three digits, as ISO 4217 numbers currencies")
}
fn visit_u64<E: de::Error>(self, code: u64) -> Result<Self::Value, E> {
u32::try_from(code)
.ok()
.and_then(|code| IsoNumericCode::try_from(code).ok())
.ok_or_else(|| E::invalid_value(Unexpected::Unsigned(code), &self))
}
fn visit_i64<E: de::Error>(self, code: i64) -> Result<Self::Value, E> {
match u64::try_from(code) {
Ok(code) => self.visit_u64(code),
Err(_) => Err(E::invalid_value(Unexpected::Signed(code), &self)),
}
}
}
impl Serialize for RoundingMode {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(match self {
RoundingMode::HalfUp => "half-up",
RoundingMode::HalfDown => "half-down",
RoundingMode::HalfEven => "half-even",
})
}
}
impl<'de> Deserialize<'de> for RoundingMode {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(RoundingModeVisitor)
}
}
struct RoundingModeVisitor;
impl Visitor<'_> for RoundingModeVisitor {
type Value = RoundingMode;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#""half-up", "half-down", or "half-even""#)
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"half-up" => Ok(RoundingMode::HalfUp),
"half-down" => Ok(RoundingMode::HalfDown),
"half-even" => Ok(RoundingMode::HalfEven),
_ => Err(E::invalid_value(Unexpected::Str(name), &self)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InvalidRateError;
use rust_decimal::prelude::*;
#[test]
fn money_round_trips_test() {
let total = Money::from_minor(10475, &Currency::USD);
let document = serde_json::to_string(&total).unwrap();
assert_eq!(document, r#"{"amount":"104.75","currency":"USD"}"#);
assert_eq!(serde_json::from_str::<Money>(&document).unwrap(), total);
}
#[test]
fn money_keeps_a_scale_finer_than_the_currency_test() {
let share = Money::from_decimal(dec!(1.005), &Currency::USD);
let document = serde_json::to_string(&share).unwrap();
assert_eq!(document, r#"{"amount":"1.005","currency":"USD"}"#);
assert_eq!(
serde_json::from_str::<Money>(&document)
.unwrap()
.amount()
.scale(),
3
);
}
#[test]
fn money_reads_numeric_amounts_test() {
let whole: Money = serde_json::from_str(r#"{"amount": 104, "currency": "USD"}"#).unwrap();
let fraction: Money =
serde_json::from_str(r#"{"amount": 104.75, "currency": "USD"}"#).unwrap();
assert_eq!(whole, Money::from_major(104, &Currency::USD));
assert_eq!(fraction, Money::from_minor(10475, &Currency::USD));
}
#[test]
fn money_reads_an_amount_in_scientific_notation_test() {
let thousand: Money =
serde_json::from_str(r#"{"amount": "1e3", "currency": "USD"}"#).unwrap();
assert_eq!(thousand.amount(), dec!(1000));
}
#[test]
fn money_reads_its_fields_in_either_order_test() {
let reversed: Money =
serde_json::from_str(r#"{"currency": "EUR", "amount": "12.00"}"#).unwrap();
assert_eq!(reversed, Money::from_major(12, &Currency::EUR));
}
#[test]
fn money_skips_unknown_fields_test() {
let annotated: Money = serde_json::from_str(
r#"{"amount": "1.00", "note": {"paid": true}, "currency": "USD"}"#,
)
.unwrap();
assert_eq!(annotated, Money::from_major(1, &Currency::USD));
}
#[test]
fn money_needs_both_fields_test() {
let no_currency = serde_json::from_str::<Money>(r#"{"amount": "1.00"}"#).unwrap_err();
let no_amount = serde_json::from_str::<Money>(r#"{"currency": "USD"}"#).unwrap_err();
assert!(no_currency.to_string().contains("missing field `currency`"));
assert!(no_amount.to_string().contains("missing field `amount`"));
}
#[test]
fn money_refuses_a_repeated_field_test() {
let repeated = serde_json::from_str::<Money>(
r#"{"amount": "1.00", "amount": "2.00", "currency": "USD"}"#,
)
.unwrap_err();
assert!(repeated.to_string().contains("duplicate field `amount`"));
}
#[test]
fn every_currency_round_trips_test() {
for currency in Currency::all() {
let document = serde_json::to_string(currency).unwrap();
assert_eq!(document, format!("\"{currency}\""));
assert_eq!(
&serde_json::from_str::<Currency>(&document).unwrap(),
currency
);
}
}
#[test]
fn unassigned_currency_code_is_refused_test() {
let error = serde_json::from_str::<Currency>(r#""ZZZ""#).unwrap_err();
assert!(
error
.to_string()
.contains(r#"invalid value: string "ZZZ", expected the alphabetic code"#)
);
}
#[test]
fn alphabetic_code_admits_unassigned_codes_test() {
let unassigned: IsoAlphabeticCode = serde_json::from_str(r#""ZZZ""#).unwrap();
assert_eq!(unassigned.as_str(), "ZZZ");
assert_eq!(serde_json::to_string(&unassigned).unwrap(), r#""ZZZ""#);
assert!(serde_json::from_str::<IsoAlphabeticCode>(r#""usd""#).is_err());
assert!(serde_json::from_str::<IsoAlphabeticCode>(r#""USDD""#).is_err());
}
#[test]
fn numeric_code_round_trips_test() {
for currency in Currency::all() {
let code = currency.numeric_code();
let document = serde_json::to_string(&code).unwrap();
assert_eq!(document, code.value().to_string());
assert_eq!(
serde_json::from_str::<IsoNumericCode>(&document).unwrap(),
code
);
}
}
#[test]
fn numeric_code_outside_three_digits_is_refused_test() {
let too_many = serde_json::from_str::<IsoNumericCode>("1000").unwrap_err();
let negative = serde_json::from_str::<IsoNumericCode>("-1").unwrap_err();
assert!(
too_many
.to_string()
.contains("invalid value: integer `1000`")
);
assert!(negative.to_string().contains("invalid value: integer `-1`"));
}
#[test]
fn rounding_mode_round_trips_test() {
let modes = [
(RoundingMode::HalfUp, r#""half-up""#),
(RoundingMode::HalfDown, r#""half-down""#),
(RoundingMode::HalfEven, r#""half-even""#),
];
for (mode, document) in modes {
assert_eq!(serde_json::to_string(&mode).unwrap(), document);
assert_eq!(
serde_json::from_str::<RoundingMode>(document).unwrap(),
mode
);
}
assert!(serde_json::from_str::<RoundingMode>(r#""HalfUp""#).is_err());
}
#[test]
fn bag_round_trips_test() {
let wallet: MoneyBag = [
Money::from_major(30, &Currency::USD),
Money::from_major(10, &Currency::EUR),
]
.into_iter()
.collect();
let document = serde_json::to_string(&wallet).unwrap();
assert_eq!(document, r#"{"EUR":"10.00","USD":"30.00"}"#);
assert_eq!(serde_json::from_str::<MoneyBag>(&document).unwrap(), wallet);
}
#[test]
fn empty_bag_round_trips_test() {
let document = serde_json::to_string(&MoneyBag::new()).unwrap();
assert_eq!(document, "{}");
assert!(
serde_json::from_str::<MoneyBag>(&document)
.unwrap()
.is_empty()
);
}
#[test]
fn bag_drops_a_zero_balance_test() {
let wallet: MoneyBag = serde_json::from_str(r#"{"EUR": "0.00", "USD": "10.00"}"#).unwrap();
assert_eq!(wallet.len(), 1);
assert_eq!(
wallet,
MoneyBag::from(Money::from_major(10, &Currency::USD))
);
}
#[test]
fn bag_sums_a_repeated_currency_test() {
let wallet: MoneyBag = serde_json::from_str(r#"{"USD": "10.00", "USD": "5.00"}"#).unwrap();
assert_eq!(
wallet.balance(&Currency::USD),
Money::from_major(15, &Currency::USD)
);
}
#[test]
fn bag_reports_a_balance_it_cannot_hold_test() {
let document = format!(r#"{{"USD": "{}", "USD": "1"}}"#, Decimal::MAX);
let error = serde_json::from_str::<MoneyBag>(&document).unwrap_err();
assert!(
error
.to_string()
.contains("the USD balance totals past what a decimal can hold")
);
}
#[test]
fn bag_refuses_an_unassigned_currency_test() {
assert!(serde_json::from_str::<MoneyBag>(r#"{"ZZZ": "1.00"}"#).is_err());
}
fn usd_eur() -> ExchangeRate {
ExchangeRate::new(Currency::USD, Currency::EUR, dec!(0.9)).unwrap()
}
#[test]
fn rate_round_trips_test() {
let document = serde_json::to_string(&usd_eur()).unwrap();
assert_eq!(document, r#"{"from":"USD","to":"EUR","rate":"0.9"}"#);
assert_eq!(
serde_json::from_str::<ExchangeRate>(&document).unwrap(),
usd_eur()
);
}
#[test]
fn rate_reads_a_numeric_multiplier_test() {
let quoted: ExchangeRate =
serde_json::from_str(r#"{"from": "USD", "to": "EUR", "rate": 0.9}"#).unwrap();
assert_eq!(quoted, usd_eur());
}
#[test]
fn rate_reads_its_fields_in_any_order_test() {
let shuffled: ExchangeRate =
serde_json::from_str(r#"{"rate": "0.9", "to": "EUR", "from": "USD"}"#).unwrap();
assert_eq!(shuffled, usd_eur());
}
#[test]
fn rate_skips_unknown_fields_test() {
let annotated: ExchangeRate = serde_json::from_str(
r#"{"as_of": "2026-08-14", "from": "USD", "to": "EUR", "rate": "0.9"}"#,
)
.unwrap();
assert_eq!(annotated, usd_eur());
}
#[test]
fn rate_needs_all_three_fields_test() {
let no_rate =
serde_json::from_str::<ExchangeRate>(r#"{"from": "USD", "to": "EUR"}"#).unwrap_err();
let no_from =
serde_json::from_str::<ExchangeRate>(r#"{"to": "EUR", "rate": "0.9"}"#).unwrap_err();
let no_to =
serde_json::from_str::<ExchangeRate>(r#"{"from": "USD", "rate": "0.9"}"#).unwrap_err();
assert!(no_rate.to_string().contains("missing field `rate`"));
assert!(no_from.to_string().contains("missing field `from`"));
assert!(no_to.to_string().contains("missing field `to`"));
}
#[test]
fn rate_refuses_a_repeated_field_test() {
let repeated = serde_json::from_str::<ExchangeRate>(
r#"{"from": "USD", "to": "EUR", "rate": "0.9", "rate": "0.92"}"#,
)
.unwrap_err();
assert!(repeated.to_string().contains("duplicate field `rate`"));
}
#[test]
fn rate_refuses_a_multiplier_no_exchange_takes_place_at_test() {
let zero =
serde_json::from_str::<ExchangeRate>(r#"{"from": "USD", "to": "EUR", "rate": "0"}"#)
.unwrap_err();
let negative =
serde_json::from_str::<ExchangeRate>(r#"{"from": "USD", "to": "EUR", "rate": "-0.9"}"#)
.unwrap_err();
assert!(zero.to_string().contains(&InvalidRateError.to_string()));
assert!(negative.to_string().contains(&InvalidRateError.to_string()));
}
#[test]
fn identity_round_trips_test() {
let par = ExchangeRate::identity(Currency::USD);
let document = serde_json::to_string(&par).unwrap();
assert_eq!(document, r#"{"from":"USD","to":"USD","rate":"1"}"#);
assert_eq!(
serde_json::from_str::<ExchangeRate>(&document).unwrap(),
par
);
}
#[test]
fn exchange_round_trips_test() {
let mut desk = Exchange::new();
desk.set_rate(&usd_eur());
desk.set_rate(&ExchangeRate::new(Currency::USD, Currency::JPY, dec!(144)).unwrap());
desk.set_rate(&ExchangeRate::new(Currency::EUR, Currency::USD, dec!(1.1)).unwrap());
let document = serde_json::to_string(&desk).unwrap();
assert_eq!(
document,
r#"{"EUR/USD":"1.1","USD/EUR":"0.9","USD/JPY":"144"}"#
);
assert_eq!(serde_json::from_str::<Exchange>(&document).unwrap(), desk);
}
#[test]
fn empty_exchange_round_trips_test() {
let document = serde_json::to_string(&Exchange::new()).unwrap();
assert_eq!(document, "{}");
assert_eq!(
serde_json::from_str::<Exchange>(&document).unwrap(),
Exchange::new()
);
}
#[test]
fn exchange_keeps_the_last_quote_of_a_repeated_pair_test() {
let desk: Exchange =
serde_json::from_str(r#"{"USD/EUR": "0.9", "USD/EUR": "0.92"}"#).unwrap();
let quote = desk.rate(&Currency::USD, &Currency::EUR).unwrap();
assert_eq!(quote.rate(), dec!(0.92));
}
#[test]
fn exchange_omits_the_par_it_answers_from_the_rule_test() {
let desk: Exchange = serde_json::from_str(r#"{"USD/EUR": "0.9"}"#).unwrap();
assert!(desk.rate(&Currency::USD, &Currency::USD).is_some());
assert_eq!(
serde_json::to_string(&desk).unwrap(),
r#"{"USD/EUR":"0.9"}"#
);
}
#[test]
fn exchange_refuses_a_key_that_is_not_a_pair_test() {
assert!(serde_json::from_str::<Exchange>(r#"{"USD": "0.9"}"#).is_err());
assert!(serde_json::from_str::<Exchange>(r#"{"USD/ZZZ": "0.9"}"#).is_err());
assert!(serde_json::from_str::<Exchange>(r#"{"usd/eur": "0.9"}"#).is_err());
}
#[test]
fn exchange_refuses_a_multiplier_no_exchange_takes_place_at_test() {
let error = serde_json::from_str::<Exchange>(r#"{"USD/EUR": "0"}"#).unwrap_err();
assert!(error.to_string().contains(&InvalidRateError.to_string()));
}
}