1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use chrono::prelude::*;
use chrono_tz::Tz;

/// Represents a currency.
#[derive(Debug, Eq, PartialEq)]
pub struct Currency {
    /// The currency's name.
    pub name: String,
    /// The currency's code. Can be a physical currency using ISO 4217 or a cryptocurrency.
    pub code: String,
}

/// Represents the exchange rate for a currency pair.
#[derive(Debug, PartialEq)]
pub struct ExchangeRate {
    /// Currency to get the exchange rate for.
    pub from: Currency,
    /// Destination currency for the exchange rate.
    pub to: Currency,
    /// Value of the exchange rate.
    pub rate: f64,
    /// Date the exchange rate corresponds to.
    pub date: DateTime<Tz>,
}

pub(crate) mod parser {
    use super::*;
    use deserialize::{from_str, parse_date};
    use failure::{err_msg, Error};
    use serde_json;
    use std::io::Read;

    #[derive(Debug, Deserialize)]
    struct ExchangeRateHelper {
        #[serde(rename = "Error Message")]
        error: Option<String>,
        #[serde(rename = "Realtime Currency Exchange Rate")]
        data: Option<RealtimeExchangeRate>,
    }

    #[derive(Debug, Deserialize)]
    struct RealtimeExchangeRate {
        #[serde(rename = "1. From_Currency Code")]
        from_code: String,
        #[serde(rename = "2. From_Currency Name")]
        from_name: String,
        #[serde(rename = "3. To_Currency Code")]
        to_code: String,
        #[serde(rename = "4. To_Currency Name")]
        to_name: String,
        #[serde(rename = "5. Exchange Rate", deserialize_with = "from_str")]
        rate: f64,
        #[serde(rename = "6. Last Refreshed")]
        last_refreshed: String,
        #[serde(rename = "7. Time Zone")]
        time_zone: String,
    }

    pub(crate) fn parse(reader: impl Read) -> Result<ExchangeRate, Error> {
        let helper: ExchangeRateHelper = serde_json::from_reader(reader)?;

        if let Some(error) = helper.error {
            return Err(format_err!("received error: {}", error));
        }

        let data = helper
            .data
            .ok_or_else(|| err_msg("missing exchange rate data"))?;

        let time_zone: Tz = data
            .time_zone
            .parse()
            .map_err(|_| err_msg("error parsing time zone"))?;

        let date = parse_date(&data.last_refreshed, time_zone)?;

        let exchange_rate = ExchangeRate {
            from: Currency {
                name: data.from_name,
                code: data.from_code,
            },
            to: Currency {
                name: data.to_name,
                code: data.to_code,
            },
            rate: data.rate,
            date,
        };
        Ok(exchange_rate)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono_tz::UTC;
    use deserialize::parse_date;
    use std::io::BufReader;

    #[test]
    fn parse() {
        let data: &[u8] = include_bytes!("../tests/json/currency_exchange_rate.json");
        let exchange_rate =
            parser::parse(BufReader::new(data)).expect("failed to parse exchange rate");
        assert_eq!(
            exchange_rate,
            ExchangeRate {
                from: Currency {
                    name: "Euro".to_string(),
                    code: "EUR".to_string(),
                },
                to: Currency {
                    name: "United States Dollar".to_string(),
                    code: "USD".to_string(),
                },
                rate: 1.16665014,
                date: parse_date("2018-06-23 10:27:49", UTC).unwrap(),
            }
        );
    }
}