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