1use std::error;
2use std::fmt;
3use std::str::FromStr;
4
5use async_trait::async_trait;
6use serde::de::{self, Visitor};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use chrono::{DateTime, Utc};
9
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum CurrencyError {
14 InvalidLength,
15 InvalidCharacter,
16 DeserializationFailed,
17 ConversionFailed,
18}
19
20impl fmt::Display for CurrencyError {
21 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22 match self {
23 CurrencyError::InvalidLength => {
24 write!(f, "currency codes must consist of exactly three characters")
25 }
26 CurrencyError::InvalidCharacter => write!(
27 f,
28 "currency codes must contain only alphabetic ASCII characters"
29 ),
30 CurrencyError::DeserializationFailed => write!(f, "currency deserialization failed"),
31 CurrencyError::ConversionFailed => write!(f, "currency conversion failed"),
32 }
33 }
34}
35
36impl error::Error for CurrencyError {
38 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
39 None
40 }
41}
42
43impl de::Error for CurrencyError {
44 fn custom<T: fmt::Display>(_: T) -> Self {
45 CurrencyError::DeserializationFailed
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Copy)]
51pub struct Currency {
52 iso_code: [char; 3],
53 rounding_digits: i32,
54}
55
56impl fmt::Display for Currency {
57 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58 write!(
59 f,
60 "{}{}{}",
61 self.iso_code[0], self.iso_code[1], self.iso_code[2]
62 )
63 }
64}
65
66fn default_rounding_digits(curr: &str) -> i32 {
67 match curr {
68 "JPY" | "TRL" => 0,
69 _ => 2
70 }
71}
72
73impl FromStr for Currency {
75 type Err = CurrencyError;
76
77 fn from_str(curr: &str) -> Result<Currency, CurrencyError> {
78 let rounding_digits = default_rounding_digits(curr);
79 let mut iso_code = [' ', ' ', ' '];
80 let mut idx = 0;
81 for c in curr.chars() {
82 if idx >= 3 {
83 return Err(CurrencyError::InvalidLength);
84 }
85 let c = c.to_ascii_uppercase();
86 if c.is_ascii_alphabetic() {
87 iso_code[idx] = c.to_ascii_uppercase();
88 idx += 1;
89 } else {
90 return Err(CurrencyError::InvalidCharacter);
91 }
92 }
93 if idx != 3 {
94 Err(CurrencyError::InvalidLength)
95 } else {
96 Ok(Currency{iso_code, rounding_digits} )
97 }
98 }
99}
100
101impl Serialize for Currency {
102 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103 where
104 S: Serializer,
105 {
106 serializer.serialize_str(&format!("{}", &self))
107 }
108}
109
110struct CurrencyVisitor;
111
112impl<'de> Visitor<'de> for CurrencyVisitor {
113 type Value = Currency;
114
115 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
116 formatter.write_str("a currency code must consist of three alphabetic characters")
117 }
118
119 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
120 where
121 E: de::Error,
122 {
123 match Currency::from_str(value) {
124 Ok(val) => Ok(val),
125 Err(err) => Err(E::custom(format!("{}", err))),
126 }
127 }
128}
129
130impl<'de> Deserialize<'de> for Currency {
131 fn deserialize<D>(deserializer: D) -> Result<Currency, D::Error>
132 where
133 D: Deserializer<'de>,
134 {
135 deserializer.deserialize_str(CurrencyVisitor)
136 }
137}
138
139impl Currency {
140 pub fn rounding_digits(&self) -> i32 {
141 self.rounding_digits
142 }
143}
144
145#[async_trait]
147pub trait CurrencyConverter {
148 async fn fx_rate(&self, foreign_currency: Currency, domestic_currency: Currency, time: DateTime<Utc>) -> Result<f64, CurrencyError>;
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn read_write_currency() {
158 let currency = Currency::from_str("EUR").unwrap();
160 assert_eq!(format!("{}", currency), "EUR".to_string());
161
162 let currency = Currency::from_str("euR").unwrap();
164 assert_eq!(format!("{}", currency), "EUR".to_string());
165
166 let currency = Currency::from_str("EURO");
168 assert_eq!(currency, Err(CurrencyError::InvalidLength));
169
170 let currency = Currency::from_str("EU");
172 assert_eq!(currency, Err(CurrencyError::InvalidLength));
173
174 let currency = Currency::from_str("éUR");
176 assert_eq!(currency, Err(CurrencyError::InvalidCharacter));
177
178 let currency = Currency::from_str("EU1");
180 assert_eq!(currency, Err(CurrencyError::InvalidCharacter));
181 }
182
183 #[test]
184 fn deserialize_currency() {
185 let input = r#""EUR""#;
186
187 let curr: Currency = serde_json::from_str(input).unwrap();
188 assert_eq!(format!("{}", curr), "EUR");
189 }
190 #[test]
191 fn serialize_currency() {
192 let curr = Currency {
193 iso_code: ['E', 'U', 'R'], rounding_digits: 2
194 };
195 let json = serde_json::to_string(&curr).unwrap();
196 assert_eq!(json, r#""EUR""#);
197 }
198}