errcraft 0.1.0

Beautiful, structured, and colorful error handling for Rust.
Documentation
//! Core error frame implementation.

#[cfg(not(feature = "std"))]
use alloc::{borrow::Cow, boxed::Box, string::String};
#[cfg(feature = "std")]
use std::borrow::Cow;

use crate::context::{ContextChain, ContextLayer, IntoContextValue};

/// The main error type for errcraft.
///
/// `ErrFrame` is a composable error type that supports rich context, nested chaining,
/// and beautiful rendering. It implements `std::error::Error` when the `std` feature is enabled.
///
/// # Examples
///
/// ```rust
/// use errcraft::ErrFrame;
///
/// let err = ErrFrame::new("Configuration error")
///     .context("file", "config.toml")
///     .context("line", 42);
/// ```
#[derive(Debug)]
pub struct ErrFrame {
    /// The primary error message
    pub(crate) message: Cow<'static, str>,
    /// The underlying cause of this error
    #[cfg(feature = "std")]
    pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
    #[cfg(not(feature = "std"))]
    pub(crate) source: Option<String>,
    /// Contextual information
    pub(crate) context: ContextChain,
    /// Captured backtrace
    #[cfg(all(feature = "std", feature = "backtrace"))]
    pub(crate) backtrace: Option<std::backtrace::Backtrace>,
}

impl ErrFrame {
    /// Creates a new error frame with the given message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use errcraft::ErrFrame;
    ///
    /// let err = ErrFrame::new("Something went wrong");
    /// ```
    pub fn new<M: Into<Cow<'static, str>>>(message: M) -> Self {
        Self {
            message: message.into(),
            source: None,
            context: ContextChain::new(),
            #[cfg(all(feature = "std", feature = "backtrace"))]
            backtrace: None,
        }
    }

    /// Attaches a source error to this frame.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use errcraft::ErrFrame;
    /// use std::io;
    ///
    /// let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
    /// let err = ErrFrame::new("Failed to read file")
    ///     .with_source(io_err);
    /// ```
    #[cfg(feature = "std")]
    pub fn with_source<E>(mut self, err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        self.source = Some(Box::new(err));
        self
    }

    /// Attaches a source error message (no_std variant).
    #[cfg(not(feature = "std"))]
    pub fn with_source<E: core::fmt::Display>(mut self, err: E) -> Self {
        use alloc::format;
        self.source = Some(format!("{}", err));
        self
    }

    /// Adds structured context with a key-value pair.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use errcraft::ErrFrame;
    ///
    /// let err = ErrFrame::new("Database query failed")
    ///     .context("table", "users")
    ///     .context("id", 123);
    /// ```
    pub fn context<K, V>(mut self, key: K, value: V) -> Self
    where
        K: Into<Cow<'static, str>>,
        V: IntoContextValue,
    {
        self.context.push(ContextLayer::with_key(key, value));
        self
    }

    /// Adds plain text context without a key.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use errcraft::ErrFrame;
    ///
    /// let err = ErrFrame::new("Upload failed")
    ///     .context_text("File size exceeded limit");
    /// ```
    pub fn context_text<T: Into<Cow<'static, str>>>(mut self, text: T) -> Self {
        self.context.push(ContextLayer::text(text));
        self
    }

    /// Captures a backtrace at this point.
    ///
    /// This is a no-op if the `backtrace` feature is not enabled.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use errcraft::ErrFrame;
    ///
    /// let err = ErrFrame::new("Critical error")
    ///     .capture_backtrace();
    /// ```
    #[cfg(all(feature = "std", feature = "backtrace"))]
    pub fn capture_backtrace(mut self) -> Self {
        self.backtrace = Some(std::backtrace::Backtrace::capture());
        self
    }

    #[cfg(not(all(feature = "std", feature = "backtrace")))]
    /// Captures a backtrace at this point (no-op without backtrace feature).
    pub fn capture_backtrace(self) -> Self {
        self
    }

    /// Returns the error message.
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Returns an iterator over the context layers.
    pub fn context_layers(&self) -> impl Iterator<Item = &ContextLayer> {
        self.context.iter()
    }

    /// Returns the backtrace, if captured.
    #[cfg(all(feature = "std", feature = "backtrace"))]
    pub fn backtrace(&self) -> Option<&std::backtrace::Backtrace> {
        self.backtrace.as_ref()
    }
}

#[cfg(feature = "std")]
impl std::fmt::Display for ErrFrame {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)?;

        if !self.context.is_empty() {
            write!(f, " (")?;
            let mut first = true;
            for layer in self.context.iter() {
                if !first {
                    write!(f, ", ")?;
                }
                first = false;

                if let Some(key) = layer.key() {
                    write!(f, "{}={}", key, layer.value().as_str())?;
                } else {
                    write!(f, "{}", layer.value().as_str())?;
                }
            }
            write!(f, ")")?;
        }

        Ok(())
    }
}

#[cfg(not(feature = "std"))]
impl core::fmt::Display for ErrFrame {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}", self.message)?;

        if !self.context.is_empty() {
            write!(f, " (")?;
            let mut first = true;
            for layer in self.context.iter() {
                if !first {
                    write!(f, ", ")?;
                }
                first = false;

                if let Some(key) = layer.key() {
                    write!(f, "{}={}", key, layer.value().as_str())?;
                } else {
                    write!(f, "{}", layer.value().as_str())?;
                }
            }
            write!(f, ")")?;
        }

        Ok(())
    }
}

#[cfg(feature = "std")]
impl std::error::Error for ErrFrame {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_ref().map(|e| e.as_ref() as _)
    }
}

// Note: We don't implement a blanket From<E: std::error::Error> because it conflicts
// with the reflexive From<T> for T impl. Instead, use .with_source() or explicit conversions.