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,
Pair, ParseCurrencyError, RoundingMode,
};
const AMOUNT_FIELD: &str = "amount";
const CURRENCY_FIELD: &str = "currency";
const MONEY_FIELDS: &[&str] = &[AMOUNT_FIELD, CURRENCY_FIELD];
const CODE_SPELLING: &str = "three capital letters, as ISO 4217 writes its codes";
const ASSIGNED_CODE: &str = "a code ISO 4217 assigns to a currency";
const BASE_FIELD: &str = "base";
const QUOTE_FIELD: &str = "quote";
const RATE_FIELD: &str = "rate";
const EXCHANGE_RATE_FIELDS: &[&str] = &[BASE_FIELD, QUOTE_FIELD, RATE_FIELD];
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, 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 its currency")
}
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, currency))
}
}
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, currency))
.map_err(de::Error::custom)?;
}
Ok(bag)
}
}
impl Serialize for ExchangeRate {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut fields = serializer.serialize_struct("ExchangeRate", EXCHANGE_RATE_FIELDS.len())?;
fields.serialize_field(BASE_FIELD, &self.base())?;
fields.serialize_field(QUOTE_FIELD, &self.quote())?;
fields.serialize_field(RATE_FIELD, &Figure(self.rate()))?;
fields.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 base: Option<Currency> = None;
let mut quote: Option<Currency> = None;
let mut rate: Option<Figure> = None;
while let Some(field) = map.next_key()? {
match field {
ExchangeRateField::Base if base.is_some() => {
return Err(de::Error::duplicate_field(BASE_FIELD));
}
ExchangeRateField::Quote if quote.is_some() => {
return Err(de::Error::duplicate_field(QUOTE_FIELD));
}
ExchangeRateField::Rate if rate.is_some() => {
return Err(de::Error::duplicate_field(RATE_FIELD));
}
ExchangeRateField::Base => base = Some(map.next_value()?),
ExchangeRateField::Quote => quote = Some(map.next_value()?),
ExchangeRateField::Rate => rate = Some(map.next_value()?),
ExchangeRateField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
let base = base.ok_or_else(|| de::Error::missing_field(BASE_FIELD))?;
let quote = quote.ok_or_else(|| de::Error::missing_field(QUOTE_FIELD))?;
let Figure(rate) = rate.ok_or_else(|| de::Error::missing_field(RATE_FIELD))?;
ExchangeRate::new((base, quote), rate).map_err(de::Error::custom)
}
}
enum ExchangeRateField {
Base,
Quote,
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 {
BASE_FIELD => ExchangeRateField::Base,
QUOTE_FIELD => ExchangeRateField::Quote,
RATE_FIELD => ExchangeRateField::Rate,
_ => ExchangeRateField::Other,
})
}
}
impl Serialize for Pair {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
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 {
f.write_str(r#"a currency pair, such as "USD/EUR""#)
}
fn visit_str<E: de::Error>(self, pair: &str) -> Result<Self::Value, E> {
pair.parse().map_err(de::Error::custom)
}
}
impl Serialize for Exchange {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut table = serializer.serialize_map(Some(self.len()))?;
for rate in self {
table.serialize_entry(&rate.pair(), &Figure(rate.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")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut desk = Exchange::new();
while let Some((pair, Figure(rate))) = map.next_entry::<Pair, Figure>()? {
let rate = ExchangeRate::new(pair, rate).map_err(de::Error::custom)?;
desk.set_rate(rate);
}
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 an ISO 4217 currency")
}
fn visit_str<E: de::Error>(self, code: &str) -> Result<Self::Value, E> {
code.parse().map_err(|error| {
let expected: &dyn de::Expected = match error {
ParseCurrencyError::Code { .. } => &CODE_SPELLING,
ParseCurrencyError::UnknownCurrency { .. } => &ASSIGNED_CODE,
};
E::invalid_value(Unexpected::Str(code), expected)
})
}
}
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(CODE_SPELLING)
}
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("a whole number of at most three digits, as ISO 4217 writes its codes")
}
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::ExchangeRateError;
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(&format!(
r#"invalid value: string "ZZZ", expected {ASSIGNED_CODE}"#
)));
}
#[test]
fn misspelled_currency_code_is_refused_in_different_words_test() {
let error = serde_json::from_str::<Currency>(r#""usd""#).unwrap_err();
assert!(error.to_string().contains(&format!(
r#"invalid value: string "usd", expected {CODE_SPELLING}"#
)));
}
#[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 is too large for a decimal")
);
}
#[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#"{"base":"USD","quote":"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#"{"base": "USD", "quote": "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", "quote": "EUR", "base": "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", "base": "USD", "quote": "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#"{"base": "USD", "quote": "EUR"}"#).unwrap_err();
let no_base =
serde_json::from_str::<ExchangeRate>(r#"{"quote": "EUR", "rate": "0.9"}"#).unwrap_err();
let no_quote =
serde_json::from_str::<ExchangeRate>(r#"{"base": "USD", "rate": "0.9"}"#).unwrap_err();
assert!(no_rate.to_string().contains("missing field `rate`"));
assert!(no_base.to_string().contains("missing field `base`"));
assert!(no_quote.to_string().contains("missing field `quote`"));
}
#[test]
fn rate_refuses_a_repeated_field_test() {
let repeated = serde_json::from_str::<ExchangeRate>(
r#"{"base": "USD", "quote": "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#"{"base": "USD", "quote": "EUR", "rate": "0"}"#)
.unwrap_err();
let negative = serde_json::from_str::<ExchangeRate>(
r#"{"base": "USD", "quote": "EUR", "rate": "-0.9"}"#,
)
.unwrap_err();
assert!(
zero.to_string()
.contains(&ExchangeRateError::InvalidRate { rate: dec!(0) }.to_string())
);
assert!(
negative
.to_string()
.contains(&ExchangeRateError::InvalidRate { rate: dec!(-0.9) }.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#"{"base":"USD","quote":"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_rate_of_a_repeated_pair_test() {
let desk: Exchange =
serde_json::from_str(r#"{"USD/EUR": "0.9", "USD/EUR": "0.92"}"#).unwrap();
let rate = desk.rate((Currency::USD, Currency::EUR)).unwrap();
assert_eq!(rate.rate(), dec!(0.92));
}
#[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(&ExchangeRateError::InvalidRate { rate: dec!(0) }.to_string())
);
}
}