Skip to main content

ekodb_client/
error.rs

1//! Error types for the ekoDB client
2
3use thiserror::Error;
4
5/// Result type alias for ekoDB client operations
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur when using the ekoDB client
9#[derive(Error, Debug)]
10pub enum Error {
11    /// HTTP request failed
12    #[error("HTTP request failed: {0}")]
13    Http(#[from] reqwest::Error),
14
15    /// API returned an error response
16    #[error("API error ({code}): {message}")]
17    Api {
18        /// HTTP status code
19        code: u16,
20        /// Error message from the server
21        message: String,
22    },
23
24    /// Token expired - can be retried with token refresh
25    #[error("Token expired, please refresh")]
26    TokenExpired,
27
28    /// Invalid URL
29    #[error("Invalid URL: {0}")]
30    InvalidUrl(#[from] url::ParseError),
31
32    /// Serialization error
33    #[error("Serialization error: {0}")]
34    Serialization(#[from] serde_json::Error),
35
36    /// WebSocket error
37    #[error("WebSocket error: {0}")]
38    WebSocket(String),
39
40    /// Connection error
41    #[error("Connection error: {0}")]
42    Connection(String),
43
44    /// Timeout error
45    #[error("Operation timed out")]
46    Timeout,
47
48    /// Rate limit exceeded
49    #[error("Rate limit exceeded. Retry after {retry_after_secs} seconds")]
50    RateLimit {
51        /// Seconds to wait before retrying
52        retry_after_secs: u64,
53    },
54
55    /// Service unavailable
56    #[error("Service unavailable: {0}")]
57    ServiceUnavailable(String),
58
59    /// Invalid configuration
60    #[error("Invalid configuration: {0}")]
61    InvalidConfig(String),
62
63    /// Record not found
64    #[error("Record not found")]
65    NotFound,
66
67    /// Validation error
68    #[error("Validation error: {0}")]
69    Validation(String),
70
71    /// Authentication error
72    #[error("Authentication error: {0}")]
73    Authentication(String),
74
75    /// Tool execution failed (server returned success=false)
76    #[error("Tool execution failed: {0}")]
77    ToolExecution(String),
78}
79
80impl Error {
81    /// Create an API error from status code and message
82    pub fn api(code: u16, message: impl Into<String>) -> Self {
83        Error::Api {
84            code,
85            message: message.into(),
86        }
87    }
88
89    /// Check if the error is retryable
90    pub fn is_retryable(&self) -> bool {
91        matches!(
92            self,
93            Error::RateLimit { .. }
94                | Error::ServiceUnavailable(_)
95                | Error::Timeout
96                | Error::Connection(_)
97        )
98    }
99
100    /// Get retry delay in seconds if applicable
101    pub fn retry_delay_secs(&self) -> Option<u64> {
102        match self {
103            Error::RateLimit { retry_after_secs } => Some(*retry_after_secs),
104            Error::ServiceUnavailable(_) => Some(10),
105            Error::Timeout => Some(5),
106            Error::Connection(_) => Some(3),
107            _ => None,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    // ========================================================================
117    // Error Construction Tests
118    // ========================================================================
119
120    #[test]
121    fn test_error_api_construction() {
122        let err = Error::api(404, "Not found");
123        match err {
124            Error::Api { code, message } => {
125                assert_eq!(code, 404);
126                assert_eq!(message, "Not found");
127            }
128            _ => panic!("Expected Api error"),
129        }
130    }
131
132    #[test]
133    fn test_error_api_with_string_message() {
134        let err = Error::api(500, String::from("Internal server error"));
135        match err {
136            Error::Api { code, message } => {
137                assert_eq!(code, 500);
138                assert_eq!(message, "Internal server error");
139            }
140            _ => panic!("Expected Api error"),
141        }
142    }
143
144    // ========================================================================
145    // Error Display Tests
146    // ========================================================================
147
148    #[test]
149    fn test_error_display_api() {
150        let err = Error::api(404, "Resource not found");
151        assert_eq!(format!("{}", err), "API error (404): Resource not found");
152    }
153
154    #[test]
155    fn test_error_display_auth() {
156        let err = Error::Authentication("Invalid credentials".to_string());
157        assert_eq!(
158            format!("{}", err),
159            "Authentication error: Invalid credentials"
160        );
161    }
162
163    #[test]
164    fn test_error_display_token_expired() {
165        let err = Error::TokenExpired;
166        assert_eq!(format!("{}", err), "Token expired, please refresh");
167    }
168
169    #[test]
170    fn test_error_display_timeout() {
171        let err = Error::Timeout;
172        assert_eq!(format!("{}", err), "Operation timed out");
173    }
174
175    #[test]
176    fn test_error_display_rate_limit() {
177        let err = Error::RateLimit {
178            retry_after_secs: 30,
179        };
180        assert_eq!(
181            format!("{}", err),
182            "Rate limit exceeded. Retry after 30 seconds"
183        );
184    }
185
186    #[test]
187    fn test_error_display_service_unavailable() {
188        let err = Error::ServiceUnavailable("Maintenance".to_string());
189        assert_eq!(format!("{}", err), "Service unavailable: Maintenance");
190    }
191
192    #[test]
193    fn test_error_display_not_found() {
194        let err = Error::NotFound;
195        assert_eq!(format!("{}", err), "Record not found");
196    }
197
198    #[test]
199    fn test_error_display_validation() {
200        let err = Error::Validation("Invalid email format".to_string());
201        assert_eq!(format!("{}", err), "Validation error: Invalid email format");
202    }
203
204    #[test]
205    fn test_error_display_connection() {
206        let err = Error::Connection("Connection refused".to_string());
207        assert_eq!(format!("{}", err), "Connection error: Connection refused");
208    }
209
210    #[test]
211    fn test_error_display_websocket() {
212        let err = Error::WebSocket("Connection closed".to_string());
213        assert_eq!(format!("{}", err), "WebSocket error: Connection closed");
214    }
215
216    #[test]
217    fn test_error_display_invalid_config() {
218        let err = Error::InvalidConfig("Missing API key".to_string());
219        assert_eq!(format!("{}", err), "Invalid configuration: Missing API key");
220    }
221
222    #[test]
223    fn test_error_display_authentication() {
224        let err = Error::Authentication("Token expired".to_string());
225        assert_eq!(format!("{}", err), "Authentication error: Token expired");
226    }
227
228    // ========================================================================
229    // Retryable Error Tests
230    // ========================================================================
231
232    #[test]
233    fn test_is_retryable_rate_limit() {
234        let err = Error::RateLimit {
235            retry_after_secs: 30,
236        };
237        assert!(err.is_retryable());
238    }
239
240    #[test]
241    fn test_is_retryable_service_unavailable() {
242        let err = Error::ServiceUnavailable("Maintenance".to_string());
243        assert!(err.is_retryable());
244    }
245
246    #[test]
247    fn test_is_retryable_timeout() {
248        let err = Error::Timeout;
249        assert!(err.is_retryable());
250    }
251
252    #[test]
253    fn test_is_retryable_connection() {
254        let err = Error::Connection("Connection reset".to_string());
255        assert!(err.is_retryable());
256    }
257
258    #[test]
259    fn test_is_not_retryable_api_error() {
260        let err = Error::api(404, "Not found");
261        assert!(!err.is_retryable());
262    }
263
264    #[test]
265    fn test_is_not_retryable_auth() {
266        let err = Error::Authentication("Invalid credentials".to_string());
267        assert!(!err.is_retryable());
268    }
269
270    #[test]
271    fn test_is_not_retryable_token_expired() {
272        let err = Error::TokenExpired;
273        assert!(!err.is_retryable());
274    }
275
276    #[test]
277    fn test_is_not_retryable_validation() {
278        let err = Error::Validation("Bad input".to_string());
279        assert!(!err.is_retryable());
280    }
281
282    #[test]
283    fn test_is_not_retryable_not_found() {
284        let err = Error::NotFound;
285        assert!(!err.is_retryable());
286    }
287
288    #[test]
289    fn test_is_not_retryable_invalid_config() {
290        let err = Error::InvalidConfig("Missing field".to_string());
291        assert!(!err.is_retryable());
292    }
293
294    #[test]
295    fn test_is_not_retryable_websocket() {
296        let err = Error::WebSocket("Connection closed".to_string());
297        assert!(!err.is_retryable());
298    }
299
300    #[test]
301    fn test_is_not_retryable_authentication() {
302        let err = Error::Authentication("Invalid token".to_string());
303        assert!(!err.is_retryable());
304    }
305
306    // ========================================================================
307    // Retry Delay Tests
308    // ========================================================================
309
310    #[test]
311    fn test_retry_delay_rate_limit() {
312        let err = Error::RateLimit {
313            retry_after_secs: 60,
314        };
315        assert_eq!(err.retry_delay_secs(), Some(60));
316    }
317
318    #[test]
319    fn test_retry_delay_rate_limit_custom() {
320        let err = Error::RateLimit {
321            retry_after_secs: 120,
322        };
323        assert_eq!(err.retry_delay_secs(), Some(120));
324    }
325
326    #[test]
327    fn test_retry_delay_service_unavailable() {
328        let err = Error::ServiceUnavailable("Maintenance".to_string());
329        assert_eq!(err.retry_delay_secs(), Some(10));
330    }
331
332    #[test]
333    fn test_retry_delay_timeout() {
334        let err = Error::Timeout;
335        assert_eq!(err.retry_delay_secs(), Some(5));
336    }
337
338    #[test]
339    fn test_retry_delay_connection() {
340        let err = Error::Connection("Failed".to_string());
341        assert_eq!(err.retry_delay_secs(), Some(3));
342    }
343
344    #[test]
345    fn test_retry_delay_none_for_api_error() {
346        let err = Error::api(404, "Not found");
347        assert_eq!(err.retry_delay_secs(), None);
348    }
349
350    #[test]
351    fn test_retry_delay_none_for_auth() {
352        let err = Error::Authentication("Failed".to_string());
353        assert_eq!(err.retry_delay_secs(), None);
354    }
355
356    #[test]
357    fn test_retry_delay_none_for_not_found() {
358        let err = Error::NotFound;
359        assert_eq!(err.retry_delay_secs(), None);
360    }
361
362    #[test]
363    fn test_retry_delay_none_for_validation() {
364        let err = Error::Validation("Invalid".to_string());
365        assert_eq!(err.retry_delay_secs(), None);
366    }
367
368    #[test]
369    fn test_retry_delay_none_for_token_expired() {
370        let err = Error::TokenExpired;
371        assert_eq!(err.retry_delay_secs(), None);
372    }
373
374    // ========================================================================
375    // Debug Trait Tests
376    // ========================================================================
377
378    #[test]
379    fn test_error_debug_format() {
380        let err = Error::api(500, "Server error");
381        let debug_str = format!("{:?}", err);
382        assert!(debug_str.contains("Api"));
383        assert!(debug_str.contains("500"));
384        assert!(debug_str.contains("Server error"));
385    }
386
387    // ========================================================================
388    // Edge Cases
389    // ========================================================================
390
391    #[test]
392    fn test_rate_limit_zero_seconds() {
393        let err = Error::RateLimit {
394            retry_after_secs: 0,
395        };
396        assert!(err.is_retryable());
397        assert_eq!(err.retry_delay_secs(), Some(0));
398    }
399
400    #[test]
401    fn test_empty_message_errors() {
402        let err = Error::Authentication(String::new());
403        assert_eq!(format!("{}", err), "Authentication error: ");
404
405        let err = Error::Connection(String::new());
406        assert_eq!(format!("{}", err), "Connection error: ");
407    }
408
409    #[test]
410    fn test_api_error_various_codes() {
411        for code in [400, 401, 403, 404, 500, 502, 503] {
412            let err = Error::api(code, "test");
413            match err {
414                Error::Api { code: c, .. } => assert_eq!(c, code),
415                _ => panic!("Expected Api error"),
416            }
417        }
418    }
419}