Skip to main content

etopay_api_types/api/
decimal.rs

1/// Represents a decimal amount of either FIAT or crypto currencies, always in the main unit for
2/// respective currency/network (eg. EURO, USD, ETH, IOTA).
3#[derive(Debug, Copy, Clone, PartialEq)]
4#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
5#[cfg_attr(feature = "utoipa", schema(value_type = String))]
6pub struct Decimal(pub rust_decimal::Decimal);
7
8impl From<rust_decimal::Decimal> for Decimal {
9    fn from(value: rust_decimal::Decimal) -> Self {
10        Self(value)
11    }
12}
13
14/// Implementations of serde Serialize and Deserialize to make sure it is represented correctly
15/// as a String.
16mod serde {
17    use super::Decimal;
18
19    impl serde::Serialize for Decimal {
20        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21        where
22            S: serde::Serializer,
23        {
24            serializer.serialize_str(&self.0.to_string())
25        }
26    }
27
28    struct DecimalVisitor;
29
30    impl serde::de::Visitor<'_> for DecimalVisitor {
31        type Value = Decimal;
32
33        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
34            formatter.write_str("String containing a decimal number")
35        }
36
37        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
38        where
39            E: serde::de::Error,
40        {
41            // This is the crucial part: using `from_str_exact` we make sure no rounding is
42            // implicitly performed. Instead, there will be an error if the number cannot be
43            // represented correctly.
44            //
45            // The built-in serialization uses `from_str` which applies truncation/rounding to fit
46            // which we do not want!
47            rust_decimal::Decimal::from_str_exact(v)
48                .map(Decimal)
49                .map_err(|e| E::custom(format!("Could not parse {v}: {e}")))
50        }
51    }
52
53    impl<'de> serde::Deserialize<'de> for Decimal {
54        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55        where
56            D: serde::Deserializer<'de>,
57        {
58            deserializer.deserialize_string(DecimalVisitor)
59        }
60    }
61}
62
63#[cfg(test)]
64mod test {
65    use super::Decimal;
66    use rust_decimal_macros::dec;
67
68    #[rstest::rstest]
69    #[case("\"0.000000000000000001\"", Ok(Decimal::from(dec!(0.000000000000000001))))]
70    #[case("\"1.000000000000000001\"", Ok(Decimal::from(dec!(1.000000000000000001))))]
71    #[case("\"10000000000.000000000000000001\"", Ok(Decimal::from(dec!(10000000000.000000000000000001))))]
72    #[case("\"100000000000.000000000000000001\"", Err(serde_json::error::Category::Data))]
73    #[case("\"10000000000000000000000000001\"", Ok(Decimal::from(dec!(10000000000000000000000000001))))]
74    fn test_deserialization(#[case] input: &str, #[case] expected: Result<Decimal, serde_json::error::Category>) {
75        let result = serde_json::from_str::<Decimal>(input);
76
77        match (result, expected) {
78            (Ok(d), Ok(d2)) => assert_eq!(d, d2),
79            (Err(e), Err(e2)) => assert_eq!(e.classify(), e2),
80            (other, other2) => panic!("Expected: {:?} but got {:?}", other2, other),
81        }
82    }
83}