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