Skip to main content

mudra_cli/api/
client.rs

1//! HTTP client for currency API operations
2
3use crate::{CurrencyError, Result, config::Config};
4use reqwest::{Client, Response, StatusCode};
5use serde::de::DeserializeOwned;
6use std::time::Duration;
7use tokio::time::sleep;
8
9/// HTTP client wrapper for currency API operations
10#[derive(Debug, Clone)]
11pub struct CurrencyClient {
12    /// The underlying HTTP client
13    client: Client,
14    /// Configuration settings
15    config: Config,
16}
17
18impl CurrencyClient {
19    /// Create a new currency client with default configuration
20    pub fn new() -> Result<Self> {
21        let config = Config::new();
22        Self::with_config(config)
23    }
24
25    /// Create a new currency client with custom configuration
26    pub fn with_config(config: Config) -> Result<Self> {
27        // Build the HTTP client with our configuration
28        let client = Client::builder()
29            .timeout(config.timeout)
30            .user_agent(&config.user_agent)
31            .build()
32            .map_err(|e| {
33                CurrencyError::configuration(format!("Failed to create HTTP client: {}", e))
34            })?;
35
36        Ok(CurrencyClient { client, config })
37    }
38
39    /// Create a client from environment variables
40    pub fn from_env() -> Result<Self> {
41        let config = Config::from_env()?;
42        Self::with_config(config)
43    }
44
45    /// Make a GET request to the specified endpoint
46    pub async fn get<T>(&self, endpoint: &str) -> Result<T>
47    where
48        T: DeserializeOwned,
49    {
50        let url = format!(
51            "{}/{}",
52            self.config.base_url,
53            endpoint.trim_start_matches('/')
54        );
55
56        // Retry logic for failed requests
57        let mut last_error = None;
58
59        for attempt in 1..=self.config.max_retries {
60            match self.make_request(&url).await {
61                Ok(response) => {
62                    return self.handle_response(response).await;
63                }
64                Err(e) => {
65                    last_error = Some(e);
66
67                    if attempt < self.config.max_retries {
68                        // Exponential backoff: wait longer between each retry
69                        let delay = Duration::from_millis(1000 * 2_u64.pow(attempt - 1));
70                        sleep(delay).await;
71                    }
72                }
73            }
74        }
75
76        // If we get here, all retries failed
77        Err(last_error.unwrap_or_else(|| CurrencyError::api("All retries exhausted")))
78    }
79
80    /// Make the actual HTTP request
81    async fn make_request(&self, url: &str) -> Result<Response> {
82        let response = self.client.get(url).send().await?; // The ? operator converts reqwest::Error to our CurrencyError
83
84        Ok(response)
85    }
86
87    /// Handle the HTTP response, checking status codes and parsing JSON
88    async fn handle_response<T>(&self, response: Response) -> Result<T>
89    where
90        T: DeserializeOwned,
91    {
92        let status = response.status();
93
94        // Check for HTTP error status codes
95        match status {
96            StatusCode::OK => {
97                // Success - parse the JSON response
98                let json_text = response.text().await?;
99
100                // Parse JSON and provide better error context
101                serde_json::from_str(&json_text).map_err(|e| CurrencyError::from(e))
102            }
103            StatusCode::UNAUTHORIZED => {
104                Err(CurrencyError::api("Invalid API key or unauthorized access"))
105            }
106            StatusCode::TOO_MANY_REQUESTS => Err(CurrencyError::api(
107                "Rate limit exceeded. Please try again later",
108            )),
109            StatusCode::NOT_FOUND => Err(CurrencyError::api("API endpoint not found")),
110            StatusCode::BAD_REQUEST => {
111                // Try to get error details from response body
112                let error_text = response
113                    .text()
114                    .await
115                    .unwrap_or_else(|_| "Bad request".to_string());
116                Err(CurrencyError::api(format!("Bad request: {}", error_text)))
117            }
118            _ => {
119                let error_text = response
120                    .text()
121                    .await
122                    .unwrap_or_else(|_| "Unknown error".to_string());
123                Err(CurrencyError::api(format!(
124                    "HTTP error {}: {}",
125                    status.as_u16(),
126                    error_text
127                )))
128            }
129        }
130    }
131
132    /// Get the base URL being used
133    pub fn base_url(&self) -> &str {
134        &self.config.base_url
135    }
136
137    /// Check if the client is configured with an API key
138    pub fn has_api_key(&self) -> bool {
139        self.config.api_key.is_some()
140    }
141}