finql_data/
currency.rs

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/// Error type related to the Currency
12#[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
36/// This is important for other errors to wrap this one.
37impl 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/// Special type for currencies
50#[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
73/// Transform a string into a Currency
74impl 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/// Trait for calculating FX rates for currency conversion
146#[async_trait]
147pub trait CurrencyConverter {
148    /// returns the price of 1 unit of foreign currency in terms of domestic currency
149    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        // valid iso code
159        let currency = Currency::from_str("EUR").unwrap();
160        assert_eq!(format!("{}", currency), "EUR".to_string());
161
162        // case ignorance
163        let currency = Currency::from_str("euR").unwrap();
164        assert_eq!(format!("{}", currency), "EUR".to_string());
165
166        // to long
167        let currency = Currency::from_str("EURO");
168        assert_eq!(currency, Err(CurrencyError::InvalidLength));
169
170        // to short
171        let currency = Currency::from_str("EU");
172        assert_eq!(currency, Err(CurrencyError::InvalidLength));
173
174        // invalid character1
175        let currency = Currency::from_str("éUR");
176        assert_eq!(currency, Err(CurrencyError::InvalidCharacter));
177
178        // invalid character2
179        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}