use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorType {
Unknown,
InvalidArgument,
CannotConnect,
Disconnected,
ConnectionTimeout,
Backend(BackendError),
}
impl fmt::Display for ErrorType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorType::Unknown => write!(f, "Unknown"),
ErrorType::InvalidArgument => write!(f, "InvalidArgument"),
ErrorType::CannotConnect => write!(f, "CannotConnect"),
ErrorType::Disconnected => write!(f, "Disconnected"),
ErrorType::ConnectionTimeout => write!(f, "ConnectionTimeout"),
ErrorType::Backend(sub) => write!(f, "Backend.{sub}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackendError {
EngineShutdown,
}
impl fmt::Display for BackendError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BackendError::EngineShutdown => write!(f, "EngineShutdown"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamoError {
error_type: ErrorType,
message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
caused_by: Option<Box<DynamoError>>,
}
impl DynamoError {
pub fn builder() -> DynamoErrorBuilder {
DynamoErrorBuilder::default()
}
pub fn msg(message: impl Into<String>) -> Self {
Self::builder().message(message).build()
}
pub fn error_type(&self) -> ErrorType {
self.error_type
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for DynamoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.error_type, self.message)
}
}
impl std::error::Error for DynamoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.caused_by
.as_deref()
.map(|e| e as &(dyn std::error::Error + 'static))
}
}
impl<'a> From<&'a (dyn std::error::Error + 'static)> for DynamoError {
fn from(err: &'a (dyn std::error::Error + 'static)) -> Self {
if let Some(dynamo_err) = err.downcast_ref::<DynamoError>() {
return dynamo_err.clone();
}
Self {
error_type: ErrorType::Unknown,
message: err.to_string(),
caused_by: err.source().map(|s| Box::new(DynamoError::from(s))),
}
}
}
impl From<Box<dyn std::error::Error + 'static>> for DynamoError {
fn from(err: Box<dyn std::error::Error + 'static>) -> Self {
match err.downcast::<DynamoError>() {
Ok(dynamo_err) => *dynamo_err,
Err(err) => DynamoError::from(&*err as &(dyn std::error::Error + 'static)),
}
}
}
#[derive(Default)]
pub struct DynamoErrorBuilder {
error_type: Option<ErrorType>,
message: Option<String>,
caused_by: Option<Box<DynamoError>>,
}
impl DynamoErrorBuilder {
pub fn error_type(mut self, error_type: ErrorType) -> Self {
self.error_type = Some(error_type);
self
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn cause(mut self, cause: impl std::error::Error + 'static) -> Self {
self.caused_by = Some(Box::new(DynamoError::from(
&cause as &(dyn std::error::Error + 'static),
)));
self
}
pub fn build(self) -> DynamoError {
DynamoError {
error_type: self.error_type.unwrap_or(ErrorType::Unknown),
message: self.message.unwrap_or_default(),
caused_by: self.caused_by,
}
}
}
pub fn match_error_chain(
err: &(dyn std::error::Error + 'static),
match_set: &[ErrorType],
exclude_set: &[ErrorType],
) -> bool {
let mut found = false;
let mut current: Option<&(dyn std::error::Error + 'static)> = Some(err);
while let Some(e) = current {
if let Some(dynamo_err) = e.downcast_ref::<DynamoError>() {
if exclude_set.contains(&dynamo_err.error_type()) {
return false;
}
if match_set.contains(&dynamo_err.error_type()) {
found = true;
}
}
current = e.source();
}
found
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
const _: () = {
fn assert_stderror<T: std::error::Error>() {}
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_static<T: 'static>() {}
fn assert_all() {
assert_stderror::<DynamoError>();
assert_send::<DynamoError>();
assert_sync::<DynamoError>();
assert_static::<DynamoError>();
}
};
#[test]
fn test_msg_constructor() {
let err = DynamoError::msg("something failed");
assert_eq!(err.error_type(), ErrorType::Unknown);
assert_eq!(err.message(), "something failed");
assert!(err.source().is_none());
}
#[test]
fn test_new_constructor_with_cause() {
let cause = std::io::Error::other("io error");
let err = DynamoError::builder()
.error_type(ErrorType::Unknown)
.message("operation failed")
.cause(cause)
.build();
assert_eq!(err.error_type(), ErrorType::Unknown);
assert_eq!(err.message(), "operation failed");
assert!(err.source().is_some());
}
#[test]
fn test_display_shows_only_current_error() {
let cause = std::io::Error::other("io error");
let err = DynamoError::builder()
.error_type(ErrorType::Unknown)
.message("operation failed")
.cause(cause)
.build();
assert_eq!(err.to_string(), "Unknown: operation failed");
}
#[test]
fn test_source_chain() {
let cause = std::io::Error::other("io error");
let err = DynamoError::builder()
.error_type(ErrorType::Unknown)
.message("operation failed")
.cause(cause)
.build();
let source = err.source().unwrap();
assert!(source.to_string().contains("io error"));
}
#[test]
fn test_from_boxed_std_error() {
let std_err = std::io::Error::other("io error");
let boxed: Box<dyn std::error::Error> = Box::new(std_err);
let dynamo_err = DynamoError::from(boxed);
assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
assert_eq!(dynamo_err.message(), "io error");
}
#[test]
fn test_from_boxed_takes_ownership_of_dynamo_error() {
let inner = DynamoError::msg("original");
let boxed: Box<dyn std::error::Error> = Box::new(inner);
let dynamo_err = DynamoError::from(boxed);
assert_eq!(dynamo_err.error_type(), ErrorType::Unknown);
assert_eq!(dynamo_err.message(), "original");
}
#[test]
fn test_from_boxed_with_source_chain() {
#[derive(Debug)]
struct OuterError {
source: std::io::Error,
}
impl fmt::Display for OuterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "outer error occurred")
}
}
impl std::error::Error for OuterError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let outer = OuterError { source: inner };
let boxed: Box<dyn std::error::Error> = Box::new(outer);
let dynamo_err = DynamoError::from(boxed);
assert_eq!(dynamo_err.message(), "outer error occurred");
assert!(dynamo_err.source().is_some());
let cause = dynamo_err.source().unwrap();
assert!(cause.to_string().contains("file not found"));
}
#[test]
fn test_serialization_roundtrip() {
let cause = DynamoError::msg("inner cause");
let err = DynamoError::builder()
.error_type(ErrorType::Unknown)
.message("outer error")
.cause(cause)
.build();
let json = serde_json::to_string(&err).unwrap();
let deserialized: DynamoError = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.error_type(), ErrorType::Unknown);
assert_eq!(deserialized.message(), "outer error");
assert!(deserialized.source().is_some());
let cause = deserialized
.source()
.unwrap()
.downcast_ref::<DynamoError>()
.unwrap();
assert_eq!(cause.message(), "inner cause");
}
#[test]
fn test_error_type_display() {
assert_eq!(ErrorType::Unknown.to_string(), "Unknown");
}
}