Skip to main content

sofie_values/money/
encoding.rs

1use sofie_core::show::Fields;
2use std::fmt;
3
4use serde::de::{self, MapAccess, Visitor};
5use serde::ser::SerializeStruct;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use sofie_core::{Cast, ColumnReader, ColumnSpec, Error, Result, Show, Shown, SqlType, SqlValue};
8
9use crate::{Currency, Money};
10
11impl Cast for Money {
12    const COLUMNS: &'static [ColumnSpec] = &[
13        ColumnSpec::new("", SqlType::Int),
14        ColumnSpec::new("_currency", SqlType::Text).unordered(),
15    ];
16
17    fn encode(&self, out: &mut Vec<SqlValue>) {
18        self.minor.encode(out);
19        self.currency.encode(out);
20    }
21
22    fn decode(row: &mut dyn ColumnReader) -> Result<Self> {
23        let minor = i64::decode(row)?;
24        let currency = Currency::decode(row)?;
25        Ok(Self::new(minor, currency))
26    }
27}
28
29impl Fields for Money {
30    fn name(&self, index: usize) -> Option<&str> {
31        ["minor", "currency"].get(index).copied()
32    }
33
34    fn get(&self, name: &str) -> Option<Shown<'_>> {
35        match name {
36            "minor" => Some(Shown::Integer(self.minor)),
37            "currency" => Some(self.currency.show()),
38            _ => None,
39        }
40    }
41}
42
43impl Show for Money {
44    fn show(&self) -> Shown<'_> {
45        Shown::Record(self)
46    }
47}
48
49const FIELDS: &[&str] = &["minor", "amount", "currency"];
50
51impl Serialize for Money {
52    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
53        let mut fields = serializer.serialize_struct("Money", 2)?;
54        fields.serialize_field("minor", &self.minor)?;
55        fields.serialize_field("currency", &self.currency)?;
56        fields.end()
57    }
58}
59
60/// Minor units from code, or major units a person typed into a form.
61#[derive(Default)]
62struct MoneyInput {
63    minor: Option<i64>,
64    amount: Option<String>,
65    currency: Option<Currency>,
66}
67
68impl MoneyInput {
69    fn into_money(self) -> Result<Money> {
70        let currency = self
71            .currency
72            .ok_or_else(|| Error::invalid("currency", "is required"))?;
73        match (self.minor, self.amount) {
74            (Some(minor), None) => Ok(Money::new(minor, currency)),
75            (None, Some(amount)) => Money::parse_major(&amount, currency),
76            (None, None) => Err(Error::invalid("amount", "is required")),
77            (Some(_), Some(_)) => Err(Error::invalid(
78                "amount",
79                "needs either `minor` or `amount`, not both",
80            )),
81        }
82    }
83}
84
85/// Written by hand so the core values need no `serde_derive`.
86impl<'de> Visitor<'de> for MoneyInput {
87    type Value = Money;
88
89    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str("an object of `currency` and `minor` or `amount`")
91    }
92
93    fn visit_map<A: MapAccess<'de>>(mut self, mut map: A) -> Result<Money, A::Error> {
94        while let Some(field) = map.next_key::<String>()? {
95            match field.as_str() {
96                "minor" => self.minor = Some(map.next_value()?),
97                "amount" => self.amount = Some(map.next_value()?),
98                "currency" => self.currency = Some(map.next_value()?),
99                _ => return Err(de::Error::unknown_field(&field, FIELDS)),
100            }
101        }
102        self.into_money().map_err(Error::into_deserialize_error)
103    }
104}
105
106impl<'de> Deserialize<'de> for Money {
107    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108        deserializer.deserialize_struct("Money", FIELDS, MoneyInput::default())
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::testing::{euros, round_trip, suffixes};
116
117    #[test]
118    fn spans_an_amount_and_a_currency_column() {
119        assert_eq!(suffixes::<Money>(), ["", "_currency"]);
120    }
121
122    #[test]
123    fn round_trips_through_its_columns() {
124        let money = euros(14990);
125        assert_eq!(
126            round_trip(&money),
127            vec![SqlValue::Int(14990), SqlValue::Text("EUR".into())]
128        );
129    }
130
131    #[test]
132    fn round_trips_through_json() {
133        let json = serde_json::to_value(euros(14990)).unwrap();
134
135        assert_eq!(json, serde_json::json!({"minor": 14990, "currency": "EUR"}));
136        assert_eq!(serde_json::from_value::<Money>(json).unwrap(), euros(14990));
137    }
138
139    #[test]
140    fn json_deserialization_validates_the_currency() {
141        let json = serde_json::json!({"minor": 1, "currency": "euro"});
142
143        assert!(serde_json::from_value::<Money>(json).is_err());
144    }
145
146    #[test]
147    fn json_deserialization_refuses_a_float_amount() {
148        let json = serde_json::json!({"minor": 149.9, "currency": "EUR"});
149
150        assert!(serde_json::from_value::<Money>(json).is_err());
151    }
152
153    #[test]
154    fn json_deserialization_reads_a_typed_amount() {
155        let json = serde_json::json!({"amount": "149.90", "currency": "EUR"});
156
157        assert_eq!(serde_json::from_value::<Money>(json).unwrap(), euros(14990));
158    }
159
160    #[test]
161    fn json_deserialization_validates_a_typed_amount() {
162        for json in [
163            serde_json::json!({"amount": "149.999", "currency": "EUR"}),
164            serde_json::json!({"amount": "1e3", "currency": "EUR"}),
165            serde_json::json!({"amount": 149.9, "currency": "EUR"}),
166            serde_json::json!({"amount": "1", "currency": "euro"}),
167            serde_json::json!({"amount": "1"}),
168        ] {
169            assert!(
170                serde_json::from_value::<Money>(json.clone()).is_err(),
171                "{json}"
172            );
173        }
174        let error = serde_json::from_value::<Money>(
175            serde_json::json!({"amount": "149.999", "currency": "EUR"}),
176        )
177        .unwrap_err();
178        assert_eq!(error.to_string(), "EUR amounts have at most 2 decimals");
179        let missing = serde_json::from_value::<Money>(serde_json::json!({"currency": "EUR"}));
180        assert_eq!(missing.unwrap_err().to_string(), "is required");
181    }
182
183    #[test]
184    fn json_deserialization_needs_exactly_one_amount_shape() {
185        for json in [
186            serde_json::json!({"minor": 1, "amount": "0.01", "currency": "EUR"}),
187            serde_json::json!({"currency": "EUR"}),
188            serde_json::json!({"minor": 1, "currency": "EUR", "note": "x"}),
189        ] {
190            assert!(
191                serde_json::from_value::<Money>(json.clone()).is_err(),
192                "{json}"
193            );
194        }
195    }
196}