1use std::collections::HashMap;
4
5use reqwest::header::{ACCEPT, USER_AGENT};
6use serde::{de::DeserializeOwned, Deserialize, Deserializer};
7use serde_json::Value;
8
9use crate::{
10 client::Client,
11 error::UniRateError,
12 models::{HistoricalLimitsResponse, VatRateResponse, VatRatesResponse},
13 VERSION,
14};
15
16impl Client {
21 pub async fn get_rate(
25 &self,
26 from: impl AsRef<str>,
27 to: impl AsRef<str>,
28 ) -> Result<f64, UniRateError> {
29 let query = vec![("from", upper(from)), ("to", upper(to))];
30 let body: RateResponse = self.request("/api/rates", &query).await?;
31 Ok(body.rate)
32 }
33
34 pub async fn get_all_rates(
36 &self,
37 from: impl AsRef<str>,
38 ) -> Result<HashMap<String, f64>, UniRateError> {
39 let query = vec![("from", upper(from))];
40 let body: RatesResponse = self.request("/api/rates", &query).await?;
41 Ok(body.rates)
42 }
43
44 pub async fn convert(
46 &self,
47 amount: f64,
48 from: impl AsRef<str>,
49 to: impl AsRef<str>,
50 ) -> Result<f64, UniRateError> {
51 let query = vec![
52 ("from", upper(from)),
53 ("to", upper(to)),
54 ("amount", amount.to_string()),
55 ];
56 let body: ConvertResponse = self.request("/api/convert", &query).await?;
57 Ok(body.result)
58 }
59
60 pub async fn get_supported_currencies(&self) -> Result<Vec<String>, UniRateError> {
62 let body: CurrenciesResponse = self.request("/api/currencies", &[]).await?;
63 Ok(body.currencies)
64 }
65
66 pub async fn get_historical_rate(
70 &self,
71 date: impl Into<String>,
72 from: impl AsRef<str>,
73 to: impl AsRef<str>,
74 ) -> Result<f64, UniRateError> {
75 let query = vec![
76 ("date", date.into()),
77 ("amount", "1".to_string()),
78 ("from", upper(from)),
79 ("to", upper(to)),
80 ];
81 let body: RateResponse = self.request("/api/historical/rates", &query).await?;
82 Ok(body.rate)
83 }
84
85 pub async fn get_historical_rates(
89 &self,
90 date: impl Into<String>,
91 from: impl AsRef<str>,
92 ) -> Result<HashMap<String, f64>, UniRateError> {
93 let query = vec![
94 ("date", date.into()),
95 ("amount", "1".to_string()),
96 ("from", upper(from)),
97 ];
98 let body: RatesResponse = self.request("/api/historical/rates", &query).await?;
99 Ok(body.rates)
100 }
101
102 pub async fn convert_historical(
106 &self,
107 amount: f64,
108 from: impl AsRef<str>,
109 to: impl AsRef<str>,
110 date: impl Into<String>,
111 ) -> Result<f64, UniRateError> {
112 let query = vec![
113 ("date", date.into()),
114 ("amount", amount.to_string()),
115 ("from", upper(from)),
116 ("to", upper(to)),
117 ];
118 let body: ConvertResponse = self.request("/api/historical/rates", &query).await?;
119 Ok(body.result)
120 }
121
122 pub async fn get_time_series(
126 &self,
127 start_date: impl Into<String>,
128 end_date: impl Into<String>,
129 base: impl AsRef<str>,
130 currencies: Option<&[&str]>,
131 amount: f64,
132 ) -> Result<HashMap<String, HashMap<String, f64>>, UniRateError> {
133 let mut query = vec![
134 ("start_date", start_date.into()),
135 ("end_date", end_date.into()),
136 ("amount", amount.to_string()),
137 ("base", upper(base)),
138 ];
139 if let Some(list) = currencies {
140 if !list.is_empty() {
141 let joined = list
142 .iter()
143 .map(|c| c.to_ascii_uppercase())
144 .collect::<Vec<_>>()
145 .join(",");
146 query.push(("currencies", joined));
147 }
148 }
149 let body: TimeSeriesResponse = self.request("/api/historical/timeseries", &query).await?;
150 Ok(body.data)
151 }
152
153 pub async fn get_historical_limits(&self) -> Result<HistoricalLimitsResponse, UniRateError> {
157 self.request("/api/historical/limits", &[]).await
158 }
159
160 pub async fn get_vat_rates(&self) -> Result<VatRatesResponse, UniRateError> {
162 self.request("/api/vat/rates", &[]).await
163 }
164
165 pub async fn get_vat_rate(
167 &self,
168 country: impl AsRef<str>,
169 ) -> Result<VatRateResponse, UniRateError> {
170 let query = vec![("country", upper(country))];
171 self.request("/api/vat/rates", &query).await
172 }
173}
174
175impl Client {
180 async fn request<T: DeserializeOwned>(
181 &self,
182 path: &str,
183 query: &[(&str, String)],
184 ) -> Result<T, UniRateError> {
185 let url = format!("{}{}", self.base_url, path);
186 let mut params: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
189 params.push(("api_key", self.api_key.as_str()));
190
191 let request = self
192 .http
193 .get(&url)
194 .header(ACCEPT, "application/json")
198 .header(USER_AGENT, format!("unirate-rust/{VERSION}"))
199 .query(¶ms);
200
201 let response = request.send().await?;
202 let status = response.status();
203 let body = response.text().await?;
204
205 if status.is_success() {
206 if body.is_empty() {
207 return Err(UniRateError::Api {
208 status: status.as_u16(),
209 body: "empty response body".into(),
210 });
211 }
212 return serde_json::from_str::<T>(&body).map_err(UniRateError::from);
213 }
214
215 Err(match status.as_u16() {
216 400 => UniRateError::InvalidDate,
217 401 => UniRateError::Authentication,
218 404 => UniRateError::InvalidCurrency,
219 429 => UniRateError::RateLimit,
220 other => UniRateError::Api {
221 status: other,
222 body,
223 },
224 })
225 }
226}
227
228#[derive(Debug, Deserialize)]
233struct RateResponse {
234 #[serde(deserialize_with = "deserialize_number")]
235 rate: f64,
236}
237
238#[derive(Debug, Deserialize)]
239struct RatesResponse {
240 #[serde(deserialize_with = "deserialize_number_map")]
241 rates: HashMap<String, f64>,
242}
243
244#[derive(Debug, Deserialize)]
245struct ConvertResponse {
246 #[serde(deserialize_with = "deserialize_number")]
247 result: f64,
248}
249
250#[derive(Debug, Deserialize)]
251struct CurrenciesResponse {
252 currencies: Vec<String>,
253}
254
255#[derive(Debug, Deserialize)]
256struct TimeSeriesResponse {
257 #[serde(deserialize_with = "deserialize_nested_number_map")]
258 data: HashMap<String, HashMap<String, f64>>,
259}
260
261pub(crate) fn deserialize_number<'de, D>(deserializer: D) -> Result<f64, D::Error>
268where
269 D: Deserializer<'de>,
270{
271 let value = Value::deserialize(deserializer)?;
272 value_to_f64(&value).map_err(serde::de::Error::custom)
273}
274
275fn deserialize_number_map<'de, D>(deserializer: D) -> Result<HashMap<String, f64>, D::Error>
276where
277 D: Deserializer<'de>,
278{
279 let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
280 let mut out = HashMap::with_capacity(raw.len());
281 for (k, v) in raw {
282 out.insert(k, value_to_f64(&v).map_err(serde::de::Error::custom)?);
283 }
284 Ok(out)
285}
286
287fn deserialize_nested_number_map<'de, D>(
288 deserializer: D,
289) -> Result<HashMap<String, HashMap<String, f64>>, D::Error>
290where
291 D: Deserializer<'de>,
292{
293 let raw: HashMap<String, HashMap<String, Value>> = HashMap::deserialize(deserializer)?;
294 let mut out = HashMap::with_capacity(raw.len());
295 for (date, inner) in raw {
296 let mut converted = HashMap::with_capacity(inner.len());
297 for (code, value) in inner {
298 converted.insert(
299 code,
300 value_to_f64(&value).map_err(serde::de::Error::custom)?,
301 );
302 }
303 out.insert(date, converted);
304 }
305 Ok(out)
306}
307
308fn value_to_f64(value: &Value) -> Result<f64, String> {
309 match value {
310 Value::Number(n) => n
311 .as_f64()
312 .ok_or_else(|| format!("cannot represent {n} as f64")),
313 Value::String(s) => s
314 .parse::<f64>()
315 .map_err(|e| format!("cannot parse {s:?} as f64: {e}")),
316 other => Err(format!("expected number or numeric string, got {other:?}")),
317 }
318}
319
320fn upper(s: impl AsRef<str>) -> String {
325 s.as_ref().to_ascii_uppercase()
326}