1use std::time::Duration;
2
3pub type Result<T> = std::result::Result<T, Error>;
5
6#[derive(Debug, thiserror::Error)]
8pub enum Error {
9 #[error("authentication error {code} ({error_type}): {message}")]
11 Authentication {
12 code: u16,
14 message: String,
16 error_type: String,
18 details: String,
20 is_transient: bool,
22 http_status_code: u16,
24 error_subcode: u16,
26 },
27
28 #[error("rate limit error {code}: {message}")]
30 RateLimit {
31 code: u16,
33 message: String,
35 error_type: String,
37 details: String,
39 retry_after: Duration,
41 is_transient: bool,
43 http_status_code: u16,
45 error_subcode: u16,
47 },
48
49 #[error("validation error {code}: {message}")]
51 Validation {
52 code: u16,
54 message: String,
56 error_type: String,
58 details: String,
60 field: String,
62 is_transient: bool,
64 http_status_code: u16,
66 error_subcode: u16,
68 },
69
70 #[error("network error: {message}")]
72 Network {
73 code: u16,
75 message: String,
77 error_type: String,
79 details: String,
81 temporary: bool,
83 is_transient: bool,
85 http_status_code: u16,
87 error_subcode: u16,
89 #[source]
91 cause: Option<reqwest::Error>,
92 },
93
94 #[error("API error {code}: {message}")]
96 Api {
97 code: u16,
99 message: String,
101 error_type: String,
103 details: String,
105 request_id: String,
107 is_transient: bool,
109 http_status_code: u16,
111 error_subcode: u16,
113 },
114
115 #[error("HTTP error: {0}")]
117 Http(#[from] reqwest::Error),
118
119 #[error("JSON error: {0}")]
121 Json(#[from] serde_json::Error),
122}
123
124pub fn new_authentication_error(code: u16, message: &str, details: &str) -> Error {
130 Error::Authentication {
131 code,
132 message: message.to_owned(),
133 error_type: "authentication_error".to_owned(),
134 details: details.to_owned(),
135 is_transient: false,
136 http_status_code: 0,
137 error_subcode: 0,
138 }
139}
140
141pub fn new_rate_limit_error(
143 code: u16,
144 message: &str,
145 details: &str,
146 retry_after: Duration,
147) -> Error {
148 Error::RateLimit {
149 code,
150 message: message.to_owned(),
151 error_type: "rate_limit_error".to_owned(),
152 details: details.to_owned(),
153 retry_after,
154 is_transient: false,
155 http_status_code: 0,
156 error_subcode: 0,
157 }
158}
159
160pub fn new_validation_error(code: u16, message: &str, details: &str, field: &str) -> Error {
162 Error::Validation {
163 code,
164 message: message.to_owned(),
165 error_type: "validation_error".to_owned(),
166 details: details.to_owned(),
167 field: field.to_owned(),
168 is_transient: false,
169 http_status_code: 0,
170 error_subcode: 0,
171 }
172}
173
174pub fn new_network_error(code: u16, message: &str, details: &str, temporary: bool) -> Error {
176 new_network_error_with_cause(code, message, details, temporary, None)
177}
178
179pub fn new_network_error_with_cause(
181 code: u16,
182 message: &str,
183 details: &str,
184 temporary: bool,
185 cause: Option<reqwest::Error>,
186) -> Error {
187 Error::Network {
188 code,
189 message: message.to_owned(),
190 error_type: "network_error".to_owned(),
191 details: details.to_owned(),
192 temporary,
193 is_transient: false,
194 http_status_code: 0,
195 error_subcode: 0,
196 cause,
197 }
198}
199
200pub fn new_api_error(code: u16, message: &str, details: &str, request_id: &str) -> Error {
202 Error::Api {
203 code,
204 message: message.to_owned(),
205 error_type: "api_error".to_owned(),
206 details: details.to_owned(),
207 request_id: request_id.to_owned(),
208 is_transient: false,
209 http_status_code: 0,
210 error_subcode: 0,
211 }
212}
213
214pub struct BaseFields<'a> {
220 pub code: u16,
222 pub message: &'a str,
224 pub error_type: &'a str,
226 pub details: &'a str,
228 pub is_transient: bool,
230 pub http_status_code: u16,
232 pub error_subcode: u16,
234}
235
236pub fn extract_base_fields(err: &Error) -> Option<BaseFields<'_>> {
239 match err {
240 Error::Authentication {
241 code,
242 message,
243 error_type,
244 details,
245 is_transient,
246 http_status_code,
247 error_subcode,
248 } => Some(BaseFields {
249 code: *code,
250 message,
251 error_type,
252 details,
253 is_transient: *is_transient,
254 http_status_code: *http_status_code,
255 error_subcode: *error_subcode,
256 }),
257 Error::RateLimit {
258 code,
259 message,
260 error_type,
261 details,
262 is_transient,
263 http_status_code,
264 error_subcode,
265 ..
266 } => Some(BaseFields {
267 code: *code,
268 message,
269 error_type,
270 details,
271 is_transient: *is_transient,
272 http_status_code: *http_status_code,
273 error_subcode: *error_subcode,
274 }),
275 Error::Validation {
276 code,
277 message,
278 error_type,
279 details,
280 is_transient,
281 http_status_code,
282 error_subcode,
283 ..
284 } => Some(BaseFields {
285 code: *code,
286 message,
287 error_type,
288 details,
289 is_transient: *is_transient,
290 http_status_code: *http_status_code,
291 error_subcode: *error_subcode,
292 }),
293 Error::Network {
294 code,
295 message,
296 error_type,
297 details,
298 is_transient,
299 http_status_code,
300 error_subcode,
301 ..
302 } => Some(BaseFields {
303 code: *code,
304 message,
305 error_type,
306 details,
307 is_transient: *is_transient,
308 http_status_code: *http_status_code,
309 error_subcode: *error_subcode,
310 }),
311 Error::Api {
312 code,
313 message,
314 error_type,
315 details,
316 is_transient,
317 http_status_code,
318 error_subcode,
319 ..
320 } => Some(BaseFields {
321 code: *code,
322 message,
323 error_type,
324 details,
325 is_transient: *is_transient,
326 http_status_code: *http_status_code,
327 error_subcode: *error_subcode,
328 }),
329 Error::Http(_) | Error::Json(_) => None,
330 }
331}
332
333pub fn set_error_metadata(
335 err: &mut Error,
336 is_transient: bool,
337 http_status_code: u16,
338 error_subcode: u16,
339) {
340 match err {
341 Error::Authentication {
342 is_transient: t,
343 http_status_code: h,
344 error_subcode: s,
345 ..
346 }
347 | Error::RateLimit {
348 is_transient: t,
349 http_status_code: h,
350 error_subcode: s,
351 ..
352 }
353 | Error::Validation {
354 is_transient: t,
355 http_status_code: h,
356 error_subcode: s,
357 ..
358 }
359 | Error::Network {
360 is_transient: t,
361 http_status_code: h,
362 error_subcode: s,
363 ..
364 }
365 | Error::Api {
366 is_transient: t,
367 http_status_code: h,
368 error_subcode: s,
369 ..
370 } => {
371 *t = is_transient;
372 *h = http_status_code;
373 *s = error_subcode;
374 }
375 Error::Http(_) | Error::Json(_) => {}
376 }
377}
378
379impl Error {
384 pub fn is_authentication(&self) -> bool {
386 matches!(self, Error::Authentication { .. })
387 }
388
389 pub fn is_rate_limit(&self) -> bool {
391 matches!(self, Error::RateLimit { .. })
392 }
393
394 pub fn is_validation(&self) -> bool {
396 matches!(self, Error::Validation { .. })
397 }
398
399 pub fn is_network(&self) -> bool {
401 matches!(self, Error::Network { .. })
402 }
403
404 pub fn is_api(&self) -> bool {
406 matches!(self, Error::Api { .. })
407 }
408
409 pub fn is_transient(&self) -> bool {
411 match self {
412 Error::Authentication { is_transient, .. }
413 | Error::RateLimit { is_transient, .. }
414 | Error::Validation { is_transient, .. }
415 | Error::Network { is_transient, .. }
416 | Error::Api { is_transient, .. } => *is_transient,
417 _ => false,
418 }
419 }
420
421 pub fn is_retryable(&self) -> bool {
423 match self {
424 Error::RateLimit { .. } => true,
425 Error::Network {
426 temporary,
427 is_transient,
428 ..
429 } => *temporary || *is_transient,
430 _ => self.is_transient(),
431 }
432 }
433}
434
435#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_new_authentication_error() {
445 let err = new_authentication_error(401, "Invalid token", "Token expired");
446 assert!(err.is_authentication());
447 assert!(!err.is_rate_limit());
448 assert!(!err.is_transient());
449 assert!(!err.is_retryable());
450 assert!(err.to_string().contains("401"));
451 assert!(err.to_string().contains("Invalid token"));
452 }
453
454 #[test]
455 fn test_new_rate_limit_error() {
456 let err = new_rate_limit_error(429, "Too many requests", "", Duration::from_secs(60));
457 assert!(err.is_rate_limit());
458 assert!(!err.is_authentication());
459 assert!(err.is_retryable());
460 }
461
462 #[test]
463 fn test_new_validation_error() {
464 let err = new_validation_error(400, "Bad input", "text too long", "text");
465 assert!(err.is_validation());
466 assert!(!err.is_retryable());
467 }
468
469 #[test]
470 fn test_new_network_error() {
471 let err = new_network_error(0, "Connection refused", "", true);
472 assert!(err.is_network());
473 assert!(err.is_retryable());
474 }
475
476 #[test]
477 fn test_new_network_error_not_temporary() {
478 let err = new_network_error(0, "DNS failure", "", false);
479 assert!(err.is_network());
480 assert!(!err.is_retryable());
481 }
482
483 #[test]
484 fn test_network_error_transient_is_retryable() {
485 let mut err = new_network_error(0, "Transient failure", "", false);
486 assert!(!err.is_retryable());
487 set_error_metadata(&mut err, true, 503, 0);
488 assert!(err.is_transient());
489 assert!(err.is_retryable());
490 }
491
492 #[test]
493 fn test_new_api_error() {
494 let err = new_api_error(500, "Internal error", "", "req-123");
495 assert!(err.is_api());
496 assert!(!err.is_retryable());
497 }
498
499 #[test]
500 fn test_set_error_metadata() {
501 let mut err = new_api_error(500, "Internal error", "", "");
502 assert!(!err.is_transient());
503 set_error_metadata(&mut err, true, 503, 42);
504 assert!(err.is_transient());
505 assert!(err.is_retryable());
506 let base = extract_base_fields(&err).unwrap();
507 assert_eq!(base.http_status_code, 503);
508 assert_eq!(base.error_subcode, 42);
509 }
510
511 #[test]
512 fn test_extract_base_fields_http() {
513 let err = Error::Json(serde_json::from_str::<String>("invalid").unwrap_err());
515 assert!(extract_base_fields(&err).is_none());
516 }
517
518 #[test]
519 fn test_is_helpers_exhaustive() {
520 let auth = new_authentication_error(401, "x", "");
521 assert!(auth.is_authentication());
522 assert!(!auth.is_rate_limit());
523 assert!(!auth.is_validation());
524 assert!(!auth.is_network());
525 assert!(!auth.is_api());
526
527 let rate = new_rate_limit_error(429, "x", "", Duration::from_secs(1));
528 assert!(!rate.is_authentication());
529 assert!(rate.is_rate_limit());
530
531 let val = new_validation_error(400, "x", "", "f");
532 assert!(val.is_validation());
533
534 let net = new_network_error(0, "x", "", false);
535 assert!(net.is_network());
536
537 let api = new_api_error(500, "x", "", "");
538 assert!(api.is_api());
539 }
540}