use std::fmt;
use std::backtrace::Backtrace;
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[derive(Debug)]
pub struct ErrorContext {
pub operation: String,
pub location: Option<SourceLocation>,
pub metadata: Vec<(String, String)>,
pub backtrace: Option<Backtrace>,
pub parent: Option<Box<ErrorContext>>,
}
impl ErrorContext {
pub fn new(operation: impl Into<String>) -> Self {
Self {
operation: operation.into(),
location: None,
metadata: Vec::new(),
backtrace: if cfg!(debug_assertions) {
Some(Backtrace::capture())
} else {
None
},
parent: None,
}
}
pub fn with_location(mut self, file: &'static str, line: u32, column: u32) -> Self {
self.location = Some(SourceLocation { file, line, column });
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn wrap(mut self, parent: ErrorContext) -> Self {
self.parent = Some(Box::new(parent));
self
}
pub fn format_chain(&self) -> String {
let mut output = String::new();
self.format_chain_recursive(&mut output, 0);
output
}
fn format_chain_recursive(&self, output: &mut String, depth: usize) {
let indent = " ".repeat(depth);
output.push_str(&format!("{}❯ {}\n", indent, self.operation));
if let Some(loc) = &self.location {
output.push_str(&format!("{} at {}\n", indent, loc));
}
if !self.metadata.is_empty() {
for (key, value) in &self.metadata {
output.push_str(&format!("{} {}: {}\n", indent, key, value));
}
}
if let Some(parent) = &self.parent {
output.push('\n');
parent.format_chain_recursive(output, depth + 1);
}
}
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_chain())
}
}
pub trait ResultExt<T, E> {
fn context(self, operation: impl Into<String>) -> Result<T, ErrorWithContext<E>>;
fn context_at(
self,
operation: impl Into<String>,
file: &'static str,
line: u32,
column: u32,
) -> Result<T, ErrorWithContext<E>>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> {
fn context(self, operation: impl Into<String>) -> Result<T, ErrorWithContext<E>> {
self.map_err(|err| ErrorWithContext {
error: err,
context: ErrorContext::new(operation),
})
}
fn context_at(
self,
operation: impl Into<String>,
file: &'static str,
line: u32,
column: u32,
) -> Result<T, ErrorWithContext<E>> {
self.map_err(|err| ErrorWithContext {
error: err,
context: ErrorContext::new(operation).with_location(file, line, column),
})
}
}
#[derive(Debug)]
pub struct ErrorWithContext<E> {
pub error: E,
pub context: ErrorContext,
}
impl<E: fmt::Display> fmt::Display for ErrorWithContext<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n╔════════════════════════════════════════════════════════════╗")?;
writeln!(f, "║ ERROR TRACEBACK ║")?;
writeln!(f, "╠════════════════════════════════════════════════════════════╣\n")?;
write!(f, "{}", self.context)?;
writeln!(f, "\n╠════════════════════════════════════════════════════════════╣")?;
writeln!(f, "║ ROOT CAUSE ║")?;
writeln!(f, "╠════════════════════════════════════════════════════════════╣\n")?;
writeln!(f, "{}", self.error)?;
writeln!(f, "\n╚════════════════════════════════════════════════════════════╝")?;
Ok(())
}
}
impl<E: std::error::Error + 'static> std::error::Error for ErrorWithContext<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[macro_export]
macro_rules! with_context {
($result:expr, $msg:expr) => {
$crate::ex::helpers::error_context::ResultExt::context_at(
$result,
$msg,
file!(),
line!(),
column!(),
)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_chain() {
let ctx1 = ErrorContext::new("inner operation");
let ctx2 = ErrorContext::new("middle operation").wrap(ctx1);
let ctx3 = ErrorContext::new("outer operation").wrap(ctx2);
let chain = ctx3.format_chain();
assert!(chain.contains("outer operation"));
assert!(chain.contains("middle operation"));
assert!(chain.contains("inner operation"));
}
#[test]
fn test_metadata() {
let ctx = ErrorContext::new("test")
.with_metadata("buffer_size", "1024")
.with_metadata("usage", "VERTEX");
assert_eq!(ctx.metadata.len(), 2);
}
}