use std::collections::HashMap;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FoundationError {
#[error("Configuration error: {message}")]
Config { message: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Error: {0}")]
Other(#[from] anyhow::Error),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("HTTP error: {0}")]
Http(String),
#[error("URL parse error: {0}")]
UrlParse(String),
#[error("Authentication error: {0}")]
Authentication(String),
}
impl FoundationError {
pub fn with_context(self, context: &str) -> Self {
FoundationError::Other(anyhow::Error::new(self).context(context.to_string()))
}
}
pub type Result<T> = std::result::Result<T, FoundationError>;
pub fn io_error_with_path(
err: std::io::Error,
path: &std::path::Path,
action: &str,
) -> FoundationError {
FoundationError::Io(std::io::Error::new(
err.kind(),
format!("Failed to {action} {}: {err}", path.display()),
))
}
pub trait ErrorContext<T, E> {
fn context(self, context: impl fmt::Display) -> Result<T>;
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String;
}
impl<T, E> ErrorContext<T, E> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn context(self, context: impl fmt::Display) -> Result<T> {
self.map_err(|e| FoundationError::Other(anyhow::Error::new(e).context(context.to_string())))
}
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String,
{
self.map_err(|e| FoundationError::Other(anyhow::Error::new(e).context(f())))
}
}
#[derive(Debug)]
pub struct ErrorWithMetadata {
pub error: Box<dyn std::error::Error + Send + Sync>,
pub metadata: HashMap<String, String>,
}
impl ErrorWithMetadata {
pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Self {
error: Box::new(error),
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(|s| s.as_str())
}
}
impl fmt::Display for ErrorWithMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)?;
if !self.metadata.is_empty() {
write!(f, " [")?;
let mut first = true;
for (key, value) in &self.metadata {
if !first {
write!(f, ", ")?;
}
write!(f, "{key}={value}")?;
first = false;
}
write!(f, "]")?;
}
Ok(())
}
}
impl std::error::Error for ErrorWithMetadata {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.error.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_with_metadata() {
let err = ErrorWithMetadata::new(FoundationError::InvalidInput("test".to_string()))
.with_metadata("file", "config.json")
.with_metadata("line", "42");
assert_eq!(err.get_metadata("file"), Some("config.json"));
assert_eq!(err.get_metadata("line"), Some("42"));
assert_eq!(err.get_metadata("missing"), None);
let display = format!("{err}");
assert!(display.contains("Invalid input"));
assert!(display.contains("file=config.json"));
assert!(display.contains("line=42"));
}
#[test]
fn test_error_context() {
let result: std::result::Result<(), std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
let with_context = result.context("Reading configuration");
assert!(with_context.is_err());
let err_msg = format!("{}", with_context.unwrap_err());
assert!(err_msg.contains("Reading configuration"));
}
}