zeal_sdk/
errors.rs

1//! Error types for the Zeal SDK
2
3/// Result type alias for Zeal SDK operations
4pub type Result<T> = std::result::Result<T, ZealError>;
5
6/// Main error type for the Zeal SDK  
7#[derive(Debug, thiserror::Error)]
8pub enum ZealError {
9    /// Network-related errors
10    #[error("Network error: {source}")]
11    NetworkError {
12        #[source]
13        source: reqwest::Error,
14        retryable: bool,
15    },
16
17    /// WebSocket-related errors
18    #[error("WebSocket error: {message}")]
19    WebSocketError { message: String },
20
21    /// JSON parsing errors
22    #[error("JSON error: {source}")]
23    JsonError {
24        #[source]
25        source: serde_json::Error,
26    },
27
28    /// Invalid configuration
29    #[error("Configuration error: {message}")]
30    ConfigurationError { message: String },
31
32    /// API errors from the server
33    #[error("API error ({status}): {message}")]
34    ApiError {
35        status: u16,
36        message: String,
37        error_code: Option<String>,
38    },
39
40    /// Resource not found
41    #[error("Resource not found: {resource} with ID '{id}'")]
42    NotFound { resource: String, id: String },
43
44    /// Validation errors
45    #[error("Validation error in field '{field}': {message}")]
46    ValidationError { field: String, message: String },
47
48    /// Authentication/authorization errors
49    #[error("Authentication error: {message}")]
50    AuthenticationError { message: String },
51
52    /// Rate limiting errors
53    #[error("Rate limit exceeded: {message}")]
54    RateLimitError {
55        message: String,
56        retry_after: Option<std::time::Duration>,
57    },
58
59    /// Timeout errors
60    #[error("Operation timed out: {operation}")]
61    TimeoutError { operation: String },
62
63    /// Connection errors
64    #[error("Connection error: {message}")]
65    ConnectionError { message: String },
66
67    /// Serialization errors
68    #[error("Serialization error: {source}")]
69    SerializationError {
70        #[source]
71        source: Box<dyn std::error::Error + Send + Sync>,
72    },
73
74    /// URL parsing errors
75    #[error("Invalid URL: {source}")]
76    InvalidUrl {
77        #[source]
78        source: url::ParseError,
79    },
80
81    /// I/O errors
82    #[error("I/O error: {source}")]
83    IoError {
84        #[source]
85        source: std::io::Error,
86    },
87
88    /// Generic errors
89    #[error("Error: {message}")]
90    Other { message: String },
91}
92
93impl Clone for ZealError {
94    fn clone(&self) -> Self {
95        match self {
96            Self::NetworkError { .. } => Self::Other {
97                message: "Network error".to_string(),
98            },
99            Self::WebSocketError { message } => Self::WebSocketError {
100                message: message.clone(),
101            },
102            Self::JsonError { .. } => Self::Other {
103                message: "JSON parsing error".to_string(),
104            },
105            Self::ConfigurationError { message } => Self::ConfigurationError {
106                message: message.clone(),
107            },
108            Self::ApiError {
109                status,
110                message,
111                error_code,
112            } => Self::ApiError {
113                status: *status,
114                message: message.clone(),
115                error_code: error_code.clone(),
116            },
117            Self::NotFound { resource, id } => Self::NotFound {
118                resource: resource.clone(),
119                id: id.clone(),
120            },
121            Self::ValidationError { field, message } => Self::ValidationError {
122                field: field.clone(),
123                message: message.clone(),
124            },
125            Self::AuthenticationError { message } => Self::AuthenticationError {
126                message: message.clone(),
127            },
128            Self::RateLimitError {
129                message,
130                retry_after,
131            } => Self::RateLimitError {
132                message: message.clone(),
133                retry_after: *retry_after,
134            },
135            Self::TimeoutError { operation } => Self::TimeoutError {
136                operation: operation.clone(),
137            },
138            Self::ConnectionError { message } => Self::ConnectionError {
139                message: message.clone(),
140            },
141            Self::SerializationError { .. } => Self::Other {
142                message: "Serialization error".to_string(),
143            },
144            Self::InvalidUrl { .. } => Self::Other {
145                message: "Invalid URL".to_string(),
146            },
147            Self::IoError { .. } => Self::Other {
148                message: "IO error".to_string(),
149            },
150            Self::Other { message } => Self::Other {
151                message: message.clone(),
152            },
153        }
154    }
155}
156
157impl ZealError {
158    /// Create a network error
159    pub fn network_error(source: reqwest::Error) -> Self {
160        let retryable = source.is_timeout()
161            || source.is_connect()
162            || source
163                .status()
164                .is_some_and(|s| matches!(s.as_u16(), 408 | 429 | 500..=599));
165
166        Self::NetworkError { source, retryable }
167    }
168
169    /// Create a WebSocket error
170    pub fn websocket_error<S: Into<String>>(message: S) -> Self {
171        Self::WebSocketError {
172            message: message.into(),
173        }
174    }
175
176    /// Create a configuration error
177    pub fn configuration_error<S: Into<String>>(message: S) -> Self {
178        Self::ConfigurationError {
179            message: message.into(),
180        }
181    }
182
183    /// Create an API error
184    pub fn api_error(status: u16, message: String, error_code: Option<String>) -> Self {
185        Self::ApiError {
186            status,
187            message,
188            error_code,
189        }
190    }
191
192    /// Create a not found error
193    pub fn not_found<S: Into<String>>(resource: S, id: S) -> Self {
194        Self::NotFound {
195            resource: resource.into(),
196            id: id.into(),
197        }
198    }
199
200    /// Create a validation error
201    pub fn validation_error<S: Into<String>>(field: S, message: S) -> Self {
202        Self::ValidationError {
203            field: field.into(),
204            message: message.into(),
205        }
206    }
207
208    /// Create an authentication error
209    pub fn authentication_error<S: Into<String>>(message: S) -> Self {
210        Self::AuthenticationError {
211            message: message.into(),
212        }
213    }
214
215    /// Create a rate limit error
216    pub fn rate_limit_error<S: Into<String>>(
217        message: S,
218        retry_after: Option<std::time::Duration>,
219    ) -> Self {
220        Self::RateLimitError {
221            message: message.into(),
222            retry_after,
223        }
224    }
225
226    /// Create a timeout error
227    pub fn timeout_error<S: Into<String>>(operation: S) -> Self {
228        Self::TimeoutError {
229            operation: operation.into(),
230        }
231    }
232
233    /// Create a connection error
234    pub fn connection_error<S: Into<String>>(message: S) -> Self {
235        Self::ConnectionError {
236            message: message.into(),
237        }
238    }
239
240    /// Create a generic error
241    pub fn other<S: Into<String>>(message: S) -> Self {
242        Self::Other {
243            message: message.into(),
244        }
245    }
246
247    /// Check if the error is retryable
248    pub fn is_retryable(&self) -> bool {
249        match self {
250            Self::NetworkError { retryable, .. } => *retryable,
251            Self::RateLimitError { .. } => true,
252            Self::TimeoutError { .. } => true,
253            Self::ConnectionError { .. } => true,
254            Self::ApiError { status, .. } => matches!(*status, 408 | 429 | 500..=599),
255            _ => false,
256        }
257    }
258
259    /// Get the retry delay if applicable
260    pub fn retry_after(&self) -> Option<std::time::Duration> {
261        match self {
262            Self::RateLimitError { retry_after, .. } => *retry_after,
263            _ => None,
264        }
265    }
266
267    /// Check if the error is a client error (4xx)
268    pub fn is_client_error(&self) -> bool {
269        match self {
270            Self::ApiError { status, .. } => matches!(*status, 400..=499),
271            Self::NotFound { .. } => true,
272            Self::ValidationError { .. } => true,
273            Self::AuthenticationError { .. } => true,
274            _ => false,
275        }
276    }
277
278    /// Check if the error is a server error (5xx)
279    pub fn is_server_error(&self) -> bool {
280        match self {
281            Self::ApiError { status, .. } => matches!(*status, 500..=599),
282            _ => false,
283        }
284    }
285}
286
287impl From<reqwest::Error> for ZealError {
288    fn from(err: reqwest::Error) -> Self {
289        Self::network_error(err)
290    }
291}
292
293impl From<serde_json::Error> for ZealError {
294    fn from(err: serde_json::Error) -> Self {
295        Self::JsonError { source: err }
296    }
297}
298
299impl From<url::ParseError> for ZealError {
300    fn from(err: url::ParseError) -> Self {
301        Self::InvalidUrl { source: err }
302    }
303}
304
305impl From<std::io::Error> for ZealError {
306    fn from(err: std::io::Error) -> Self {
307        Self::IoError { source: err }
308    }
309}
310
311impl From<tokio_tungstenite::tungstenite::Error> for ZealError {
312    fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
313        Self::websocket_error(err.to_string())
314    }
315}
316
317/// Error builder for constructing complex errors
318#[derive(Debug, Default)]
319pub struct ErrorBuilder {
320    message: Option<String>,
321    source: Option<Box<dyn std::error::Error + Send + Sync>>,
322    retryable: bool,
323    status: Option<u16>,
324    error_code: Option<String>,
325}
326
327impl ErrorBuilder {
328    /// Create a new error builder
329    pub fn new() -> Self {
330        Self::default()
331    }
332
333    /// Set the error message
334    pub fn message<S: Into<String>>(mut self, message: S) -> Self {
335        self.message = Some(message.into());
336        self
337    }
338
339    /// Set the source error
340    pub fn source<E: std::error::Error + Send + Sync + 'static>(mut self, source: E) -> Self {
341        self.source = Some(Box::new(source));
342        self
343    }
344
345    /// Mark the error as retryable
346    pub fn retryable(mut self, retryable: bool) -> Self {
347        self.retryable = retryable;
348        self
349    }
350
351    /// Set the HTTP status code
352    pub fn status(mut self, status: u16) -> Self {
353        self.status = Some(status);
354        self
355    }
356
357    /// Set the error code
358    pub fn error_code<S: Into<String>>(mut self, code: S) -> Self {
359        self.error_code = Some(code.into());
360        self
361    }
362
363    /// Build the error
364    pub fn build(self) -> ZealError {
365        let message = self.message.unwrap_or_else(|| "Unknown error".to_string());
366
367        if let Some(status) = self.status {
368            ZealError::ApiError {
369                status,
370                message,
371                error_code: self.error_code,
372            }
373        } else if let Some(source) = self.source {
374            ZealError::SerializationError { source }
375        } else {
376            ZealError::Other { message }
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_error_creation() {
387        let err = ZealError::not_found("template", "test-id");
388        assert!(matches!(err, ZealError::NotFound { .. }));
389    }
390
391    #[test]
392    fn test_retryable_errors() {
393        let err = ZealError::api_error(500, "Server error".to_string(), None);
394        assert!(err.is_retryable());
395
396        let err = ZealError::api_error(400, "Bad request".to_string(), None);
397        assert!(!err.is_retryable());
398    }
399
400    #[test]
401    fn test_error_builder() {
402        let err = ErrorBuilder::new()
403            .message("Test error")
404            .status(404)
405            .build();
406
407        assert!(matches!(err, ZealError::ApiError { status: 404, .. }));
408    }
409
410    #[test]
411    fn test_client_server_error_classification() {
412        let client_err = ZealError::api_error(400, "Bad request".to_string(), None);
413        assert!(client_err.is_client_error());
414        assert!(!client_err.is_server_error());
415
416        let server_err = ZealError::api_error(500, "Server error".to_string(), None);
417        assert!(!server_err.is_client_error());
418        assert!(server_err.is_server_error());
419    }
420}