Skip to main content

redisctl_core/
error.rs

1//! Unified error handling for redisctl-core
2//!
3//! Wraps both Cloud and Enterprise errors with consistent helper methods.
4//!
5//! # Example
6//!
7//! ```rust
8//! use redisctl_core::{CoreError, Result};
9//! use redis_cloud::CloudError;
10//!
11//! fn handle_error(err: CoreError) {
12//!     if err.is_not_found() {
13//!         println!("Resource not found");
14//!     } else if err.is_retryable() {
15//!         println!("Temporary error, can retry");
16//!     }
17//! }
18//!
19//! // Cloud errors are automatically converted
20//! let cloud_err = CloudError::NotFound { message: "DB not found".to_string() };
21//! let core_err: CoreError = cloud_err.into();
22//! assert!(core_err.is_not_found());
23//! ```
24
25use std::time::Duration;
26use thiserror::Error;
27
28/// Core error type wrapping both platform errors
29#[derive(Error, Debug)]
30pub enum CoreError {
31    /// Error from Redis Cloud API
32    #[error("Cloud API error: {0}")]
33    Cloud(#[from] redis_cloud::CloudError),
34
35    /// Error from Redis Enterprise API
36    #[error("Enterprise API error: {0}")]
37    Enterprise(#[from] redis_enterprise::RestError),
38
39    /// Task timed out waiting for completion
40    #[error("Task timed out after {0:?}")]
41    TaskTimeout(Duration),
42
43    /// Task failed during async operation
44    #[error("Task failed: {0}")]
45    TaskFailed(String),
46
47    /// Validation error (e.g., module resolution)
48    #[error("Validation error: {0}")]
49    Validation(String),
50
51    /// Configuration error
52    #[error("Configuration error: {0}")]
53    Config(String),
54}
55
56/// Result type alias for core operations
57pub type Result<T> = std::result::Result<T, CoreError>;
58
59impl CoreError {
60    /// Returns true if this is a "not found" error (404)
61    #[must_use]
62    pub fn is_not_found(&self) -> bool {
63        match self {
64            CoreError::Cloud(e) => e.is_not_found(),
65            CoreError::Enterprise(e) => e.is_not_found(),
66            _ => false,
67        }
68    }
69
70    /// Returns true if this is an authentication/authorization error (401/403)
71    #[must_use]
72    pub fn is_unauthorized(&self) -> bool {
73        match self {
74            CoreError::Cloud(e) => e.is_unauthorized(),
75            CoreError::Enterprise(e) => e.is_unauthorized(),
76            _ => false,
77        }
78    }
79
80    /// Returns true if this is a server error (5xx)
81    #[must_use]
82    pub fn is_server_error(&self) -> bool {
83        match self {
84            CoreError::Cloud(e) => e.is_server_error(),
85            CoreError::Enterprise(e) => e.is_server_error(),
86            _ => false,
87        }
88    }
89
90    /// Returns true if this is a timeout error
91    #[must_use]
92    pub fn is_timeout(&self) -> bool {
93        match self {
94            CoreError::Cloud(e) => e.is_timeout(),
95            CoreError::Enterprise(e) => e.is_timeout(),
96            CoreError::TaskTimeout(_) => true,
97            _ => false,
98        }
99    }
100
101    /// Returns true if this is a rate limiting error (429)
102    #[must_use]
103    pub fn is_rate_limited(&self) -> bool {
104        match self {
105            CoreError::Cloud(e) => e.is_rate_limited(),
106            CoreError::Enterprise(e) => e.is_rate_limited(),
107            _ => false,
108        }
109    }
110
111    /// Returns true if this is a conflict/precondition error (409/412)
112    #[must_use]
113    pub fn is_conflict(&self) -> bool {
114        match self {
115            CoreError::Cloud(e) => e.is_conflict(),
116            CoreError::Enterprise(e) => e.is_conflict(),
117            _ => false,
118        }
119    }
120
121    /// Returns true if this is a bad request error (400)
122    #[must_use]
123    pub fn is_bad_request(&self) -> bool {
124        match self {
125            CoreError::Cloud(e) => e.is_bad_request(),
126            CoreError::Enterprise(e) => e.is_bad_request(),
127            CoreError::Validation(_) => true,
128            _ => false,
129        }
130    }
131
132    /// Returns true if this error is potentially retryable
133    #[must_use]
134    pub fn is_retryable(&self) -> bool {
135        match self {
136            CoreError::Cloud(e) => e.is_retryable(),
137            CoreError::Enterprise(e) => e.is_retryable(),
138            CoreError::TaskTimeout(_) => true, // Timeout might succeed on retry
139            _ => false,
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use redis_cloud::CloudError;
148    use redis_enterprise::RestError;
149
150    #[test]
151    fn test_core_error_from_cloud() {
152        let cloud_err = CloudError::NotFound {
153            message: "Database not found".to_string(),
154        };
155        let core_err: CoreError = cloud_err.into();
156
157        assert!(core_err.is_not_found());
158        assert!(!core_err.is_unauthorized());
159        assert!(!core_err.is_retryable());
160    }
161
162    #[test]
163    fn test_core_error_from_enterprise() {
164        let enterprise_err = RestError::NotFound;
165        let core_err: CoreError = enterprise_err.into();
166
167        assert!(core_err.is_not_found());
168        assert!(!core_err.is_unauthorized());
169    }
170
171    #[test]
172    fn test_core_error_cloud_helpers_delegate() {
173        // Test that all helper methods properly delegate to Cloud errors
174        let unauthorized = CloudError::AuthenticationFailed {
175            message: "Bad creds".to_string(),
176        };
177        let core_err: CoreError = unauthorized.into();
178        assert!(core_err.is_unauthorized());
179
180        let rate_limited = CloudError::RateLimited {
181            message: "Too many requests".to_string(),
182        };
183        let core_err: CoreError = rate_limited.into();
184        assert!(core_err.is_rate_limited());
185        assert!(core_err.is_retryable());
186
187        let bad_request = CloudError::BadRequest {
188            message: "Invalid input".to_string(),
189        };
190        let core_err: CoreError = bad_request.into();
191        assert!(core_err.is_bad_request());
192    }
193
194    #[test]
195    fn test_core_error_enterprise_helpers_delegate() {
196        // Test that all helper methods properly delegate to Enterprise errors
197        let unauthorized = RestError::AuthenticationFailed;
198        let core_err: CoreError = unauthorized.into();
199        assert!(core_err.is_unauthorized());
200
201        let server_error = RestError::ServerError("Internal error".to_string());
202        let core_err: CoreError = server_error.into();
203        assert!(core_err.is_server_error());
204        assert!(core_err.is_retryable());
205    }
206
207    #[test]
208    fn test_core_error_task_timeout() {
209        let err = CoreError::TaskTimeout(Duration::from_secs(600));
210        assert!(err.is_timeout());
211        assert!(err.is_retryable()); // Timeouts are retryable
212        assert!(!err.is_not_found());
213    }
214
215    #[test]
216    fn test_core_error_validation() {
217        let err = CoreError::Validation("Invalid module name".to_string());
218        assert!(err.is_bad_request()); // Validation errors map to bad request
219        assert!(!err.is_retryable());
220    }
221
222    #[test]
223    fn test_core_error_display() {
224        let cloud_err: CoreError = CloudError::NotFound {
225            message: "Not found".to_string(),
226        }
227        .into();
228        assert!(cloud_err.to_string().contains("Cloud API error"));
229
230        let timeout_err = CoreError::TaskTimeout(Duration::from_secs(60));
231        assert!(timeout_err.to_string().contains("timed out"));
232    }
233}