redis_enterprise/
client.rs

1//! REST API client implementation
2
3use crate::error::{RestError, Result};
4use reqwest::{Client, Response};
5use serde::{Serialize, de::DeserializeOwned};
6use std::sync::Arc;
7use std::time::Duration;
8use tracing::{debug, trace};
9
10// Legacy alias for backwards compatibility during migration
11pub type RestConfig = EnterpriseClientBuilder;
12
13/// Builder for EnterpriseClient
14#[derive(Debug, Clone)]
15pub struct EnterpriseClientBuilder {
16    base_url: String,
17    username: Option<String>,
18    password: Option<String>,
19    timeout: Duration,
20    insecure: bool,
21}
22
23impl Default for EnterpriseClientBuilder {
24    fn default() -> Self {
25        Self {
26            base_url: "https://localhost:9443".to_string(),
27            username: None,
28            password: None,
29            timeout: Duration::from_secs(30),
30            insecure: false,
31        }
32    }
33}
34
35impl EnterpriseClientBuilder {
36    /// Create a new builder
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Set the base URL
42    pub fn base_url(mut self, url: impl Into<String>) -> Self {
43        self.base_url = url.into();
44        self
45    }
46
47    /// Set the username
48    pub fn username(mut self, username: impl Into<String>) -> Self {
49        self.username = Some(username.into());
50        self
51    }
52
53    /// Set the password
54    pub fn password(mut self, password: impl Into<String>) -> Self {
55        self.password = Some(password.into());
56        self
57    }
58
59    /// Set the timeout
60    pub fn timeout(mut self, timeout: Duration) -> Self {
61        self.timeout = timeout;
62        self
63    }
64
65    /// Allow insecure TLS connections (self-signed certificates)
66    pub fn insecure(mut self, insecure: bool) -> Self {
67        self.insecure = insecure;
68        self
69    }
70
71    /// Build the client
72    pub fn build(self) -> Result<EnterpriseClient> {
73        let username = self.username.unwrap_or_default();
74        let password = self.password.unwrap_or_default();
75
76        let client_builder = Client::builder()
77            .timeout(self.timeout)
78            .danger_accept_invalid_certs(self.insecure);
79
80        let client = client_builder
81            .build()
82            .map_err(|e| RestError::ConnectionError(e.to_string()))?;
83
84        Ok(EnterpriseClient {
85            base_url: self.base_url,
86            username,
87            password,
88            timeout: self.timeout,
89            client: Arc::new(client),
90        })
91    }
92}
93
94/// REST API client for Redis Enterprise
95#[derive(Clone)]
96pub struct EnterpriseClient {
97    base_url: String,
98    username: String,
99    password: String,
100    timeout: Duration,
101    client: Arc<Client>,
102}
103
104// Alias for backwards compatibility
105pub type RestClient = EnterpriseClient;
106
107impl EnterpriseClient {
108    /// Create a new builder for the client
109    pub fn builder() -> EnterpriseClientBuilder {
110        EnterpriseClientBuilder::new()
111    }
112
113    /// Create a client from environment variables
114    ///
115    /// Reads configuration from:
116    /// - `REDIS_ENTERPRISE_URL`: Base URL for the cluster (default: "https://localhost:9443")
117    /// - `REDIS_ENTERPRISE_USER`: Username for authentication (default: "admin@redis.local")
118    /// - `REDIS_ENTERPRISE_PASSWORD`: Password for authentication (required)
119    /// - `REDIS_ENTERPRISE_INSECURE`: Set to "true" to skip SSL verification (default: "false")
120    pub fn from_env() -> Result<Self> {
121        use std::env;
122
123        let base_url = env::var("REDIS_ENTERPRISE_URL")
124            .unwrap_or_else(|_| "https://localhost:9443".to_string());
125        let username =
126            env::var("REDIS_ENTERPRISE_USER").unwrap_or_else(|_| "admin@redis.local".to_string());
127        let password =
128            env::var("REDIS_ENTERPRISE_PASSWORD").map_err(|_| RestError::AuthenticationFailed)?;
129        let insecure = env::var("REDIS_ENTERPRISE_INSECURE")
130            .unwrap_or_else(|_| "false".to_string())
131            .parse::<bool>()
132            .unwrap_or(false);
133
134        Self::builder()
135            .base_url(base_url)
136            .username(username)
137            .password(password)
138            .insecure(insecure)
139            .build()
140    }
141
142    /// Make a GET request
143    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
144        let url = format!("{}{}", self.base_url, path);
145        debug!("GET {}", url);
146
147        let response = self
148            .client
149            .get(&url)
150            .basic_auth(&self.username, Some(&self.password))
151            .send()
152            .await
153            .map_err(|e| self.map_reqwest_error(e, &url))?;
154
155        trace!("Response status: {}", response.status());
156        self.handle_response(response).await
157    }
158
159    /// Make a GET request for text content
160    pub async fn get_text(&self, path: &str) -> Result<String> {
161        let url = format!("{}{}", self.base_url, path);
162        debug!("GET {} (text)", url);
163
164        let response = self
165            .client
166            .get(&url)
167            .basic_auth(&self.username, Some(&self.password))
168            .send()
169            .await
170            .map_err(|e| self.map_reqwest_error(e, &url))?;
171
172        trace!("Response status: {}", response.status());
173
174        if response.status().is_success() {
175            let text = response
176                .text()
177                .await
178                .map_err(crate::error::RestError::RequestFailed)?;
179            Ok(text)
180        } else {
181            let status = response.status();
182            let error_text = response
183                .text()
184                .await
185                .unwrap_or_else(|_| "Unknown error".to_string());
186            Err(crate::error::RestError::ApiError {
187                code: status.as_u16(),
188                message: error_text,
189            })
190        }
191    }
192
193    /// Make a POST request
194    pub async fn post<B: Serialize, T: DeserializeOwned>(&self, path: &str, body: &B) -> Result<T> {
195        let url = format!("{}{}", self.base_url, path);
196        debug!("POST {}", url);
197        trace!("Request body: {:?}", serde_json::to_value(body).ok());
198
199        let response = self
200            .client
201            .post(&url)
202            .basic_auth(&self.username, Some(&self.password))
203            .json(body)
204            .send()
205            .await
206            .map_err(|e| self.map_reqwest_error(e, &url))?;
207
208        trace!("Response status: {}", response.status());
209        self.handle_response(response).await
210    }
211
212    /// Make a PUT request
213    pub async fn put<B: Serialize, T: DeserializeOwned>(&self, path: &str, body: &B) -> Result<T> {
214        let url = format!("{}{}", self.base_url, path);
215        debug!("PUT {}", url);
216        trace!("Request body: {:?}", serde_json::to_value(body).ok());
217
218        let response = self
219            .client
220            .put(&url)
221            .basic_auth(&self.username, Some(&self.password))
222            .json(body)
223            .send()
224            .await
225            .map_err(|e| self.map_reqwest_error(e, &url))?;
226
227        trace!("Response status: {}", response.status());
228        self.handle_response(response).await
229    }
230
231    /// Make a DELETE request
232    pub async fn delete(&self, path: &str) -> Result<()> {
233        let url = format!("{}{}", self.base_url, path);
234        debug!("DELETE {}", url);
235
236        let response = self
237            .client
238            .delete(&url)
239            .basic_auth(&self.username, Some(&self.password))
240            .send()
241            .await
242            .map_err(|e| self.map_reqwest_error(e, &url))?;
243
244        trace!("Response status: {}", response.status());
245        if response.status().is_success() {
246            Ok(())
247        } else {
248            let status = response.status();
249            let text = response.text().await.unwrap_or_default();
250            Err(RestError::ApiError {
251                code: status.as_u16(),
252                message: text,
253            })
254        }
255    }
256
257    /// Execute raw GET request returning JSON Value
258    pub async fn get_raw(&self, path: &str) -> Result<serde_json::Value> {
259        self.get(path).await
260    }
261
262    /// Execute raw POST request with JSON body
263    pub async fn post_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
264        self.post(path, &body).await
265    }
266
267    /// Execute raw PUT request with JSON body
268    pub async fn put_raw(&self, path: &str, body: serde_json::Value) -> Result<serde_json::Value> {
269        self.put(path, &body).await
270    }
271
272    /// POST request for actions that return no content
273    pub async fn post_action<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
274        let url = format!("{}{}", self.base_url, path);
275        debug!("POST {}", url);
276        trace!("Request body: {:?}", serde_json::to_value(body).ok());
277
278        let response = self
279            .client
280            .post(&url)
281            .basic_auth(&self.username, Some(&self.password))
282            .json(body)
283            .send()
284            .await
285            .map_err(|e| self.map_reqwest_error(e, &url))?;
286
287        trace!("Response status: {}", response.status());
288        if response.status().is_success() {
289            Ok(())
290        } else {
291            let status = response.status();
292            let text = response.text().await.unwrap_or_default();
293            Err(RestError::ApiError {
294                code: status.as_u16(),
295                message: text,
296            })
297        }
298    }
299
300    /// Get a reference to self for handler construction
301    pub fn rest_client(&self) -> Self {
302        self.clone()
303    }
304
305    /// POST request for bootstrap - handles empty response
306    pub async fn post_bootstrap<B: Serialize>(
307        &self,
308        path: &str,
309        body: &B,
310    ) -> Result<serde_json::Value> {
311        let url = format!("{}{}", self.base_url, path);
312
313        let response = self
314            .client
315            .post(&url)
316            .basic_auth(&self.username, Some(&self.password))
317            .json(body)
318            .send()
319            .await
320            .map_err(|e| self.map_reqwest_error(e, &url))?;
321
322        let status = response.status();
323        if status.is_success() {
324            // Try to parse JSON, but if empty/invalid, return success
325            let text = response.text().await.unwrap_or_default();
326            if text.is_empty() || text.trim().is_empty() {
327                Ok(serde_json::json!({"status": "success"}))
328            } else {
329                Ok(serde_json::from_str(&text)
330                    .unwrap_or_else(|_| serde_json::json!({"status": "success", "response": text})))
331            }
332        } else {
333            let text = response.text().await.unwrap_or_default();
334            Err(RestError::ApiError {
335                code: status.as_u16(),
336                message: text,
337            })
338        }
339    }
340
341    /// Execute raw PATCH request with JSON body
342    pub async fn patch_raw(
343        &self,
344        path: &str,
345        body: serde_json::Value,
346    ) -> Result<serde_json::Value> {
347        let url = format!("{}{}", self.base_url, path);
348        let response = self
349            .client
350            .patch(&url)
351            .basic_auth(&self.username, Some(&self.password))
352            .json(&body)
353            .send()
354            .await
355            .map_err(|e| self.map_reqwest_error(e, &url))?;
356
357        if response.status().is_success() {
358            response
359                .json()
360                .await
361                .map_err(|e| RestError::ParseError(e.to_string()))
362        } else {
363            let status = response.status();
364            let text = response.text().await.unwrap_or_default();
365            Err(RestError::ApiError {
366                code: status.as_u16(),
367                message: text,
368            })
369        }
370    }
371
372    /// Execute raw DELETE request returning any response body
373    pub async fn delete_raw(&self, path: &str) -> Result<serde_json::Value> {
374        let url = format!("{}{}", self.base_url, path);
375        let response = self
376            .client
377            .delete(&url)
378            .basic_auth(&self.username, Some(&self.password))
379            .send()
380            .await
381            .map_err(|e| self.map_reqwest_error(e, &url))?;
382
383        if response.status().is_success() {
384            if response.content_length() == Some(0) {
385                Ok(serde_json::json!({"status": "deleted"}))
386            } else {
387                response
388                    .json()
389                    .await
390                    .map_err(|e| RestError::ParseError(e.to_string()))
391            }
392        } else {
393            let status = response.status();
394            let text = response.text().await.unwrap_or_default();
395            Err(RestError::ApiError {
396                code: status.as_u16(),
397                message: text,
398            })
399        }
400    }
401
402    /// Map reqwest errors to more specific error messages
403    fn map_reqwest_error(&self, error: reqwest::Error, url: &str) -> RestError {
404        if error.is_connect() {
405            RestError::ConnectionError(format!(
406                "Failed to connect to {}: Connection refused or host unreachable. Check if the Redis Enterprise server is running and accessible.",
407                url
408            ))
409        } else if error.is_timeout() {
410            RestError::ConnectionError(format!(
411                "Request to {} timed out after {:?}. Check network connectivity or increase timeout.",
412                url, self.timeout
413            ))
414        } else if error.is_decode() {
415            RestError::ConnectionError(format!(
416                "Failed to decode JSON response from {}: {}. Server may have returned invalid JSON or HTML error page.",
417                url, error
418            ))
419        } else if let Some(status) = error.status() {
420            RestError::ApiError {
421                code: status.as_u16(),
422                message: format!("HTTP {} from {}: {}", status.as_u16(), url, error),
423            }
424        } else if error.is_request() {
425            RestError::ConnectionError(format!(
426                "Request to {} failed: {}. Check URL format and network settings.",
427                url, error
428            ))
429        } else {
430            RestError::RequestFailed(error)
431        }
432    }
433
434    /// Handle HTTP response
435    async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
436        if response.status().is_success() {
437            response.json::<T>().await.map_err(Into::into)
438        } else {
439            let status = response.status();
440            let text = response.text().await.unwrap_or_default();
441
442            match status.as_u16() {
443                401 => Err(RestError::Unauthorized),
444                404 => Err(RestError::NotFound),
445                500..=599 => Err(RestError::ServerError(text)),
446                _ => Err(RestError::ApiError {
447                    code: status.as_u16(),
448                    message: text,
449                }),
450            }
451        }
452    }
453}