use std::collections::HashMap;
use reqwest::header::{ACCEPT, USER_AGENT};
use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use serde_json::Value;
use crate::{
client::Client,
error::UniRateError,
models::{HistoricalLimitsResponse, VatRateResponse, VatRatesResponse},
VERSION,
};
impl Client {
pub async fn get_rate(
&self,
from: impl AsRef<str>,
to: impl AsRef<str>,
) -> Result<f64, UniRateError> {
let query = vec![("from", upper(from)), ("to", upper(to))];
let body: RateResponse = self.request("/api/rates", &query).await?;
Ok(body.rate)
}
pub async fn get_all_rates(
&self,
from: impl AsRef<str>,
) -> Result<HashMap<String, f64>, UniRateError> {
let query = vec![("from", upper(from))];
let body: RatesResponse = self.request("/api/rates", &query).await?;
Ok(body.rates)
}
pub async fn convert(
&self,
amount: f64,
from: impl AsRef<str>,
to: impl AsRef<str>,
) -> Result<f64, UniRateError> {
let query = vec![
("from", upper(from)),
("to", upper(to)),
("amount", amount.to_string()),
];
let body: ConvertResponse = self.request("/api/convert", &query).await?;
Ok(body.result)
}
pub async fn get_supported_currencies(&self) -> Result<Vec<String>, UniRateError> {
let body: CurrenciesResponse = self.request("/api/currencies", &[]).await?;
Ok(body.currencies)
}
pub async fn get_historical_rate(
&self,
date: impl Into<String>,
from: impl AsRef<str>,
to: impl AsRef<str>,
) -> Result<f64, UniRateError> {
let query = vec![
("date", date.into()),
("amount", "1".to_string()),
("from", upper(from)),
("to", upper(to)),
];
let body: RateResponse = self.request("/api/historical/rates", &query).await?;
Ok(body.rate)
}
pub async fn get_historical_rates(
&self,
date: impl Into<String>,
from: impl AsRef<str>,
) -> Result<HashMap<String, f64>, UniRateError> {
let query = vec![
("date", date.into()),
("amount", "1".to_string()),
("from", upper(from)),
];
let body: RatesResponse = self.request("/api/historical/rates", &query).await?;
Ok(body.rates)
}
pub async fn convert_historical(
&self,
amount: f64,
from: impl AsRef<str>,
to: impl AsRef<str>,
date: impl Into<String>,
) -> Result<f64, UniRateError> {
let query = vec![
("date", date.into()),
("amount", amount.to_string()),
("from", upper(from)),
("to", upper(to)),
];
let body: ConvertResponse = self.request("/api/historical/rates", &query).await?;
Ok(body.result)
}
pub async fn get_time_series(
&self,
start_date: impl Into<String>,
end_date: impl Into<String>,
base: impl AsRef<str>,
currencies: Option<&[&str]>,
amount: f64,
) -> Result<HashMap<String, HashMap<String, f64>>, UniRateError> {
let mut query = vec![
("start_date", start_date.into()),
("end_date", end_date.into()),
("amount", amount.to_string()),
("base", upper(base)),
];
if let Some(list) = currencies {
if !list.is_empty() {
let joined = list
.iter()
.map(|c| c.to_ascii_uppercase())
.collect::<Vec<_>>()
.join(",");
query.push(("currencies", joined));
}
}
let body: TimeSeriesResponse = self.request("/api/historical/timeseries", &query).await?;
Ok(body.data)
}
pub async fn get_historical_limits(&self) -> Result<HistoricalLimitsResponse, UniRateError> {
self.request("/api/historical/limits", &[]).await
}
pub async fn get_vat_rates(&self) -> Result<VatRatesResponse, UniRateError> {
self.request("/api/vat/rates", &[]).await
}
pub async fn get_vat_rate(
&self,
country: impl AsRef<str>,
) -> Result<VatRateResponse, UniRateError> {
let query = vec![("country", upper(country))];
self.request("/api/vat/rates", &query).await
}
}
impl Client {
async fn request<T: DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
) -> Result<T, UniRateError> {
let url = format!("{}{}", self.base_url, path);
let mut params: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
params.push(("api_key", self.api_key.as_str()));
let request = self
.http
.get(&url)
.header(ACCEPT, "application/json")
.header(USER_AGENT, format!("unirate-rust/{VERSION}"))
.query(¶ms);
let response = request.send().await?;
let status = response.status();
let body = response.text().await?;
if status.is_success() {
if body.is_empty() {
return Err(UniRateError::Api {
status: status.as_u16(),
body: "empty response body".into(),
});
}
return serde_json::from_str::<T>(&body).map_err(UniRateError::from);
}
Err(match status.as_u16() {
400 => UniRateError::InvalidDate,
401 => UniRateError::Authentication,
404 => UniRateError::InvalidCurrency,
429 => UniRateError::RateLimit,
other => UniRateError::Api {
status: other,
body,
},
})
}
}
#[derive(Debug, Deserialize)]
struct RateResponse {
#[serde(deserialize_with = "deserialize_number")]
rate: f64,
}
#[derive(Debug, Deserialize)]
struct RatesResponse {
#[serde(deserialize_with = "deserialize_number_map")]
rates: HashMap<String, f64>,
}
#[derive(Debug, Deserialize)]
struct ConvertResponse {
#[serde(deserialize_with = "deserialize_number")]
result: f64,
}
#[derive(Debug, Deserialize)]
struct CurrenciesResponse {
currencies: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct TimeSeriesResponse {
#[serde(deserialize_with = "deserialize_nested_number_map")]
data: HashMap<String, HashMap<String, f64>>,
}
pub(crate) fn deserialize_number<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
value_to_f64(&value).map_err(serde::de::Error::custom)
}
fn deserialize_number_map<'de, D>(deserializer: D) -> Result<HashMap<String, f64>, D::Error>
where
D: Deserializer<'de>,
{
let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
let mut out = HashMap::with_capacity(raw.len());
for (k, v) in raw {
out.insert(k, value_to_f64(&v).map_err(serde::de::Error::custom)?);
}
Ok(out)
}
fn deserialize_nested_number_map<'de, D>(
deserializer: D,
) -> Result<HashMap<String, HashMap<String, f64>>, D::Error>
where
D: Deserializer<'de>,
{
let raw: HashMap<String, HashMap<String, Value>> = HashMap::deserialize(deserializer)?;
let mut out = HashMap::with_capacity(raw.len());
for (date, inner) in raw {
let mut converted = HashMap::with_capacity(inner.len());
for (code, value) in inner {
converted.insert(
code,
value_to_f64(&value).map_err(serde::de::Error::custom)?,
);
}
out.insert(date, converted);
}
Ok(out)
}
fn value_to_f64(value: &Value) -> Result<f64, String> {
match value {
Value::Number(n) => n
.as_f64()
.ok_or_else(|| format!("cannot represent {n} as f64")),
Value::String(s) => s
.parse::<f64>()
.map_err(|e| format!("cannot parse {s:?} as f64: {e}")),
other => Err(format!("expected number or numeric string, got {other:?}")),
}
}
fn upper(s: impl AsRef<str>) -> String {
s.as_ref().to_ascii_uppercase()
}