use crate::{CurrencyError, Result, config::Config};
use reqwest::{Client, Response, StatusCode};
use serde::de::DeserializeOwned;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug, Clone)]
pub struct CurrencyClient {
client: Client,
config: Config,
}
impl CurrencyClient {
pub fn new() -> Result<Self> {
let config = Config::new();
Self::with_config(config)
}
pub fn with_config(config: Config) -> Result<Self> {
let client = Client::builder()
.timeout(config.timeout)
.user_agent(&config.user_agent)
.build()
.map_err(|e| {
CurrencyError::configuration(format!("Failed to create HTTP client: {}", e))
})?;
Ok(CurrencyClient { client, config })
}
pub fn from_env() -> Result<Self> {
let config = Config::from_env()?;
Self::with_config(config)
}
pub async fn get<T>(&self, endpoint: &str) -> Result<T>
where
T: DeserializeOwned,
{
let url = format!(
"{}/{}",
self.config.base_url,
endpoint.trim_start_matches('/')
);
let mut last_error = None;
for attempt in 1..=self.config.max_retries {
match self.make_request(&url).await {
Ok(response) => {
return self.handle_response(response).await;
}
Err(e) => {
last_error = Some(e);
if attempt < self.config.max_retries {
let delay = Duration::from_millis(1000 * 2_u64.pow(attempt - 1));
sleep(delay).await;
}
}
}
}
Err(last_error.unwrap_or_else(|| CurrencyError::api("All retries exhausted")))
}
async fn make_request(&self, url: &str) -> Result<Response> {
let response = self.client.get(url).send().await?;
Ok(response)
}
async fn handle_response<T>(&self, response: Response) -> Result<T>
where
T: DeserializeOwned,
{
let status = response.status();
match status {
StatusCode::OK => {
let json_text = response.text().await?;
serde_json::from_str(&json_text).map_err(|e| CurrencyError::from(e))
}
StatusCode::UNAUTHORIZED => {
Err(CurrencyError::api("Invalid API key or unauthorized access"))
}
StatusCode::TOO_MANY_REQUESTS => Err(CurrencyError::api(
"Rate limit exceeded. Please try again later",
)),
StatusCode::NOT_FOUND => Err(CurrencyError::api("API endpoint not found")),
StatusCode::BAD_REQUEST => {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Bad request".to_string());
Err(CurrencyError::api(format!("Bad request: {}", error_text)))
}
_ => {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
Err(CurrencyError::api(format!(
"HTTP error {}: {}",
status.as_u16(),
error_text
)))
}
}
}
pub fn base_url(&self) -> &str {
&self.config.base_url
}
pub fn has_api_key(&self) -> bool {
self.config.api_key.is_some()
}
}