redis_cloud/
client.rs

1//! Redis Cloud API client core implementation
2//!
3//! This module contains the core HTTP client for interacting with the Redis Cloud REST API.
4//! It provides authentication handling, request/response processing, and error management.
5//!
6//! The client is designed around a builder pattern for flexible configuration and supports
7//! both typed and untyped API interactions.
8
9use crate::{CloudError as RestError, Result};
10use reqwest::Client;
11use serde::Serialize;
12use std::sync::Arc;
13
14/// Builder for constructing a CloudClient with custom configuration
15///
16/// Provides a fluent interface for configuring API credentials, base URL, timeouts,
17/// and other client settings before creating the final CloudClient instance.
18///
19/// # Examples
20///
21/// ```rust,no_run
22/// use redis_cloud::CloudClient;
23///
24/// // Basic configuration
25/// let client = CloudClient::builder()
26///     .api_key("your-api-key")
27///     .api_secret("your-api-secret")
28///     .build()?;
29///
30/// // Advanced configuration
31/// let client = CloudClient::builder()
32///     .api_key("your-api-key")
33///     .api_secret("your-api-secret")
34///     .base_url("https://api.redislabs.com/v1".to_string())
35///     .timeout(std::time::Duration::from_secs(120))
36///     .build()?;
37/// # Ok::<(), Box<dyn std::error::Error>>(())
38/// ```
39#[derive(Debug, Clone)]
40pub struct CloudClientBuilder {
41    api_key: Option<String>,
42    api_secret: Option<String>,
43    base_url: String,
44    timeout: std::time::Duration,
45}
46
47impl Default for CloudClientBuilder {
48    fn default() -> Self {
49        Self {
50            api_key: None,
51            api_secret: None,
52            base_url: "https://api.redislabs.com/v1".to_string(),
53            timeout: std::time::Duration::from_secs(30),
54        }
55    }
56}
57
58impl CloudClientBuilder {
59    /// Create a new builder
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Set the API key
65    pub fn api_key(mut self, key: impl Into<String>) -> Self {
66        self.api_key = Some(key.into());
67        self
68    }
69
70    /// Set the API secret
71    pub fn api_secret(mut self, secret: impl Into<String>) -> Self {
72        self.api_secret = Some(secret.into());
73        self
74    }
75
76    /// Set the base URL
77    pub fn base_url(mut self, url: impl Into<String>) -> Self {
78        self.base_url = url.into();
79        self
80    }
81
82    /// Set the timeout
83    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
84        self.timeout = timeout;
85        self
86    }
87
88    /// Build the client
89    pub fn build(self) -> Result<CloudClient> {
90        let api_key = self
91            .api_key
92            .ok_or_else(|| RestError::ConnectionError("API key is required".to_string()))?;
93        let api_secret = self
94            .api_secret
95            .ok_or_else(|| RestError::ConnectionError("API secret is required".to_string()))?;
96
97        let client = Client::builder()
98            .timeout(self.timeout)
99            .build()
100            .map_err(|e| RestError::ConnectionError(e.to_string()))?;
101
102        Ok(CloudClient {
103            api_key,
104            api_secret,
105            base_url: self.base_url,
106            timeout: self.timeout,
107            client: Arc::new(client),
108        })
109    }
110}
111
112/// Redis Cloud API client
113#[derive(Clone)]
114pub struct CloudClient {
115    pub(crate) api_key: String,
116    pub(crate) api_secret: String,
117    pub(crate) base_url: String,
118    #[allow(dead_code)]
119    pub(crate) timeout: std::time::Duration,
120    pub(crate) client: Arc<Client>,
121}
122
123impl CloudClient {
124    /// Create a new builder for the client
125    pub fn builder() -> CloudClientBuilder {
126        CloudClientBuilder::new()
127    }
128
129    /// Make a GET request with API key authentication
130    pub async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
131        let url = format!("{}{}", self.base_url, path);
132
133        // Redis Cloud API uses these headers for authentication
134        let response = self
135            .client
136            .get(&url)
137            .header("x-api-key", &self.api_key)
138            .header("x-api-secret-key", &self.api_secret)
139            .send()
140            .await?;
141
142        self.handle_response(response).await
143    }
144
145    /// Make a POST request
146    pub async fn post<B: Serialize, T: serde::de::DeserializeOwned>(
147        &self,
148        path: &str,
149        body: &B,
150    ) -> Result<T> {
151        let url = format!("{}{}", self.base_url, path);
152
153        // Same backwards header naming as GET
154        let response = self
155            .client
156            .post(&url)
157            .header("x-api-key", &self.api_key)
158            .header("x-api-secret-key", &self.api_secret)
159            .json(body)
160            .send()
161            .await?;
162
163        self.handle_response(response).await
164    }
165
166    /// Make a PUT request
167    pub async fn put<B: Serialize, T: serde::de::DeserializeOwned>(
168        &self,
169        path: &str,
170        body: &B,
171    ) -> Result<T> {
172        let url = format!("{}{}", self.base_url, path);
173
174        // Same backwards header naming as GET
175        let response = self
176            .client
177            .put(&url)
178            .header("x-api-key", &self.api_key)
179            .header("x-api-secret-key", &self.api_secret)
180            .json(body)
181            .send()
182            .await?;
183
184        self.handle_response(response).await
185    }
186
187    /// Make a DELETE request
188    pub async fn delete(&self, path: &str) -> Result<()> {
189        let url = format!("{}{}", self.base_url, path);
190
191        // Same backwards header naming as GET
192        let response = self
193            .client
194            .delete(&url)
195            .header("x-api-key", &self.api_key)
196            .header("x-api-secret-key", &self.api_secret)
197            .send()
198            .await?;
199
200        if response.status().is_success() {
201            Ok(())
202        } else {
203            let status = response.status();
204            let text = response.text().await.unwrap_or_default();
205
206            match status.as_u16() {
207                400 => Err(RestError::BadRequest { message: text }),
208                401 => Err(RestError::AuthenticationFailed { message: text }),
209                403 => Err(RestError::Forbidden { message: text }),
210                404 => Err(RestError::NotFound { message: text }),
211                412 => Err(RestError::PreconditionFailed),
212                500 => Err(RestError::InternalServerError { message: text }),
213                503 => Err(RestError::ServiceUnavailable { message: text }),
214                _ => Err(RestError::ApiError {
215                    code: status.as_u16(),
216                    message: text,
217                }),
218            }
219        }
220    }
221
222    /// Execute raw GET request returning JSON Value
223    pub async fn get_raw(&self, path: &str) -> Result<serde_json::Value> {
224        self.get(path).await
225    }
226
227    /// Execute raw POST request with JSON body
228    pub async fn post_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
229        self.post(path, &body).await
230    }
231
232    /// Execute raw PUT request with JSON body
233    pub async fn put_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
234        self.put(path, &body).await
235    }
236
237    /// Execute raw PATCH request with JSON body
238    pub async fn patch_raw(
239        &self,
240        path: &str,
241        body: serde_json::Value,
242    ) -> Result<serde_json::Value> {
243        let url = format!("{}{}", self.base_url, path);
244
245        // Use backwards header names for compatibility
246        let response = self
247            .client
248            .patch(&url)
249            .header("x-api-key", &self.api_key)
250            .header("x-api-secret-key", &self.api_secret)
251            .json(&body)
252            .send()
253            .await?;
254
255        self.handle_response(response).await
256    }
257
258    /// Execute raw DELETE request returning any response body
259    pub async fn delete_raw(&self, path: &str) -> Result<serde_json::Value> {
260        let url = format!("{}{}", self.base_url, path);
261
262        // Use backwards header names for compatibility
263        let response = self
264            .client
265            .delete(&url)
266            .header("x-api-key", &self.api_key)
267            .header("x-api-secret-key", &self.api_secret)
268            .send()
269            .await?;
270
271        if response.status().is_success() {
272            if response.content_length() == Some(0) {
273                Ok(serde_json::json!({"status": "deleted"}))
274            } else {
275                response.json().await.map_err(Into::into)
276            }
277        } else {
278            let status = response.status();
279            let text = response.text().await.unwrap_or_default();
280
281            match status.as_u16() {
282                400 => Err(RestError::BadRequest { message: text }),
283                401 => Err(RestError::AuthenticationFailed { message: text }),
284                403 => Err(RestError::Forbidden { message: text }),
285                404 => Err(RestError::NotFound { message: text }),
286                412 => Err(RestError::PreconditionFailed),
287                500 => Err(RestError::InternalServerError { message: text }),
288                503 => Err(RestError::ServiceUnavailable { message: text }),
289                _ => Err(RestError::ApiError {
290                    code: status.as_u16(),
291                    message: text,
292                }),
293            }
294        }
295    }
296
297    /// Handle HTTP response
298    async fn handle_response<T: serde::de::DeserializeOwned>(
299        &self,
300        response: reqwest::Response,
301    ) -> Result<T> {
302        let status = response.status();
303
304        if status.is_success() {
305            // Try to get the response text first for debugging
306            let text = response.text().await.map_err(|e| {
307                RestError::ConnectionError(format!("Failed to read response: {}", e))
308            })?;
309
310            // Try to parse as JSON
311            serde_json::from_str::<T>(&text).map_err(|e| {
312                // If parsing fails, include the actual response for debugging
313                RestError::JsonError(e)
314            })
315        } else {
316            let text = response.text().await.unwrap_or_default();
317
318            match status.as_u16() {
319                400 => Err(RestError::BadRequest { message: text }),
320                401 => Err(RestError::AuthenticationFailed { message: text }),
321                403 => Err(RestError::Forbidden { message: text }),
322                404 => Err(RestError::NotFound { message: text }),
323                412 => Err(RestError::PreconditionFailed),
324                500 => Err(RestError::InternalServerError { message: text }),
325                503 => Err(RestError::ServiceUnavailable { message: text }),
326                _ => Err(RestError::ApiError {
327                    code: status.as_u16(),
328                    message: text,
329                }),
330            }
331        }
332    }
333}