1use serde::{Deserialize, Serialize};
36use std::fmt;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ErrorType {
44 Unknown,
46 InvalidArgument,
48 CannotConnect,
50 Disconnected,
52 ConnectionTimeout,
54 ResponseTimeout,
56 Cancelled,
58 ResourceExhausted,
60 Unavailable,
62 Backend(BackendError),
64}
65
66impl fmt::Display for ErrorType {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 ErrorType::Unknown => write!(f, "Unknown"),
70 ErrorType::InvalidArgument => write!(f, "InvalidArgument"),
71 ErrorType::CannotConnect => write!(f, "CannotConnect"),
72 ErrorType::Disconnected => write!(f, "Disconnected"),
73 ErrorType::ConnectionTimeout => write!(f, "ConnectionTimeout"),
74 ErrorType::ResponseTimeout => write!(f, "ResponseTimeout"),
75 ErrorType::Cancelled => write!(f, "Cancelled"),
76 ErrorType::ResourceExhausted => write!(f, "ResourceExhausted"),
77 ErrorType::Unavailable => write!(f, "Unavailable"),
78 ErrorType::Backend(sub) => write!(f, "Backend{sub}"),
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89pub enum BackendError {
90 Unknown,
92 InvalidArgument,
94 CannotConnect,
96 Disconnected,
98 ConnectionTimeout,
100 ResponseTimeout,
102 Cancelled,
104 EngineShutdown,
106 StreamIncomplete,
108}
109
110impl fmt::Display for BackendError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 BackendError::Unknown => write!(f, "Unknown"),
114 BackendError::InvalidArgument => write!(f, "InvalidArgument"),
115 BackendError::CannotConnect => write!(f, "CannotConnect"),
116 BackendError::Disconnected => write!(f, "Disconnected"),
117 BackendError::ConnectionTimeout => write!(f, "ConnectionTimeout"),
118 BackendError::ResponseTimeout => write!(f, "ResponseTimeout"),
119 BackendError::Cancelled => write!(f, "Cancelled"),
120 BackendError::EngineShutdown => write!(f, "EngineShutdown"),
121 BackendError::StreamIncomplete => write!(f, "StreamIncomplete"),
122 }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct DynamoError {
149 error_type: ErrorType,
150 message: String,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 caused_by: Option<Box<DynamoError>>,
153}
154
155impl DynamoError {
156 pub fn builder() -> DynamoErrorBuilder {
158 DynamoErrorBuilder::default()
159 }
160
161 pub fn msg(message: impl Into<String>) -> Self {
163 Self::builder().message(message).build()
164 }
165
166 pub fn error_type(&self) -> ErrorType {
168 self.error_type
169 }
170
171 pub fn message(&self) -> &str {
173 &self.message
174 }
175}
176
177impl fmt::Display for DynamoError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "{}: {}", self.error_type, self.message)
180 }
181}
182
183impl std::error::Error for DynamoError {
184 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
185 self.caused_by
186 .as_deref()
187 .map(|e| e as &(dyn std::error::Error + 'static))
188 }
189}
190
191impl<'a> From<&'a (dyn std::error::Error + 'static)> for DynamoError {
197 fn from(err: &'a (dyn std::error::Error + 'static)) -> Self {
198 if let Some(dynamo_err) = err.downcast_ref::<DynamoError>() {
199 return dynamo_err.clone();
200 }
201
202 Self {
203 error_type: ErrorType::Unknown,
204 message: err.to_string(),
205 caused_by: err.source().map(|s| Box::new(DynamoError::from(s))),
206 }
207 }
208}
209
210impl From<Box<dyn std::error::Error + 'static>> for DynamoError {
215 fn from(err: Box<dyn std::error::Error + 'static>) -> Self {
216 match err.downcast::<DynamoError>() {
217 Ok(dynamo_err) => *dynamo_err,
218 Err(err) => DynamoError::from(&*err as &(dyn std::error::Error + 'static)),
219 }
220 }
221}
222
223#[derive(Default)]
238pub struct DynamoErrorBuilder {
239 error_type: Option<ErrorType>,
240 message: Option<String>,
241 caused_by: Option<Box<DynamoError>>,
242}
243
244impl DynamoErrorBuilder {
245 pub fn error_type(mut self, error_type: ErrorType) -> Self {
247 self.error_type = Some(error_type);
248 self
249 }
250
251 pub fn message(mut self, message: impl Into<String>) -> Self {
253 self.message = Some(message.into());
254 self
255 }
256
257 pub fn cause(mut self, cause: impl std::error::Error + 'static) -> Self {
262 self.caused_by = Some(Box::new(DynamoError::from(
263 &cause as &(dyn std::error::Error + 'static),
264 )));
265 self
266 }
267
268 pub fn build(self) -> DynamoError {
272 DynamoError {
273 error_type: self.error_type.unwrap_or(ErrorType::Unknown),
274 message: self.message.unwrap_or_default(),
275 caused_by: self.caused_by,
276 }
277 }
278}
279
280pub fn match_error_chain(
292 err: &(dyn std::error::Error + 'static),
293 match_set: &[ErrorType],
294 exclude_set: &[ErrorType],
295) -> bool {
296 let mut found = false;
297 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
298
299 while let Some(e) = current {
300 if let Some(dynamo_err) = e.downcast_ref::<DynamoError>() {
301 if exclude_set.contains(&dynamo_err.error_type()) {
302 return false;
303 }
304 if match_set.contains(&dynamo_err.error_type()) {
305 found = true;
306 }
307 }
308 current = e.source();
309 }
310
311 found
312}
313
314#[cfg(test)]
319mod tests {
320 use super::*;
321 use std::error::Error;
322
323 const _: () = {
326 fn assert_stderror<T: std::error::Error>() {}
327 fn assert_send<T: Send>() {}
328 fn assert_sync<T: Sync>() {}
329 fn assert_static<T: 'static>() {}
330 fn assert_all() {
331 assert_stderror::<DynamoError>();
332 assert_send::<DynamoError>();
333 assert_sync::<DynamoError>();
334 assert_static::<DynamoError>();
335 }
336 };
337
338 #[test]
339 fn test_msg_constructor() {
340 let err = DynamoError::msg("something failed");
341 assert_eq!(err.error_type(), ErrorType::Unknown);
342 assert_eq!(err.message(), "something failed");
343 assert!(err.source().is_none());
344 }
345
346 #[test]
347 fn test_new_constructor_with_cause() {
348 let cause = std::io::Error::other("io error");
349 let err = DynamoError::builder()
350 .error_type(ErrorType::Unknown)
351 .message("operation failed")
352 .cause(cause)
353 .build();
354
355 assert_eq!(err.error_type(), ErrorType::Unknown);
356 assert_eq!(err.message(), "operation failed");
357 assert!(err.source().is_some());
358 }
359
360 #[test]
361 fn test_display_shows_only_current_error() {
362 let cause = std::io::Error::other("io error");
363 let err = DynamoError::builder()
364 .error_type(ErrorType::Unknown)
365 .message("operation failed")
366 .cause(cause)
367 .build();
368
369 assert_eq!(err.to_string(), "Unknown: operation failed");
371 }
372
373 #[test]
374 fn test_source_chain() {
375 let cause = std::io::Error::other("io error");
376 let err = DynamoError::builder()
377 .error_type(ErrorType::Unknown)
378 .message("operation failed")
379 .cause(cause)
380 .build();
381
382 let source = err.source().unwrap();
384 assert!(source.to_string().contains("io error"));
385 }
386
387 #[test]
388 fn test_from_boxed_std_error() {
389 let std_err = std::io::Error::other("io error");
390 let boxed: Box<dyn std::error::Error> = Box::new(std_err);
391 let dynamo_err = DynamoError::from(boxed);
392
393 assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
394 assert_eq!(dynamo_err.message(), "io error");
395 }
396
397 #[test]
398 fn test_from_boxed_takes_ownership_of_dynamo_error() {
399 let inner = DynamoError::msg("original");
400 let boxed: Box<dyn std::error::Error> = Box::new(inner);
401 let dynamo_err = DynamoError::from(boxed);
402
403 assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
405 assert_eq!(dynamo_err.message(), "original");
406 }
407
408 #[test]
409 fn test_from_boxed_with_source_chain() {
410 #[derive(Debug)]
411 struct OuterError {
412 source: std::io::Error,
413 }
414
415 impl fmt::Display for OuterError {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 write!(f, "outer error occurred")
418 }
419 }
420
421 impl std::error::Error for OuterError {
422 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
423 Some(&self.source)
424 }
425 }
426
427 let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
428 let outer = OuterError { source: inner };
429 let boxed: Box<dyn std::error::Error> = Box::new(outer);
430 let dynamo_err = DynamoError::from(boxed);
431
432 assert_eq!(dynamo_err.message(), "outer error occurred");
433 assert!(dynamo_err.source().is_some());
434
435 let cause = dynamo_err.source().unwrap();
436 assert!(cause.to_string().contains("file not found"));
437 }
438
439 #[test]
440 fn test_serialization_roundtrip() {
441 let cause = DynamoError::msg("inner cause");
442 let err = DynamoError::builder()
443 .error_type(ErrorType::Unknown)
444 .message("outer error")
445 .cause(cause)
446 .build();
447
448 let json = serde_json::to_string(&err).unwrap();
449 let deserialized: DynamoError = serde_json::from_str(&json).unwrap();
450
451 assert_eq!(deserialized.error_type(), ErrorType::Unknown);
452 assert_eq!(deserialized.message(), "outer error");
453 assert!(deserialized.source().is_some());
454
455 let cause = deserialized
456 .source()
457 .unwrap()
458 .downcast_ref::<DynamoError>()
459 .unwrap();
460 assert_eq!(cause.message(), "inner cause");
461 }
462
463 #[test]
464 fn test_error_type_display() {
465 assert_eq!(ErrorType::Unknown.to_string(), "Unknown");
466 assert_eq!(ErrorType::InvalidArgument.to_string(), "InvalidArgument");
467 assert_eq!(ErrorType::CannotConnect.to_string(), "CannotConnect");
468 assert_eq!(ErrorType::Disconnected.to_string(), "Disconnected");
469 assert_eq!(
470 ErrorType::ConnectionTimeout.to_string(),
471 "ConnectionTimeout"
472 );
473 assert_eq!(ErrorType::ResponseTimeout.to_string(), "ResponseTimeout");
474 assert_eq!(ErrorType::Cancelled.to_string(), "Cancelled");
475 assert_eq!(
476 ErrorType::ResourceExhausted.to_string(),
477 "ResourceExhausted"
478 );
479 assert_eq!(ErrorType::Unavailable.to_string(), "Unavailable");
480 assert_eq!(
481 ErrorType::Backend(BackendError::Unknown).to_string(),
482 "BackendUnknown"
483 );
484 assert_eq!(
485 ErrorType::Backend(BackendError::InvalidArgument).to_string(),
486 "BackendInvalidArgument"
487 );
488 assert_eq!(
489 ErrorType::Backend(BackendError::CannotConnect).to_string(),
490 "BackendCannotConnect"
491 );
492 assert_eq!(
493 ErrorType::Backend(BackendError::Disconnected).to_string(),
494 "BackendDisconnected"
495 );
496 assert_eq!(
497 ErrorType::Backend(BackendError::ConnectionTimeout).to_string(),
498 "BackendConnectionTimeout"
499 );
500 assert_eq!(
501 ErrorType::Backend(BackendError::Cancelled).to_string(),
502 "BackendCancelled"
503 );
504 assert_eq!(
505 ErrorType::Backend(BackendError::EngineShutdown).to_string(),
506 "BackendEngineShutdown"
507 );
508 assert_eq!(
509 ErrorType::Backend(BackendError::StreamIncomplete).to_string(),
510 "BackendStreamIncomplete"
511 );
512 assert_eq!(
513 ErrorType::Backend(BackendError::ResponseTimeout).to_string(),
514 "BackendResponseTimeout"
515 );
516 }
517}