scoped-error 0.1.4

Structured error handling with semantic context trees.
Documentation
// Copyright (C) 2026 Kan-Ru Chen <kanru@kanru.info>
//
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

//! Core error types and traits for context-aware error handling.
//!
//! This module defines [`WithContext`], the central trait that enables
//! error context propagation, along with [`scoped_error::Error`] (a ready-to-use error
//! type) and [`Frame`] (a single layer of context).

use std::any::Any;
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt::{Debug, Display};
use std::panic::Location;

use crate::ext::ErrorExt;

/// A trait for error types that can carry context information.
///
/// Types implementing this trait can have context (message + location)
/// attached to them via [`with_context`](Self::with_context). This is the
/// core abstraction enabling the library's context propagation.
///
/// # Implementing
///
/// For most use cases, use the [`impl_context_error!`](crate::impl_context_error)
/// macro instead of implementing this manually.
///
/// # Example
///
/// ```
/// use std::borrow::Cow;
/// use std::error::Error;
/// use std::fmt::{Debug, Display};
/// use std::panic::Location;
///
/// use scoped_error::Frame;
/// use scoped_error::WithContext;
///
/// // Custom error type with context support
/// #[derive(Debug)]
/// struct MyError {
///     message: Cow<'static, str>,
///     source: Option<Box<dyn Error + Send + Sync>>,
///     location: Option<&'static Location<'static>>,
/// }
///
/// impl Error for MyError {}
/// impl Display for MyError {
///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
///         if let Some(loc) = self.location {
///             write!(f, "{}, at {}", self.message, loc)
///         } else {
///             f.write_str(&self.message)
///         }
///     }
/// }
///
/// impl WithContext for MyError {
///     fn with_context(mut self, context: Frame) -> Self {
///         self.source = Some(context.source);
///         self.location = Some(context.location);
///         self
///     }
///
///     fn location(&self) -> Option<&'static Location<'static>> {
///         self.location
///     }
/// }
/// ```
pub trait WithContext: StdError + Any {
    /// Attach a context layer to this error.
    ///
    /// The context includes a source error and the location where the
    /// context was added (captured via `#[track_caller]`).
    fn with_context(self, context: Frame) -> Self;

    /// Get the location where this error was created or where context was attached.
    fn location(&self) -> Option<&'static Location<'static>>;
}

/// A single layer of error context.
///
/// Contains the source error that caused this context layer, along with
/// the location where the context was added. Created automatically by
/// the conversion functions when an error occurs.
///
/// # Creation
///
/// `Frame` is typically created via `From<T>` where `T` can be
/// converted to `Box<dyn Error + Send + Sync>`. The location is captured
/// using `#[track_caller]`.
pub struct Frame {
    /// The underlying error that caused this context.
    pub source: Box<dyn StdError + Send + Sync + 'static>,
    /// The location where this context was attached.
    pub location: &'static Location<'static>,
}

impl<T> From<T> for Frame
where
    T: Into<Box<dyn StdError + Send + Sync + 'static>>,
{
    /// Creates an `Frame` from any error type, capturing the caller's location.
    #[track_caller]
    fn from(value: T) -> Self {
        let source = value.into();
        let location = Location::caller();
        Frame { source, location }
    }
}

/// A convenient, ready-to-use error type with built-in context support.
///
/// [`Error`] can be used directly without defining custom error types.
/// It stores an error message, optional source error, and optional location.
///
/// # Example
///
/// ```
/// use scoped_error::Error;
///
/// fn fallible() -> Result<(), Error> {
///     // &str or String can be converted to Error
///     Err(Error::new("Something went wrong"))
/// }
/// ```
pub struct Error {
    /// The error message.
    pub message: Cow<'static, str>,
    /// The underlying error that caused this one.
    pub source: Option<Box<dyn StdError + Send + Sync + 'static>>,
    /// The location where this error was created.
    pub location: Option<&'static Location<'static>>,
}

impl Error {
    /// Create a new `Error` with the given message.
    ///
    /// The source and location are initially `None`.
    pub fn new(msg: impl Into<Cow<'static, str>>) -> Error {
        Error {
            message: msg.into(),
            source: None,
            location: None,
        }
    }
}

impl Debug for Error {
    /// Formats using the error report for human-readable output.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.report().fmt(f)
    }
}

impl Display for Error {
    /// Displays the message with location if available.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(loc) = self.location {
            write!(f, "{}, at {}", self.message, loc)
        } else {
            f.write_str(&self.message)
        }
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.source.as_deref().map(|s| s as _)
    }
}

impl WithContext for Error {
    fn with_context(mut self, context: Frame) -> Self {
        self.source = Some(context.source);
        self.location = Some(context.location);
        self
    }
    fn location(&self) -> Option<&'static Location<'static>> {
        self.location
    }
}

impl From<(Cow<'static, str>, Frame)> for Error {
    fn from(value: (Cow<'static, str>, Frame)) -> Self {
        Error {
            message: value.0,
            source: Some(value.1.source),
            location: Some(value.1.location),
        }
    }
}