use std::fmt;
use std::str::FromStr;
use super::{DataError, DataItem};
use async_trait::async_trait;
use chrono::{DateTime, Local};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum CurrencyError {
#[error("currency codes must consist of exactly three characters")]
InvalidLength,
#[error("urrency codes must contain only alphabetic ASCII characters")]
InvalidCharacter,
#[error("currency deserialization failed")]
DeserializationFailed,
#[error("currency conversion failed")]
ConversionFailed,
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct CurrencyISOCode {
iso_code: [char; 3],
}
impl CurrencyISOCode {
pub fn new(code: &str) -> Result<CurrencyISOCode, CurrencyError> {
let mut iso_code = [' ', ' ', ' '];
let mut idx = 0;
for c in code.chars() {
if idx >= 3 {
return Err(CurrencyError::InvalidLength);
}
let c = c.to_ascii_uppercase();
if c.is_ascii_alphabetic() {
iso_code[idx] = c.to_ascii_uppercase();
idx += 1;
} else {
return Err(CurrencyError::InvalidCharacter);
}
}
if idx != 3 {
Err(CurrencyError::InvalidLength)
} else {
Ok(Self { iso_code })
}
}
}
impl fmt::Display for CurrencyISOCode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{}",
self.iso_code[0], self.iso_code[1], self.iso_code[2]
)
}
}
impl FromStr for CurrencyISOCode {
type Err = CurrencyError;
fn from_str(c: &str) -> Result<CurrencyISOCode, CurrencyError> {
Self::new(c)
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
pub struct Currency {
pub id: Option<i32>,
pub iso_code: CurrencyISOCode,
pub rounding_digits: i32,
}
impl Currency {
pub fn new(id: Option<i32>, iso_code: CurrencyISOCode, rounding_digits: Option<i32>) -> Self {
Self {
id,
iso_code,
rounding_digits: rounding_digits
.unwrap_or_else(|| default_rounding_digits(&iso_code.to_string())),
}
}
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.iso_code,)
}
}
impl DataItem for Currency {
fn get_id(&self) -> Result<i32, DataError> {
match self.id {
Some(id) => Ok(id),
None => Err(DataError::DataAccessFailure(
"Can't get id of temporary currency".to_string(),
)),
}
}
fn set_id(&mut self, id: i32) -> Result<(), DataError> {
match self.id {
Some(_) => Err(DataError::DataAccessFailure(
"Can't change id of persistent currency".to_string(),
)),
None => {
self.id = Some(id);
Ok(())
}
}
}
}
fn default_rounding_digits(curr: &str) -> i32 {
match curr {
"JPY" | "TRL" => 0,
_ => 2,
}
}
impl FromStr for Currency {
type Err = CurrencyError;
fn from_str(c: &str) -> Result<Currency, CurrencyError> {
Ok(Currency::new(None, CurrencyISOCode::from_str(c)?, None))
}
}
impl Serialize for Currency {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}", &self))
}
}
struct CurrencyVisitor;
impl<'de> Visitor<'de> for CurrencyVisitor {
type Value = Currency;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a currency code must consist of three alphabetic characters")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match Currency::from_str(value) {
Ok(val) => Ok(val),
Err(err) => Err(E::custom(format!("{}", err))),
}
}
}
impl<'de> Deserialize<'de> for Currency {
fn deserialize<D>(deserializer: D) -> Result<Currency, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(CurrencyVisitor)
}
}
impl Currency {
pub fn rounding_digits(&self) -> i32 {
self.rounding_digits
}
}
#[async_trait]
pub trait CurrencyConverter {
async fn fx_rate(
&self,
base_currency: Currency,
quote_currency: Currency,
time: DateTime<Local>,
) -> Result<f64, CurrencyError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_write_currency() {
let currency = Currency::from_str("EUR").unwrap();
assert_eq!(format!("{}", currency), "EUR".to_string());
let currency = Currency::from_str("euR").unwrap();
assert_eq!(format!("{}", currency), "EUR".to_string());
let currency = Currency::from_str("EURO");
assert_eq!(currency, Err(CurrencyError::InvalidLength));
let currency = Currency::from_str("EU");
assert_eq!(currency, Err(CurrencyError::InvalidLength));
let currency = Currency::from_str("éUR");
assert_eq!(currency, Err(CurrencyError::InvalidCharacter));
let currency = Currency::from_str("EU1");
assert_eq!(currency, Err(CurrencyError::InvalidCharacter));
}
#[test]
fn deserialize_currency() {
let input = r#""EUR""#;
let curr: Currency = serde_json::from_str(input).unwrap();
assert_eq!(format!("{}", curr), "EUR");
}
#[test]
fn serialize_currency() {
let curr = Currency {
id: None,
iso_code: CurrencyISOCode::from_str("EUR").unwrap(),
rounding_digits: 2,
};
let json = serde_json::to_string(&curr).unwrap();
assert_eq!(json, r#""EUR""#);
}
}