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, Format, IsoAlphabeticCode, IsoNumericCode, Money,
MoneyBag, Pair, ParseCurrencyError, RoundingMode,
format::{Grouping, Identifier, NegativeStyle, Position, Precision},
};
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];
const IDENTIFIER_FIELD: &str = "identifier";
const POSITION_FIELD: &str = "position";
const SPACED_FIELD: &str = "spaced";
const NEGATIVE_FIELD: &str = "negative";
const PRECISION_FIELD: &str = "precision";
const GROUPING_FIELD: &str = "grouping";
const GROUP_SEPARATOR_FIELD: &str = "group_separator";
const DECIMAL_SEPARATOR_FIELD: &str = "decimal_separator";
const FORMAT_FIELDS: &[&str] = &[
IDENTIFIER_FIELD,
POSITION_FIELD,
SPACED_FIELD,
NEGATIVE_FIELD,
PRECISION_FIELD,
GROUPING_FIELD,
GROUP_SEPARATOR_FIELD,
DECIMAL_SEPARATOR_FIELD,
];
const DIGITS_FIELD: &str = "digits";
const ROUNDING_FIELD: &str = "rounding";
const PRECISION_FIELDS: &[&str] = &[DIGITS_FIELD, ROUNDING_FIELD];
const FIRST_FIELD: &str = "first";
const REPEAT_FIELD: &str = "repeat";
const GROUPING_FIELDS: &[&str] = &[FIRST_FIELD, REPEAT_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)),
}
}
}
impl Serialize for Format {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let unset = usize::from(self.position.is_none())
+ usize::from(self.spaced.is_none())
+ usize::from(self.precision.is_none());
let mut format = serializer.serialize_struct("Format", FORMAT_FIELDS.len() - unset)?;
format.serialize_field(IDENTIFIER_FIELD, &self.identifier)?;
match self.position {
Some(position) => format.serialize_field(POSITION_FIELD, &position)?,
None => format.skip_field(POSITION_FIELD)?,
}
match self.spaced {
Some(spaced) => format.serialize_field(SPACED_FIELD, &spaced)?,
None => format.skip_field(SPACED_FIELD)?,
}
format.serialize_field(NEGATIVE_FIELD, &self.negative)?;
match self.precision {
Some(precision) => format.serialize_field(PRECISION_FIELD, &precision)?,
None => format.skip_field(PRECISION_FIELD)?,
}
format.serialize_field(GROUPING_FIELD, &self.grouping)?;
format.serialize_field(GROUP_SEPARATOR_FIELD, &self.group_separator)?;
format.serialize_field(DECIMAL_SEPARATOR_FIELD, &self.decimal_separator)?;
format.end()
}
}
impl<'de> Deserialize<'de> for Format {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct("Format", FORMAT_FIELDS, FormatVisitor)
}
}
struct FormatVisitor;
impl<'de> Visitor<'de> for FormatVisitor {
type Value = Format;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a set of formatting options")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut identifier: Option<Identifier> = None;
let mut position: Option<Option<Position>> = None;
let mut spaced: Option<Option<bool>> = None;
let mut negative: Option<NegativeStyle> = None;
let mut precision: Option<Option<Precision>> = None;
let mut grouping: Option<Grouping> = None;
let mut group_separator: Option<char> = None;
let mut decimal_separator: Option<char> = None;
while let Some(field) = map.next_key()? {
match field {
FormatField::Identifier if identifier.is_some() => {
return Err(de::Error::duplicate_field(IDENTIFIER_FIELD));
}
FormatField::Position if position.is_some() => {
return Err(de::Error::duplicate_field(POSITION_FIELD));
}
FormatField::Spaced if spaced.is_some() => {
return Err(de::Error::duplicate_field(SPACED_FIELD));
}
FormatField::Negative if negative.is_some() => {
return Err(de::Error::duplicate_field(NEGATIVE_FIELD));
}
FormatField::Precision if precision.is_some() => {
return Err(de::Error::duplicate_field(PRECISION_FIELD));
}
FormatField::Grouping if grouping.is_some() => {
return Err(de::Error::duplicate_field(GROUPING_FIELD));
}
FormatField::GroupSeparator if group_separator.is_some() => {
return Err(de::Error::duplicate_field(GROUP_SEPARATOR_FIELD));
}
FormatField::DecimalSeparator if decimal_separator.is_some() => {
return Err(de::Error::duplicate_field(DECIMAL_SEPARATOR_FIELD));
}
FormatField::Identifier => identifier = Some(map.next_value()?),
FormatField::Position => position = Some(map.next_value()?),
FormatField::Spaced => spaced = Some(map.next_value()?),
FormatField::Negative => negative = Some(map.next_value()?),
FormatField::Precision => precision = Some(map.next_value()?),
FormatField::Grouping => grouping = Some(map.next_value()?),
FormatField::GroupSeparator => group_separator = Some(map.next_value()?),
FormatField::DecimalSeparator => decimal_separator = Some(map.next_value()?),
FormatField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
let starting = Format::new();
Ok(Format {
identifier: identifier.unwrap_or(starting.identifier),
position: position.unwrap_or(starting.position),
spaced: spaced.unwrap_or(starting.spaced),
negative: negative.unwrap_or(starting.negative),
precision: precision.unwrap_or(starting.precision),
grouping: grouping.unwrap_or(starting.grouping),
group_separator: group_separator.unwrap_or(starting.group_separator),
decimal_separator: decimal_separator.unwrap_or(starting.decimal_separator),
})
}
}
enum FormatField {
Identifier,
Position,
Spaced,
Negative,
Precision,
Grouping,
GroupSeparator,
DecimalSeparator,
Other,
}
impl<'de> Deserialize<'de> for FormatField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_identifier(FormatFieldVisitor)
}
}
struct FormatFieldVisitor;
impl Visitor<'_> for FormatFieldVisitor {
type Value = FormatField;
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 {
IDENTIFIER_FIELD => FormatField::Identifier,
POSITION_FIELD => FormatField::Position,
SPACED_FIELD => FormatField::Spaced,
NEGATIVE_FIELD => FormatField::Negative,
PRECISION_FIELD => FormatField::Precision,
GROUPING_FIELD => FormatField::Grouping,
GROUP_SEPARATOR_FIELD => FormatField::GroupSeparator,
DECIMAL_SEPARATOR_FIELD => FormatField::DecimalSeparator,
_ => FormatField::Other,
})
}
}
impl Serialize for Identifier {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(match self {
Identifier::Code => "code",
Identifier::Symbol => "symbol",
Identifier::None => "none",
})
}
}
impl<'de> Deserialize<'de> for Identifier {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(IdentifierVisitor)
}
}
struct IdentifierVisitor;
impl Visitor<'_> for IdentifierVisitor {
type Value = Identifier;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#""code", "symbol", or "none""#)
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"code" => Ok(Identifier::Code),
"symbol" => Ok(Identifier::Symbol),
"none" => Ok(Identifier::None),
_ => Err(E::invalid_value(Unexpected::Str(name), &self)),
}
}
}
impl Serialize for Position {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(match self {
Position::Prefix => "prefix",
Position::Suffix => "suffix",
})
}
}
impl<'de> Deserialize<'de> for Position {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(PositionVisitor)
}
}
struct PositionVisitor;
impl Visitor<'_> for PositionVisitor {
type Value = Position;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#""prefix" or "suffix""#)
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"prefix" => Ok(Position::Prefix),
"suffix" => Ok(Position::Suffix),
_ => Err(E::invalid_value(Unexpected::Str(name), &self)),
}
}
}
impl Serialize for NegativeStyle {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(match self {
NegativeStyle::Minus => "minus",
NegativeStyle::Parentheses => "parentheses",
})
}
}
impl<'de> Deserialize<'de> for NegativeStyle {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(NegativeStyleVisitor)
}
}
struct NegativeStyleVisitor;
impl Visitor<'_> for NegativeStyleVisitor {
type Value = NegativeStyle;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(r#""minus" or "parentheses""#)
}
fn visit_str<E: de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"minus" => Ok(NegativeStyle::Minus),
"parentheses" => Ok(NegativeStyle::Parentheses),
_ => Err(E::invalid_value(Unexpected::Str(name), &self)),
}
}
}
impl Serialize for Grouping {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut grouping = serializer.serialize_struct("Grouping", GROUPING_FIELDS.len())?;
grouping.serialize_field(FIRST_FIELD, &self.first)?;
grouping.serialize_field(REPEAT_FIELD, &self.repeat)?;
grouping.end()
}
}
impl<'de> Deserialize<'de> for Grouping {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct("Grouping", GROUPING_FIELDS, GroupingVisitor)
}
}
struct GroupingVisitor;
impl<'de> Visitor<'de> for GroupingVisitor {
type Value = Grouping;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a first group size and the size that repeats after it")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut first: Option<u8> = None;
let mut repeat: Option<u8> = None;
while let Some(field) = map.next_key()? {
match field {
GroupingField::First if first.is_some() => {
return Err(de::Error::duplicate_field(FIRST_FIELD));
}
GroupingField::Repeat if repeat.is_some() => {
return Err(de::Error::duplicate_field(REPEAT_FIELD));
}
GroupingField::First => first = Some(map.next_value()?),
GroupingField::Repeat => repeat = Some(map.next_value()?),
GroupingField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
Ok(Grouping::new(
first.ok_or_else(|| de::Error::missing_field(FIRST_FIELD))?,
repeat.ok_or_else(|| de::Error::missing_field(REPEAT_FIELD))?,
))
}
}
enum GroupingField {
First,
Repeat,
Other,
}
impl<'de> Deserialize<'de> for GroupingField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_identifier(GroupingFieldVisitor)
}
}
struct GroupingFieldVisitor;
impl Visitor<'_> for GroupingFieldVisitor {
type Value = GroupingField;
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 {
FIRST_FIELD => GroupingField::First,
REPEAT_FIELD => GroupingField::Repeat,
_ => GroupingField::Other,
})
}
}
impl Serialize for Precision {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut precision = serializer.serialize_struct("Precision", PRECISION_FIELDS.len())?;
precision.serialize_field(DIGITS_FIELD, &self.digits)?;
precision.serialize_field(ROUNDING_FIELD, &self.mode)?;
precision.end()
}
}
impl<'de> Deserialize<'de> for Precision {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_struct("Precision", PRECISION_FIELDS, PrecisionVisitor)
}
}
struct PrecisionVisitor;
impl<'de> Visitor<'de> for PrecisionVisitor {
type Value = Precision;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a number of fractional digits and a rounding mode")
}
fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
let mut digits: Option<u32> = None;
let mut mode: Option<RoundingMode> = None;
while let Some(field) = map.next_key()? {
match field {
PrecisionField::Digits if digits.is_some() => {
return Err(de::Error::duplicate_field(DIGITS_FIELD));
}
PrecisionField::Rounding if mode.is_some() => {
return Err(de::Error::duplicate_field(ROUNDING_FIELD));
}
PrecisionField::Digits => digits = Some(map.next_value()?),
PrecisionField::Rounding => mode = Some(map.next_value()?),
PrecisionField::Other => {
map.next_value::<de::IgnoredAny>()?;
}
}
}
Ok(Precision {
digits: digits.ok_or_else(|| de::Error::missing_field(DIGITS_FIELD))?,
mode: mode.ok_or_else(|| de::Error::missing_field(ROUNDING_FIELD))?,
})
}
}
enum PrecisionField {
Digits,
Rounding,
Other,
}
impl<'de> Deserialize<'de> for PrecisionField {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_identifier(PrecisionFieldVisitor)
}
}
struct PrecisionFieldVisitor;
impl Visitor<'_> for PrecisionFieldVisitor {
type Value = PrecisionField;
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 {
DIGITS_FIELD => PrecisionField::Digits,
ROUNDING_FIELD => PrecisionField::Rounding,
_ => PrecisionField::Other,
})
}
}
#[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 starting_format_round_trips_test() {
let format = Format::new();
let document = serde_json::to_string(&format).unwrap();
assert_eq!(
document,
r#"{"identifier":"code","negative":"minus","grouping":{"first":3,"repeat":3},"group_separator":",","decimal_separator":"."}"#
);
assert_eq!(serde_json::from_str::<Format>(&document).unwrap(), format);
}
#[test]
fn format_with_every_option_set_round_trips_test() {
let format = Format::new()
.symbol()
.suffix()
.spaced()
.parentheses()
.precision(2, RoundingMode::HalfUp)
.grouping(3, 2)
.separators(' ', ',');
let document = serde_json::to_string(&format).unwrap();
assert_eq!(
document,
r#"{"identifier":"symbol","position":"suffix","spaced":true,"negative":"parentheses","precision":{"digits":2,"rounding":"half-up"},"grouping":{"first":3,"repeat":2},"group_separator":" ","decimal_separator":","}"#
);
assert_eq!(serde_json::from_str::<Format>(&document).unwrap(), format);
}
#[test]
fn format_fills_missing_fields_from_the_starting_options_test() {
let sparse: Format = serde_json::from_str(r#"{"group_separator": "_"}"#).unwrap();
assert_eq!(sparse, Format::new().separators('_', '.'));
assert_eq!(serde_json::from_str::<Format>("{}").unwrap(), Format::new());
}
#[test]
fn format_reads_a_null_option_as_unset_test() {
let explicit: Format =
serde_json::from_str(r#"{"position": null, "spaced": null, "precision": null}"#)
.unwrap();
assert_eq!(explicit, Format::new());
}
#[test]
fn format_reads_its_fields_in_any_order_test() {
let shuffled: Format =
serde_json::from_str(r#"{"negative": "parentheses", "identifier": "none"}"#).unwrap();
assert_eq!(shuffled, Format::new().amount_only().parentheses());
}
#[test]
fn format_skips_unknown_fields_test() {
let annotated: Format =
serde_json::from_str(r#"{"locale": "en-IN", "grouping": {"first": 3, "repeat": 2}}"#)
.unwrap();
assert_eq!(annotated, Format::new().grouping(3, 2));
}
#[test]
fn format_refuses_a_repeated_field_test() {
let repeated =
serde_json::from_str::<Format>(r#"{"spaced": true, "spaced": false}"#).unwrap_err();
assert!(repeated.to_string().contains("duplicate field `spaced`"));
}
#[test]
fn format_refuses_an_option_it_does_not_offer_test() {
let identifier = serde_json::from_str::<Format>(r#"{"identifier": "sign"}"#).unwrap_err();
let position = serde_json::from_str::<Format>(r#"{"position": "above"}"#).unwrap_err();
let negative = serde_json::from_str::<Format>(r#"{"negative": "red"}"#).unwrap_err();
assert!(
identifier
.to_string()
.contains(r#"expected "code", "symbol", or "none""#)
);
assert!(
position
.to_string()
.contains(r#"expected "prefix" or "suffix""#)
);
assert!(
negative
.to_string()
.contains(r#"expected "minus" or "parentheses""#)
);
}
#[test]
fn grouping_needs_both_sizes_test() {
let no_repeat =
serde_json::from_str::<Format>(r#"{"grouping": {"first": 3}}"#).unwrap_err();
let no_first =
serde_json::from_str::<Format>(r#"{"grouping": {"repeat": 3}}"#).unwrap_err();
assert!(no_repeat.to_string().contains("missing field `repeat`"));
assert!(no_first.to_string().contains("missing field `first`"));
}
#[test]
fn precision_needs_both_fields_test() {
let no_rounding =
serde_json::from_str::<Format>(r#"{"precision": {"digits": 2}}"#).unwrap_err();
let no_digits = serde_json::from_str::<Format>(r#"{"precision": {"rounding": "half-up"}}"#)
.unwrap_err();
assert!(no_rounding.to_string().contains("missing field `rounding`"));
assert!(no_digits.to_string().contains("missing field `digits`"));
}
#[test]
fn a_saved_format_still_renders_test() {
let saved = r#"{"identifier": "symbol", "grouping": {"first": 3, "repeat": 2}}"#;
let format: Format = serde_json::from_str(saved).unwrap();
let money = Money::from_major(1_234_567, Currency::USD);
assert_eq!(money.format_with(format).to_string(), "$12,34,567.00");
}
#[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())
);
}
}