use std::fmt;
use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Terminal error: {0}")]
Terminal(String),
#[error("Event error: {0}")]
Event(String),
#[error("Command error: {0}")]
Command(String),
#[error("Component error: {0}")]
Component(String),
#[error("Model error: {0}")]
Model(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Custom error: {0}")]
Custom(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Async runtime error: {0}")]
AsyncRuntime(String),
#[error("Resource limit exceeded: {0}")]
ResourceLimit(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Parsing error: {0}")]
Parse(String),
#[error("Operation timed out: {0}")]
Timeout(String),
#[error("Channel send error: {0}")]
ChannelSend(String),
#[error("Channel receive error")]
ChannelRecv(#[from] std::sync::mpsc::RecvError),
}
impl<T> From<std::sync::mpsc::SendError<T>> for Error {
fn from(err: std::sync::mpsc::SendError<T>) -> Self {
Error::ChannelSend(format!("Failed to send message: {err}"))
}
}
impl From<tokio::time::error::Elapsed> for Error {
fn from(err: tokio::time::error::Elapsed) -> Self {
Error::Timeout(format!("Operation timed out: {err}"))
}
}
pub trait ErrorContext<T> {
fn context(self, msg: &str) -> Result<T>;
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String;
}
impl<T, E> ErrorContext<T> for std::result::Result<T, E>
where
E: Into<Error>,
{
fn context(self, msg: &str) -> Result<T> {
self.map_err(|err| {
let base_error = err.into();
Error::Custom(Box::new(ContextError {
context: msg.to_string(),
source: base_error,
}))
})
}
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String,
{
self.map_err(|err| {
let base_error = err.into();
Error::Custom(Box::new(ContextError {
context: f(),
source: base_error,
}))
})
}
}
#[derive(Debug)]
struct ContextError {
context: String,
source: Error,
}
impl fmt::Display for ContextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.context, self.source)
}
}
impl std::error::Error for ContextError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
pub trait ErrorHandler {
fn handle_error(&mut self, error: Error) -> bool;
}
pub struct DefaultErrorHandler;
impl ErrorHandler for DefaultErrorHandler {
fn handle_error(&mut self, error: Error) -> bool {
eprintln!("Error: {error}");
let mut current_error: &dyn std::error::Error = &error;
while let Some(source) = current_error.source() {
eprintln!(" Caused by: {source}");
current_error = source;
}
false }
}
pub fn set_panic_handler() {
std::panic::set_hook(Box::new(|panic_info| {
let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
let location = if let Some(location) = panic_info.location() {
format!(
" at {}:{}:{}",
location.file(),
location.line(),
location.column()
)
} else {
String::new()
};
eprintln!("Panic occurred: {msg}{location}");
}));
}
#[macro_export]
macro_rules! bail {
($msg:literal $(,)?) => {
return Err($crate::error::Error::Custom(
format!($msg).into()
))
};
($err:expr $(,)?) => {
return Err($crate::error::Error::Custom(
format!("{}", $err).into()
))
};
($fmt:expr, $($arg:tt)*) => {
return Err($crate::error::Error::Custom(
format!($fmt, $($arg)*).into()
))
};
}
#[macro_export]
macro_rules! ensure {
($cond:expr, $msg:literal $(,)?) => {
if !$cond {
$crate::bail!($msg);
}
};
($cond:expr, $err:expr $(,)?) => {
if !$cond {
$crate::bail!($err);
}
};
($cond:expr, $fmt:expr, $($arg:tt)*) => {
if !$cond {
$crate::bail!($fmt, $($arg)*);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[test]
fn test_error_display() {
let err = Error::Terminal("Failed to initialize".to_string());
assert_eq!(err.to_string(), "Terminal error: Failed to initialize");
let err = Error::Io(io::Error::new(io::ErrorKind::NotFound, "File not found"));
assert_eq!(err.to_string(), "I/O error: File not found");
}
#[test]
fn test_error_context() {
let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
let with_context = result.context("While initializing terminal");
assert!(with_context.is_err());
let err_str = with_context.unwrap_err().to_string();
assert!(err_str.contains("While initializing terminal"));
assert!(err_str.contains("Base error"));
}
#[test]
fn test_error_from_io() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "Access denied");
let err: Error = io_err.into();
match err {
Error::Io(_) => (),
_ => panic!("Expected Io error variant"),
}
}
#[test]
fn test_bail_macro() {
fn test_fn() -> Result<()> {
bail!("Test error");
}
assert!(test_fn().is_err());
assert_eq!(
test_fn().unwrap_err().to_string(),
"Custom error: Test error"
);
}
#[test]
fn test_ensure_macro() {
fn test_fn(value: i32) -> Result<i32> {
ensure!(value > 0, "Value must be positive");
Ok(value)
}
assert!(test_fn(5).is_ok());
assert!(test_fn(-1).is_err());
}
#[test]
fn test_all_error_variants() {
let errors = vec![
Error::Terminal("terminal error".to_string()),
Error::Event("event error".to_string()),
Error::Command("command error".to_string()),
Error::Component("component error".to_string()),
Error::Model("model error".to_string()),
Error::Config("config error".to_string()),
];
for error in errors {
let display_str = error.to_string();
assert!(!display_str.is_empty());
assert!(error.source().is_none());
}
}
#[test]
fn test_error_source_chain() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
let err = Error::Io(io_err);
assert!(StdError::source(&err).is_some());
let source = err.source().unwrap();
assert_eq!(source.to_string(), "File not found");
}
#[test]
fn test_custom_error() {
let custom_err = Box::new(io::Error::other("custom"));
let err = Error::Custom(custom_err);
assert!(StdError::source(&err).is_some());
assert!(err.to_string().contains("Custom error"));
}
#[test]
fn test_channel_error_conversions() {
let recv_err = std::sync::mpsc::RecvError;
let err: Error = recv_err.into();
match err {
Error::ChannelRecv(_) => (),
_ => panic!("Expected ChannelRecv error variant"),
}
let (tx, _rx) = std::sync::mpsc::channel::<i32>();
drop(_rx);
let send_result = tx.send(42);
if let Err(send_err) = send_result {
let err: Error = send_err.into();
match err {
Error::ChannelSend(_) => (),
_ => panic!("Expected ChannelSend error variant"),
}
}
}
#[test]
fn test_with_context() {
let result: Result<()> = Err(Error::Terminal("Base error".to_string()));
let with_context = result.with_context(|| "Dynamic context".to_string());
assert!(with_context.is_err());
let err_str = with_context.unwrap_err().to_string();
assert!(err_str.contains("Dynamic context"));
}
#[test]
fn test_default_error_handler() {
let mut handler = DefaultErrorHandler;
let error = Error::Terminal("test error".to_string());
assert!(!handler.handle_error(error));
}
#[test]
fn test_context_error() {
let base_error = Error::Terminal("base".to_string());
let context_error = ContextError {
context: "context".to_string(),
source: base_error,
};
let display_str = context_error.to_string();
assert!(display_str.contains("context"));
assert!(display_str.contains("base"));
assert!(StdError::source(&context_error).is_some());
}
#[test]
fn test_bail_macro_with_format() {
fn test_fn(value: i32) -> Result<()> {
bail!("Value {} is invalid", value);
}
let err = test_fn(42).unwrap_err();
assert!(err.to_string().contains("Value 42 is invalid"));
}
#[test]
fn test_ensure_macro_with_format() {
fn test_fn(value: i32, min: i32) -> Result<i32> {
ensure!(value >= min, "Value {} must be >= {}", value, min);
Ok(value)
}
assert!(test_fn(10, 5).is_ok());
let err = test_fn(3, 5).unwrap_err();
assert!(err.to_string().contains("Value 3 must be >= 5"));
}
#[test]
fn test_panic_handler() {
set_panic_handler();
let _ = std::panic::take_hook();
}
#[test]
fn test_new_error_variants() {
let errors = vec![
Error::AsyncRuntime("async error".to_string()),
Error::ResourceLimit("limit exceeded".to_string()),
Error::Validation("invalid input".to_string()),
Error::Parse("parse failed".to_string()),
Error::Timeout("timed out".to_string()),
Error::ChannelSend("send failed".to_string()),
];
for error in errors {
let display_str = error.to_string();
assert!(!display_str.is_empty());
assert!(error.source().is_none());
}
}
#[test]
fn test_timeout_error_conversion() {
let err = Error::Timeout("Operation timed out".to_string());
assert!(err.to_string().contains("timed out"));
}
#[test]
fn test_error_display_messages() {
assert_eq!(
Error::Io(io::Error::new(io::ErrorKind::NotFound, "file")).to_string(),
"I/O error: file"
);
assert_eq!(
Error::Terminal("term".to_string()).to_string(),
"Terminal error: term"
);
assert_eq!(
Error::Event("evt".to_string()).to_string(),
"Event error: evt"
);
assert_eq!(
Error::Command("cmd".to_string()).to_string(),
"Command error: cmd"
);
assert_eq!(
Error::Component("comp".to_string()).to_string(),
"Component error: comp"
);
assert_eq!(
Error::Model("model".to_string()).to_string(),
"Model error: model"
);
assert_eq!(
Error::Config("cfg".to_string()).to_string(),
"Configuration error: cfg"
);
assert_eq!(
Error::AsyncRuntime("async".to_string()).to_string(),
"Async runtime error: async"
);
assert_eq!(
Error::ResourceLimit("limit".to_string()).to_string(),
"Resource limit exceeded: limit"
);
assert_eq!(
Error::Validation("valid".to_string()).to_string(),
"Validation error: valid"
);
assert_eq!(
Error::Parse("parse".to_string()).to_string(),
"Parsing error: parse"
);
assert_eq!(
Error::Timeout("timeout".to_string()).to_string(),
"Operation timed out: timeout"
);
assert_eq!(
Error::ChannelSend("send".to_string()).to_string(),
"Channel send error: send"
);
}
}