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 Backend(BackendError),
56}
57
58impl fmt::Display for ErrorType {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 ErrorType::Unknown => write!(f, "Unknown"),
62 ErrorType::InvalidArgument => write!(f, "InvalidArgument"),
63 ErrorType::CannotConnect => write!(f, "CannotConnect"),
64 ErrorType::Disconnected => write!(f, "Disconnected"),
65 ErrorType::ConnectionTimeout => write!(f, "ConnectionTimeout"),
66 ErrorType::Backend(sub) => write!(f, "Backend.{sub}"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub enum BackendError {
78 EngineShutdown,
80}
81
82impl fmt::Display for BackendError {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 BackendError::EngineShutdown => write!(f, "EngineShutdown"),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct DynamoError {
113 error_type: ErrorType,
114 message: String,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 caused_by: Option<Box<DynamoError>>,
117}
118
119impl DynamoError {
120 pub fn builder() -> DynamoErrorBuilder {
122 DynamoErrorBuilder::default()
123 }
124
125 pub fn msg(message: impl Into<String>) -> Self {
127 Self::builder().message(message).build()
128 }
129
130 pub fn error_type(&self) -> ErrorType {
132 self.error_type
133 }
134
135 pub fn message(&self) -> &str {
137 &self.message
138 }
139}
140
141impl fmt::Display for DynamoError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{}: {}", self.error_type, self.message)
144 }
145}
146
147impl std::error::Error for DynamoError {
148 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
149 self.caused_by
150 .as_deref()
151 .map(|e| e as &(dyn std::error::Error + 'static))
152 }
153}
154
155impl<'a> From<&'a (dyn std::error::Error + 'static)> for DynamoError {
161 fn from(err: &'a (dyn std::error::Error + 'static)) -> Self {
162 if let Some(dynamo_err) = err.downcast_ref::<DynamoError>() {
163 return dynamo_err.clone();
164 }
165
166 Self {
167 error_type: ErrorType::Unknown,
168 message: err.to_string(),
169 caused_by: err.source().map(|s| Box::new(DynamoError::from(s))),
170 }
171 }
172}
173
174impl From<Box<dyn std::error::Error + 'static>> for DynamoError {
179 fn from(err: Box<dyn std::error::Error + 'static>) -> Self {
180 match err.downcast::<DynamoError>() {
181 Ok(dynamo_err) => *dynamo_err,
182 Err(err) => DynamoError::from(&*err as &(dyn std::error::Error + 'static)),
183 }
184 }
185}
186
187#[derive(Default)]
202pub struct DynamoErrorBuilder {
203 error_type: Option<ErrorType>,
204 message: Option<String>,
205 caused_by: Option<Box<DynamoError>>,
206}
207
208impl DynamoErrorBuilder {
209 pub fn error_type(mut self, error_type: ErrorType) -> Self {
211 self.error_type = Some(error_type);
212 self
213 }
214
215 pub fn message(mut self, message: impl Into<String>) -> Self {
217 self.message = Some(message.into());
218 self
219 }
220
221 pub fn cause(mut self, cause: impl std::error::Error + 'static) -> Self {
226 self.caused_by = Some(Box::new(DynamoError::from(
227 &cause as &(dyn std::error::Error + 'static),
228 )));
229 self
230 }
231
232 pub fn build(self) -> DynamoError {
236 DynamoError {
237 error_type: self.error_type.unwrap_or(ErrorType::Unknown),
238 message: self.message.unwrap_or_default(),
239 caused_by: self.caused_by,
240 }
241 }
242}
243
244pub fn match_error_chain(
256 err: &(dyn std::error::Error + 'static),
257 match_set: &[ErrorType],
258 exclude_set: &[ErrorType],
259) -> bool {
260 let mut found = false;
261 let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
262
263 while let Some(e) = current {
264 if let Some(dynamo_err) = e.downcast_ref::<DynamoError>() {
265 if exclude_set.contains(&dynamo_err.error_type()) {
266 return false;
267 }
268 if match_set.contains(&dynamo_err.error_type()) {
269 found = true;
270 }
271 }
272 current = e.source();
273 }
274
275 found
276}
277
278#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::error::Error;
286
287 const _: () = {
290 fn assert_stderror<T: std::error::Error>() {}
291 fn assert_send<T: Send>() {}
292 fn assert_sync<T: Sync>() {}
293 fn assert_static<T: 'static>() {}
294 fn assert_all() {
295 assert_stderror::<DynamoError>();
296 assert_send::<DynamoError>();
297 assert_sync::<DynamoError>();
298 assert_static::<DynamoError>();
299 }
300 };
301
302 #[test]
303 fn test_msg_constructor() {
304 let err = DynamoError::msg("something failed");
305 assert_eq!(err.error_type(), ErrorType::Unknown);
306 assert_eq!(err.message(), "something failed");
307 assert!(err.source().is_none());
308 }
309
310 #[test]
311 fn test_new_constructor_with_cause() {
312 let cause = std::io::Error::other("io error");
313 let err = DynamoError::builder()
314 .error_type(ErrorType::Unknown)
315 .message("operation failed")
316 .cause(cause)
317 .build();
318
319 assert_eq!(err.error_type(), ErrorType::Unknown);
320 assert_eq!(err.message(), "operation failed");
321 assert!(err.source().is_some());
322 }
323
324 #[test]
325 fn test_display_shows_only_current_error() {
326 let cause = std::io::Error::other("io error");
327 let err = DynamoError::builder()
328 .error_type(ErrorType::Unknown)
329 .message("operation failed")
330 .cause(cause)
331 .build();
332
333 assert_eq!(err.to_string(), "Unknown: operation failed");
335 }
336
337 #[test]
338 fn test_source_chain() {
339 let cause = std::io::Error::other("io error");
340 let err = DynamoError::builder()
341 .error_type(ErrorType::Unknown)
342 .message("operation failed")
343 .cause(cause)
344 .build();
345
346 let source = err.source().unwrap();
348 assert!(source.to_string().contains("io error"));
349 }
350
351 #[test]
352 fn test_from_boxed_std_error() {
353 let std_err = std::io::Error::other("io error");
354 let boxed: Box<dyn std::error::Error> = Box::new(std_err);
355 let dynamo_err = DynamoError::from(boxed);
356
357 assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
358 assert_eq!(dynamo_err.message(), "io error");
359 }
360
361 #[test]
362 fn test_from_boxed_takes_ownership_of_dynamo_error() {
363 let inner = DynamoError::msg("original");
364 let boxed: Box<dyn std::error::Error> = Box::new(inner);
365 let dynamo_err = DynamoError::from(boxed);
366
367 assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
369 assert_eq!(dynamo_err.message(), "original");
370 }
371
372 #[test]
373 fn test_from_boxed_with_source_chain() {
374 #[derive(Debug)]
375 struct OuterError {
376 source: std::io::Error,
377 }
378
379 impl fmt::Display for OuterError {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 write!(f, "outer error occurred")
382 }
383 }
384
385 impl std::error::Error for OuterError {
386 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
387 Some(&self.source)
388 }
389 }
390
391 let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
392 let outer = OuterError { source: inner };
393 let boxed: Box<dyn std::error::Error> = Box::new(outer);
394 let dynamo_err = DynamoError::from(boxed);
395
396 assert_eq!(dynamo_err.message(), "outer error occurred");
397 assert!(dynamo_err.source().is_some());
398
399 let cause = dynamo_err.source().unwrap();
400 assert!(cause.to_string().contains("file not found"));
401 }
402
403 #[test]
404 fn test_serialization_roundtrip() {
405 let cause = DynamoError::msg("inner cause");
406 let err = DynamoError::builder()
407 .error_type(ErrorType::Unknown)
408 .message("outer error")
409 .cause(cause)
410 .build();
411
412 let json = serde_json::to_string(&err).unwrap();
413 let deserialized: DynamoError = serde_json::from_str(&json).unwrap();
414
415 assert_eq!(deserialized.error_type(), ErrorType::Unknown);
416 assert_eq!(deserialized.message(), "outer error");
417 assert!(deserialized.source().is_some());
418
419 let cause = deserialized
420 .source()
421 .unwrap()
422 .downcast_ref::<DynamoError>()
423 .unwrap();
424 assert_eq!(cause.message(), "inner cause");
425 }
426
427 #[test]
428 fn test_error_type_display() {
429 assert_eq!(ErrorType::Unknown.to_string(), "Unknown");
430 }
431}