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#[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
85impl<'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}