exchangerateapi_rust/
lib.rs

1use reqwest::Url;
2use serde::Deserialize;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum Error {
7    #[error("http: {0}")]
8    Http(#[from] reqwest::Error),
9    #[error("api: {0}")]
10    Api(String),
11    #[error("url: {0}")]
12    Url(#[from] url::ParseError),
13}
14
15#[derive(Debug, Deserialize)]
16pub struct RatesResponse {
17    #[serde(default)]
18    pub base: Option<String>,
19    #[serde(default)]
20    pub date: Option<String>,
21    #[serde(default)]
22    pub rates: serde_json::Value,
23    #[serde(default)]
24    pub error: Option<serde_json::Value>,
25}
26
27pub struct Client {
28    api_key: String,
29    base_url: Url,
30    http: reqwest::Client,
31}
32
33impl Client {
34    pub fn new<S: Into<String>>(api_key: S) -> Result<Self, Error> {
35        let api_key = api_key.into();
36        let base_url = Url::parse("https://api.exchangerateapi.net/v1")?;
37        Ok(Self { api_key, base_url, http: reqwest::Client::new() })
38    }
39
40    pub fn with_base_url<S: Into<String>>(api_key: S, base_url: S) -> Result<Self, Error> {
41        let api_key = api_key.into();
42        let base_url = Url::parse(&base_url.into())?;
43        Ok(Self { api_key, base_url, http: reqwest::Client::new() })
44    }
45
46    pub async fn latest(&self, base: &str, symbols: Option<&[&str]>) -> Result<RatesResponse, Error> {
47        let mut url = self.base_url.join("latest")?;
48        {
49            let mut qp = url.query_pairs_mut();
50            qp.append_pair("base", base);
51            qp.append_pair("apikey", &self.api_key);
52            if let Some(s) = symbols { if !s.is_empty() { qp.append_pair("symbols", &s.join(",")); } }
53        }
54        self.get(url).await
55    }
56
57    pub async fn historical(&self, date: &str, base: &str, symbols: Option<&[&str]>) -> Result<RatesResponse, Error> {
58        let mut url = self.base_url.join("historical")?;
59        {
60            let mut qp = url.query_pairs_mut();
61            qp.append_pair("date", date);
62            qp.append_pair("base", base);
63            qp.append_pair("apikey", &self.api_key);
64            if let Some(s) = symbols { if !s.is_empty() { qp.append_pair("symbols", &s.join(",")); } }
65        }
66        self.get(url).await
67    }
68
69    async fn get(&self, url: Url) -> Result<RatesResponse, Error> {
70        let res = self.http.get(url).send().await?.error_for_status()?;
71        let data: RatesResponse = res.json().await?;
72        if let Some(err) = &data.error {
73            let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("API error");
74            return Err(Error::Api(msg.to_string()));
75        }
76        Ok(data)
77    }
78}