1pub type Result<T> = std::result::Result<T, ZealError>;
5
6#[derive(Debug, thiserror::Error)]
8pub enum ZealError {
9 #[error("Network error: {source}")]
11 NetworkError {
12 #[source]
13 source: reqwest::Error,
14 retryable: bool,
15 },
16
17 #[error("WebSocket error: {message}")]
19 WebSocketError { message: String },
20
21 #[error("JSON error: {source}")]
23 JsonError {
24 #[source]
25 source: serde_json::Error,
26 },
27
28 #[error("Configuration error: {message}")]
30 ConfigurationError { message: String },
31
32 #[error("API error ({status}): {message}")]
34 ApiError {
35 status: u16,
36 message: String,
37 error_code: Option<String>,
38 },
39
40 #[error("Resource not found: {resource} with ID '{id}'")]
42 NotFound { resource: String, id: String },
43
44 #[error("Validation error in field '{field}': {message}")]
46 ValidationError { field: String, message: String },
47
48 #[error("Authentication error: {message}")]
50 AuthenticationError { message: String },
51
52 #[error("Rate limit exceeded: {message}")]
54 RateLimitError {
55 message: String,
56 retry_after: Option<std::time::Duration>,
57 },
58
59 #[error("Operation timed out: {operation}")]
61 TimeoutError { operation: String },
62
63 #[error("Connection error: {message}")]
65 ConnectionError { message: String },
66
67 #[error("Serialization error: {source}")]
69 SerializationError {
70 #[source]
71 source: Box<dyn std::error::Error + Send + Sync>,
72 },
73
74 #[error("Invalid URL: {source}")]
76 InvalidUrl {
77 #[source]
78 source: url::ParseError,
79 },
80
81 #[error("I/O error: {source}")]
83 IoError {
84 #[source]
85 source: std::io::Error,
86 },
87
88 #[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 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 pub fn websocket_error<S: Into<String>>(message: S) -> Self {
171 Self::WebSocketError {
172 message: message.into(),
173 }
174 }
175
176 pub fn configuration_error<S: Into<String>>(message: S) -> Self {
178 Self::ConfigurationError {
179 message: message.into(),
180 }
181 }
182
183 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 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 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 pub fn authentication_error<S: Into<String>>(message: S) -> Self {
210 Self::AuthenticationError {
211 message: message.into(),
212 }
213 }
214
215 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 pub fn timeout_error<S: Into<String>>(operation: S) -> Self {
228 Self::TimeoutError {
229 operation: operation.into(),
230 }
231 }
232
233 pub fn connection_error<S: Into<String>>(message: S) -> Self {
235 Self::ConnectionError {
236 message: message.into(),
237 }
238 }
239
240 pub fn other<S: Into<String>>(message: S) -> Self {
242 Self::Other {
243 message: message.into(),
244 }
245 }
246
247 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 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 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 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#[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 pub fn new() -> Self {
330 Self::default()
331 }
332
333 pub fn message<S: Into<String>>(mut self, message: S) -> Self {
335 self.message = Some(message.into());
336 self
337 }
338
339 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 pub fn retryable(mut self, retryable: bool) -> Self {
347 self.retryable = retryable;
348 self
349 }
350
351 pub fn status(mut self, status: u16) -> Self {
353 self.status = Some(status);
354 self
355 }
356
357 pub fn error_code<S: Into<String>>(mut self, code: S) -> Self {
359 self.error_code = Some(code.into());
360 self
361 }
362
363 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}