#![forbid(unsafe_code)]
use super::error::DomainError;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
pub trait Currency: Copy + Send + Sync + 'static {
fn code() -> &'static str;
fn symbol() -> &'static str;
fn scale() -> u32;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Brl;
impl Currency for Brl {
fn code() -> &'static str {
"BRL"
}
fn symbol() -> &'static str {
"R$"
}
fn scale() -> u32 {
2
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Usd;
impl Currency for Usd {
fn code() -> &'static str {
"USD"
}
fn symbol() -> &'static str {
"$"
}
fn scale() -> u32 {
2
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(bound = "")]
pub struct Money<C: Currency> {
#[serde(with = "rust_decimal::serde::str")]
amount: Decimal,
#[serde(skip)]
_currency: PhantomData<C>,
}
impl<C: Currency> Money<C> {
pub fn try_new(amount: Decimal) -> Result<Self, DomainError> {
if amount.is_sign_negative() {
return Err(DomainError::new(
"money",
format!("{} amount must not be negative", C::code()),
));
}
let amount = amount.round_dp(C::scale());
Ok(Self {
amount,
_currency: PhantomData,
})
}
pub fn try_from_str(s: impl AsRef<str>) -> Result<Self, DomainError> {
let d = Decimal::from_str(s.as_ref().trim())
.map_err(|e| DomainError::new("money", e.to_string()))?;
Self::try_new(d)
}
#[must_use]
pub fn zero() -> Self {
Self {
amount: Decimal::ZERO,
_currency: PhantomData,
}
}
#[must_use]
pub const fn amount(&self) -> Decimal {
self.amount
}
#[must_use]
pub fn code(&self) -> &'static str {
C::code()
}
}
impl<C: Currency> fmt::Display for Money<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.amount, C::code())
}
}
impl<C: Currency> std::ops::Add for Money<C> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
amount: (self.amount + rhs.amount).round_dp(C::scale()),
_currency: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::dec;
#[test]
fn brl_add_and_serde_str() {
let a = Money::<Brl>::try_new(dec!(99.99)).unwrap();
let b = Money::<Brl>::try_new(dec!(0.01)).unwrap();
let t = a + b;
assert_eq!(t.amount(), dec!(100.00));
let j = serde_json::to_string(&t).unwrap();
assert!(j.contains('\"') && j.contains("100"), "{j}");
let back: Money<Brl> = serde_json::from_str(&j).unwrap();
assert_eq!(back.amount(), dec!(100.00));
}
#[test]
fn rejects_negative() {
assert!(Money::<Usd>::try_new(dec!(-1)).is_err());
}
#[test]
fn from_str() {
let m = Money::<Usd>::try_from_str("12.34").unwrap();
assert_eq!(m.amount(), dec!(12.34));
}
}