use eventide_domain::error::{DomainError, ErrorCode, ErrorKind};
use std::error::Error as StdError;
use std::fmt;
pub struct AppError {
kind: ErrorKind,
code: &'static str,
message: Box<str>,
source: Option<Source>,
}
enum Source {
Domain(DomainError),
Other(Box<dyn StdError + Send + Sync>),
}
impl AppError {
fn new(kind: ErrorKind, code: &'static str, message: impl Into<Box<str>>) -> Self {
Self {
kind,
code,
message: message.into(),
source: None,
}
}
#[must_use]
pub fn validation(msg: impl Into<Box<str>>) -> Self {
Self::new(ErrorKind::InvalidValue, "VALIDATION_ERROR", msg)
}
#[must_use]
pub fn unauthorized(msg: impl Into<Box<str>>) -> Self {
Self::new(ErrorKind::Unauthorized, "UNAUTHORIZED", msg)
}
#[must_use]
pub fn handler_not_found(handler_name: &str) -> Self {
Self::new(
ErrorKind::Internal,
"HANDLER_NOT_FOUND",
format!("handler not found: {handler_name}"),
)
}
#[must_use]
pub fn aggregate_not_found(aggregate_type: &str, aggregate_id: &str) -> Self {
Self::new(
ErrorKind::NotFound,
"AGGREGATE_NOT_FOUND",
format!("{aggregate_type} not found: {aggregate_id}"),
)
}
#[must_use]
pub fn handler_already_registered(handler_name: &str) -> Self {
Self::new(
ErrorKind::Internal,
"HANDLER_ALREADY_REGISTERED",
format!("handler already registered: {handler_name}"),
)
}
#[must_use]
pub fn type_mismatch(expected: &str, found: &str) -> Self {
Self::new(
ErrorKind::Internal,
"TYPE_MISMATCH",
format!("type mismatch: expected={expected}, found={found}"),
)
}
#[must_use]
pub fn internal(msg: impl Into<Box<str>>) -> Self {
Self::new(ErrorKind::Internal, "INTERNAL_ERROR", msg)
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.kind
}
#[must_use]
pub fn domain_error(&self) -> Option<&DomainError> {
match &self.source {
Some(Source::Domain(e)) => Some(e),
_ => None,
}
}
#[must_use]
pub fn get_ref(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
match &self.source {
Some(Source::Domain(e)) => Some(e),
Some(Source::Other(e)) => Some(e.as_ref()),
None => None,
}
}
#[must_use]
pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
match &self.source {
Some(Source::Domain(e)) => e.downcast_ref(),
Some(Source::Other(e)) => e.downcast_ref(),
None => None,
}
}
#[must_use]
pub fn wrap<E: StdError + Send + Sync + 'static>(
kind: ErrorKind,
code: &'static str,
error: E,
) -> Self {
Self {
kind,
code,
message: error.to_string().into(),
source: Some(Source::Other(Box::new(error))),
}
}
#[must_use]
pub fn matches(&self, kind: ErrorKind, code: &str) -> bool {
self.kind == kind && self.code == code
}
}
impl ErrorCode for AppError {
fn kind(&self) -> ErrorKind {
self.kind
}
fn code(&self) -> &str {
self.code
}
}
impl fmt::Debug for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppError")
.field("kind", &self.kind)
.field("code", &self.code)
.field("message", &self.message)
.field("source", &self.source.as_ref().map(|_| "..."))
.finish()
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for AppError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.source {
Some(Source::Domain(e)) => Some(e),
Some(Source::Other(e)) => Some(e.as_ref()),
None => None,
}
}
}
impl From<DomainError> for AppError {
fn from(e: DomainError) -> Self {
let code = e.static_code();
Self {
kind: e.kind(),
code,
message: e.to_string().into(),
source: Some(Source::Domain(e)),
}
}
}
pub type AppResult<T> = Result<T, AppError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_error_convenience_methods() {
let err = AppError::validation("test");
assert_eq!(err.kind(), ErrorKind::InvalidValue);
assert_eq!(err.code(), "VALIDATION_ERROR");
assert_eq!(err.to_string(), "test");
let err = AppError::unauthorized("no token");
assert_eq!(err.kind(), ErrorKind::Unauthorized);
assert_eq!(err.code(), "UNAUTHORIZED");
let err = AppError::handler_not_found("TestHandler");
assert_eq!(err.kind(), ErrorKind::Internal);
assert_eq!(err.code(), "HANDLER_NOT_FOUND");
}
#[test]
fn test_from_domain_error() {
let domain_err = DomainError::not_found("user 123");
let app_err: AppError = domain_err.into();
assert_eq!(app_err.kind(), ErrorKind::NotFound);
assert!(app_err.domain_error().is_some());
}
#[test]
fn test_app_error_implements_error_code() {
let err = AppError::validation("invalid input");
assert_eq!(err.kind(), ErrorKind::InvalidValue);
assert_eq!(err.code(), "VALIDATION_ERROR");
assert_eq!(err.http_status(), 400);
assert!(!err.is_retryable());
}
#[test]
fn test_aggregate_not_found() {
let err = AppError::aggregate_not_found("User", "user-123");
assert_eq!(err.kind(), ErrorKind::NotFound);
assert_eq!(err.code(), "AGGREGATE_NOT_FOUND");
assert_eq!(err.http_status(), 404);
assert!(err.to_string().contains("User"));
assert!(err.to_string().contains("user-123"));
}
#[test]
fn test_from_domain_error_preserves_custom_code() {
let domain_err = DomainError::not_found("user 123").with_code("USER_NOT_FOUND");
let app_err: AppError = domain_err.into();
assert_eq!(app_err.kind(), ErrorKind::NotFound);
assert_eq!(app_err.code(), "USER_NOT_FOUND"); }
#[test]
fn test_wrap_preserves_error() {
use std::io;
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let err = AppError::wrap(ErrorKind::Internal, "IO_ERROR", io_err);
assert_eq!(err.code(), "IO_ERROR");
assert!(err.downcast_ref::<io::Error>().is_some());
assert!(err.get_ref().is_some());
}
#[test]
fn test_downcast_ref_through_domain_error() {
use std::io;
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let domain_err = DomainError::custom(ErrorKind::Internal, io_err);
let app_err: AppError = domain_err.into();
assert!(app_err.downcast_ref::<io::Error>().is_some());
}
#[test]
fn test_matches() {
let err = AppError::validation("invalid email");
assert!(err.matches(ErrorKind::InvalidValue, "VALIDATION_ERROR"));
assert!(!err.matches(ErrorKind::NotFound, "VALIDATION_ERROR"));
assert!(!err.matches(ErrorKind::InvalidValue, "WRONG_CODE"));
let err = AppError::handler_not_found("TestHandler");
assert!(err.matches(ErrorKind::Internal, "HANDLER_NOT_FOUND"));
}
}