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    /// Normalize URL path concatenation to avoid double slashes
130    fn normalize_url(&self, path: &str) -> String {
131        let base = self.base_url.trim_end_matches('/');
132        let path = path.trim_start_matches('/');
133        format!("{}/{}", base, path)
134    }
135
136    /// Make a GET request with API key authentication
137    pub async fn get<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
138        let url = self.normalize_url(path);
139
140        // Redis Cloud API uses these headers for authentication
141        let response = self
142            .client
143            .get(&url)
144            .header("x-api-key", &self.api_key)
145            .header("x-api-secret-key", &self.api_secret)
146            .send()
147            .await?;
148
149        self.handle_response(response).await
150    }
151
152    /// Make a POST request
153    pub async fn post<B: Serialize, T: serde::de::DeserializeOwned>(
154        &self,
155        path: &str,
156        body: &B,
157    ) -> Result<T> {
158        let url = self.normalize_url(path);
159
160        // Same backwards header naming as GET
161        let response = self
162            .client
163            .post(&url)
164            .header("x-api-key", &self.api_key)
165            .header("x-api-secret-key", &self.api_secret)
166            .json(body)
167            .send()
168            .await?;
169
170        self.handle_response(response).await
171    }
172
173    /// Make a PUT request
174    pub async fn put<B: Serialize, T: serde::de::DeserializeOwned>(
175        &self,
176        path: &str,
177        body: &B,
178    ) -> Result<T> {
179        let url = self.normalize_url(path);
180
181        // Same backwards header naming as GET
182        let response = self
183            .client
184            .put(&url)
185            .header("x-api-key", &self.api_key)
186            .header("x-api-secret-key", &self.api_secret)
187            .json(body)
188            .send()
189            .await?;
190
191        self.handle_response(response).await
192    }
193
194    /// Make a DELETE request
195    pub async fn delete(&self, path: &str) -> Result<()> {
196        let url = self.normalize_url(path);
197
198        // Same backwards header naming as GET
199        let response = self
200            .client
201            .delete(&url)
202            .header("x-api-key", &self.api_key)
203            .header("x-api-secret-key", &self.api_secret)
204            .send()
205            .await?;
206
207        if response.status().is_success() {
208            Ok(())
209        } else {
210            let status = response.status();
211            let text = response.text().await.unwrap_or_default();
212
213            match status.as_u16() {
214                400 => Err(RestError::BadRequest { message: text }),
215                401 => Err(RestError::AuthenticationFailed { message: text }),
216                403 => Err(RestError::Forbidden { message: text }),
217                404 => Err(RestError::NotFound { message: text }),
218                412 => Err(RestError::PreconditionFailed),
219                500 => Err(RestError::InternalServerError { message: text }),
220                503 => Err(RestError::ServiceUnavailable { message: text }),
221                _ => Err(RestError::ApiError {
222                    code: status.as_u16(),
223                    message: text,
224                }),
225            }
226        }
227    }
228
229    /// Execute raw GET request returning JSON Value
230    pub async fn get_raw(&self, path: &str) -> Result<serde_json::Value> {
231        self.get(path).await
232    }
233
234    /// Execute raw POST request with JSON body
235    pub async fn post_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
236        self.post(path, &body).await
237    }
238
239    /// Execute raw PUT request with JSON body
240    pub async fn put_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
241        self.put(path, &body).await
242    }
243
244    /// Execute raw PATCH request with JSON body
245    pub async fn patch_raw(
246        &self,
247        path: &str,
248        body: serde_json::Value,
249    ) -> Result<serde_json::Value> {
250        let url = self.normalize_url(path);
251
252        // Use backwards header names for compatibility
253        let response = self
254            .client
255            .patch(&url)
256            .header("x-api-key", &self.api_key)
257            .header("x-api-secret-key", &self.api_secret)
258            .json(&body)
259            .send()
260            .await?;
261
262        self.handle_response(response).await
263    }
264
265    /// Execute raw DELETE request returning any response body
266    pub async fn delete_raw(&self, path: &str) -> Result<serde_json::Value> {
267        let url = self.normalize_url(path);
268
269        // Use backwards header names for compatibility
270        let response = self
271            .client
272            .delete(&url)
273            .header("x-api-key", &self.api_key)
274            .header("x-api-secret-key", &self.api_secret)
275            .send()
276            .await?;
277
278        if response.status().is_success() {
279            if response.content_length() == Some(0) {
280                Ok(serde_json::json!({"status": "deleted"}))
281            } else {
282                response.json().await.map_err(Into::into)
283            }
284        } else {
285            let status = response.status();
286            let text = response.text().await.unwrap_or_default();
287
288            match status.as_u16() {
289                400 => Err(RestError::BadRequest { message: text }),
290                401 => Err(RestError::AuthenticationFailed { message: text }),
291                403 => Err(RestError::Forbidden { message: text }),
292                404 => Err(RestError::NotFound { message: text }),
293                412 => Err(RestError::PreconditionFailed),
294                500 => Err(RestError::InternalServerError { message: text }),
295                503 => Err(RestError::ServiceUnavailable { message: text }),
296                _ => Err(RestError::ApiError {
297                    code: status.as_u16(),
298                    message: text,
299                }),
300            }
301        }
302    }
303
304    /// Handle HTTP response
305    async fn handle_response<T: serde::de::DeserializeOwned>(
306        &self,
307        response: reqwest::Response,
308    ) -> Result<T> {
309        let status = response.status();
310
311        if status.is_success() {
312            // Try to get the response text first for debugging
313            let text = response.text().await.map_err(|e| {
314                RestError::ConnectionError(format!("Failed to read response: {}", e))
315            })?;
316
317            // Try to parse as JSON
318            serde_json::from_str::<T>(&text).map_err(|e| {
319                // If parsing fails, include the actual response for debugging
320                RestError::JsonError(e)
321            })
322        } else {
323            let text = response.text().await.unwrap_or_default();
324
325            match status.as_u16() {
326                400 => Err(RestError::BadRequest { message: text }),
327                401 => Err(RestError::AuthenticationFailed { message: text }),
328                403 => Err(RestError::Forbidden { message: text }),
329                404 => Err(RestError::NotFound { message: text }),
330                412 => Err(RestError::PreconditionFailed),
331                500 => Err(RestError::InternalServerError { message: text }),
332                503 => Err(RestError::ServiceUnavailable { message: text }),
333                _ => Err(RestError::ApiError {
334                    code: status.as_u16(),
335                    message: text,
336                }),
337            }
338        }
339    }
340}