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