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            Err(RestError::ApiError {
206                code: status.as_u16(),
207                message: text,
208            })
209        }
210    }
211
212    /// Execute raw GET request returning JSON Value
213    pub async fn get_raw(&self, path: &str) -> Result<serde_json::Value> {
214        self.get(path).await
215    }
216
217    /// Execute raw POST request with JSON body
218    pub async fn post_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
219        self.post(path, &body).await
220    }
221
222    /// Execute raw PUT request with JSON body
223    pub async fn put_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
224        self.put(path, &body).await
225    }
226
227    /// Execute raw PATCH request with JSON body
228    pub async fn patch_raw(
229        &self,
230        path: &str,
231        body: serde_json::Value,
232    ) -> Result<serde_json::Value> {
233        let url = format!("{}{}", self.base_url, path);
234
235        // Use backwards header names for compatibility
236        let response = self
237            .client
238            .patch(&url)
239            .header("x-api-key", &self.api_key)
240            .header("x-api-secret-key", &self.api_secret)
241            .json(&body)
242            .send()
243            .await?;
244
245        self.handle_response(response).await
246    }
247
248    /// Execute raw DELETE request returning any response body
249    pub async fn delete_raw(&self, path: &str) -> Result<serde_json::Value> {
250        let url = format!("{}{}", self.base_url, path);
251
252        // Use backwards header names for compatibility
253        let response = self
254            .client
255            .delete(&url)
256            .header("x-api-key", &self.api_key)
257            .header("x-api-secret-key", &self.api_secret)
258            .send()
259            .await?;
260
261        if response.status().is_success() {
262            if response.content_length() == Some(0) {
263                Ok(serde_json::json!({"status": "deleted"}))
264            } else {
265                response.json().await.map_err(Into::into)
266            }
267        } else {
268            let status = response.status();
269            let text = response.text().await.unwrap_or_default();
270            Err(RestError::ApiError {
271                code: status.as_u16(),
272                message: text,
273            })
274        }
275    }
276
277    /// Handle HTTP response
278    async fn handle_response<T: serde::de::DeserializeOwned>(
279        &self,
280        response: reqwest::Response,
281    ) -> Result<T> {
282        let status = response.status();
283
284        if status.is_success() {
285            // Try to get the response text first for debugging
286            let text = response.text().await.map_err(|e| {
287                RestError::ConnectionError(format!("Failed to read response: {}", e))
288            })?;
289
290            // Try to parse as JSON
291            serde_json::from_str::<T>(&text).map_err(|e| {
292                // If parsing fails, include the actual response for debugging
293                RestError::JsonError(e)
294            })
295        } else if status == 401 {
296            // Get the error message from the response
297            let text = response
298                .text()
299                .await
300                .unwrap_or_else(|_| "No error message".to_string());
301            Err(RestError::ApiError {
302                code: 401,
303                message: format!("Authentication failed: {}", text),
304            })
305        } else {
306            let text = response.text().await.unwrap_or_default();
307            Err(RestError::ApiError {
308                code: status.as_u16(),
309                message: text,
310            })
311        }
312    }
313}