use std::backtrace::Backtrace;
use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::path::PathBuf;
#[cfg(feature = "serde")]
use serde::Serialize;
pub type Result<T> = std::result::Result<T, crate::error::AppError>;
pub trait ForgeError: std::error::Error + Send + Sync + 'static {
fn kind(&self) -> &'static str;
fn caption(&self) -> &'static str;
fn is_retryable(&self) -> bool {
false
}
fn is_fatal(&self) -> bool {
false
}
fn status_code(&self) -> u16 {
500
}
fn exit_code(&self) -> i32 {
1
}
fn user_message(&self) -> String {
self.to_string()
}
fn dev_message(&self) -> String {
format!("[{}] {}", self.kind(), self)
}
fn backtrace(&self) -> Option<&Backtrace> {
None
}
fn register(&self) {
crate::macros::call_error_hook(
self.caption(),
self.kind(),
self.is_fatal(),
self.is_retryable(),
);
}
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum AppError {
Config {
message: String,
retryable: bool,
fatal: bool,
status: u16,
},
Filesystem {
path: Option<PathBuf>,
#[cfg_attr(feature = "serde", serde(skip))]
source: io::Error,
retryable: bool,
fatal: bool,
status: u16,
},
Network {
endpoint: String,
#[cfg_attr(feature = "serde", serde(skip))]
source: Option<Box<dyn StdError + Send + Sync>>,
retryable: bool,
fatal: bool,
status: u16,
},
Other {
message: String,
retryable: bool,
fatal: bool,
status: u16,
},
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Config { message, .. } => write!(f, "⚙️ Configuration Error: {message}"),
Self::Filesystem { path, source, .. } => {
if let Some(p) = path {
write!(f, "💾 Filesystem Error at {p:?}: {source}")
} else {
write!(f, "💾 Filesystem Error: {source}")
}
}
Self::Network {
endpoint, source, ..
} => {
if let Some(src) = source {
write!(f, "🌐 Network Error on {endpoint}: {src}")
} else {
write!(f, "🌐 Network Error on {endpoint}")
}
}
Self::Other { message, .. } => write!(f, "🚨 Error: {message}"),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Filesystem { source, .. } => Some(source),
AppError::Network {
source: Some(src), ..
} => Some(src.as_ref()),
_ => None,
}
}
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Filesystem {
path: None,
source: e,
retryable: false,
fatal: true,
status: 500,
}
}
}
impl ForgeError for AppError {
fn kind(&self) -> &'static str {
match self {
Self::Config { .. } => "Config",
Self::Filesystem { .. } => "Filesystem",
Self::Network { .. } => "Network",
Self::Other { .. } => "Other",
}
}
fn caption(&self) -> &'static str {
match self {
Self::Config { .. } => "⚙️ Configuration",
Self::Filesystem { .. } => "💾 Filesystem",
Self::Network { .. } => "🌐 Network",
Self::Other { .. } => "🚨 Error",
}
}
fn is_retryable(&self) -> bool {
match self {
Self::Config { retryable, .. } => *retryable,
Self::Filesystem { retryable, .. } => *retryable,
Self::Network { retryable, .. } => *retryable,
Self::Other { retryable, .. } => *retryable,
}
}
fn is_fatal(&self) -> bool {
match self {
Self::Config { fatal, .. } => *fatal,
Self::Filesystem { fatal, .. } => *fatal,
Self::Network { fatal, .. } => *fatal,
Self::Other { fatal, .. } => *fatal,
}
}
fn status_code(&self) -> u16 {
match self {
Self::Config { status, .. } => *status,
Self::Filesystem { status, .. } => *status,
Self::Network { status, .. } => *status,
Self::Other { status, .. } => *status,
}
}
}
impl AppError {
pub fn config(message: impl Into<String>) -> Self {
let instance = Self::Config {
message: message.into(),
retryable: false,
fatal: false,
status: 500,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn filesystem(path: impl Into<String>, source: impl Into<Option<io::Error>>) -> Self {
let source = match source.into() {
Some(err) => err,
None => io::Error::other("File operation failed"),
};
let instance = Self::Filesystem {
path: Some(path.into().into()),
source,
retryable: false,
fatal: false,
status: 500,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn filesystem_with_source(path: impl Into<PathBuf>, source: io::Error) -> Self {
let instance = Self::Filesystem {
path: Some(path.into()),
source,
retryable: false,
fatal: false,
status: 500,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn network(
endpoint: impl Into<String>,
source: impl Into<Option<Box<dyn StdError + Send + Sync>>>,
) -> Self {
let source = source.into();
let instance = Self::Network {
endpoint: endpoint.into(),
source,
retryable: true,
fatal: false,
status: 503,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn network_with_source(
endpoint: impl Into<String>,
source: Option<Box<dyn StdError + Send + Sync>>,
) -> Self {
let instance = Self::Network {
endpoint: endpoint.into(),
source,
retryable: true,
fatal: false,
status: 503,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn other(message: impl Into<String>) -> Self {
let instance = Self::Other {
message: message.into(),
retryable: false,
fatal: false,
status: 500,
};
crate::macros::call_error_hook(
instance.caption(),
instance.kind(),
instance.is_fatal(),
instance.is_retryable(),
);
instance
}
pub fn with_retryable(mut self, retryable: bool) -> Self {
match &mut self {
Self::Config { retryable: r, .. } => *r = retryable,
Self::Filesystem { retryable: r, .. } => *r = retryable,
Self::Network { retryable: r, .. } => *r = retryable,
Self::Other { retryable: r, .. } => *r = retryable,
}
self
}
pub fn with_fatal(mut self, fatal: bool) -> Self {
match &mut self {
Self::Config { fatal: f, .. } => *f = fatal,
Self::Filesystem { fatal: f, .. } => *f = fatal,
Self::Network { fatal: f, .. } => *f = fatal,
Self::Other { fatal: f, .. } => *f = fatal,
}
self
}
pub fn with_status(mut self, status: u16) -> Self {
match &mut self {
Self::Config { status: s, .. } => *s = status,
Self::Filesystem { status: s, .. } => *s = status,
Self::Network { status: s, .. } => *s = status,
Self::Other { status: s, .. } => *s = status,
}
self
}
pub fn with_code(self, code: impl Into<String>) -> crate::registry::CodedError<Self> {
crate::registry::CodedError::new(self, code.into())
}
pub fn context<C: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static>(
self,
context: C,
) -> crate::context::ContextError<Self, C> {
crate::context::ContextError::new(self, context)
}
}