shdrlib 0.1.5

A three-tiered Vulkan shader compilation and rendering framework built in pure Rust
Documentation
//! Error context tracking for building error tracebacks
//!
//! This module provides utilities for tracking error propagation chains,
//! similar to anyhow's context but specialized for Vulkan operations.

use std::fmt;
use std::backtrace::Backtrace;

/// Location in source code where an error occurred
#[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)
    }
}

/// Context information for an error
#[derive(Debug)]
pub struct ErrorContext {
    /// Human-readable description of what operation was being performed
    pub operation: String,
    
    /// Source location where error was captured
    pub location: Option<SourceLocation>,
    
    /// Additional key-value context
    pub metadata: Vec<(String, String)>,
    
    /// Backtrace (captured automatically in debug builds)
    pub backtrace: Option<Backtrace>,
    
    /// Parent context (for error chains)
    pub parent: Option<Box<ErrorContext>>,
}

impl ErrorContext {
    /// Create a new error context
    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,
        }
    }

    /// Add source location information
    pub fn with_location(mut self, file: &'static str, line: u32, column: u32) -> Self {
        self.location = Some(SourceLocation { file, line, column });
        self
    }

    /// Add a metadata key-value pair
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.push((key.into(), value.into()));
        self
    }

    /// Wrap another error context (for error chains)
    pub fn wrap(mut self, parent: ErrorContext) -> Self {
        self.parent = Some(Box::new(parent));
        self
    }

    /// Get the full error chain as a formatted string
    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())
    }
}

/// Helper trait for adding context to Results
pub trait ResultExt<T, E> {
    /// Add context to an error
    fn context(self, operation: impl Into<String>) -> Result<T, ErrorWithContext<E>>;
    
    /// Add context with source location
    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),
        })
    }
}

/// Error wrapper that includes context information
#[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 for adding context with automatic file/line capture
///
/// Usage:
/// ```ignore
/// let result = some_operation().with_context!("creating buffer")?;
/// ```
#[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);
    }
}