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

//! Helper macros and utilities.
//!
//! This module provides the [`impl_context_error!`] macro, which generates
//! boilerplate implementations for custom error types.

/// Implement context-aware error handling for a custom type.
///
/// This macro generates a struct definition and all necessary trait
/// implementations to make a type work with [`expect_error`](crate::expect_error).
///
/// # Generated Structure
///
/// The macro generates a struct with these fields:
/// - `message: Cow<'static, str>` - The error message
/// - `source: Option<Box<dyn Error + Send + Sync>>` - The underlying error
/// - `location: Option<&'static Location>` - Where the error was created
///
/// # Usage
///
/// ## Basic Usage (crate-private visibility)
///
/// ```
/// use scoped_error::impl_context_error;
///
/// impl_context_error!(MyError);
/// // Generates: pub(crate) struct MyError { ... }
/// ```
///
/// ## Custom Visibility
///
/// ```
/// use scoped_error::impl_context_error;
///
/// impl_context_error!(pub MyError);
/// // Generates: pub struct MyError { ... }
/// ```
///
/// # Generated Implementations
///
/// The macro generates:
/// - `#[derive(Debug)]`
/// - `std::error::Error` (with `source()` method)
/// - `std::fmt::Display` (includes location when available)
/// - `From<(Cow<'static, str>, Frame)>` (for use with `expect_error`)
#[macro_export]
macro_rules! impl_context_error {
    ($error_type:ident) => {
        impl_context_error!(pub(crate) $error_type);
    };
    ($vis:vis $error_type:ident) => {
        #[derive(Debug)]
        $vis struct $error_type {
            message: std::borrow::Cow<'static, str>,
            source: std::option::Option<Box<dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static>>,
            location: std::option::Option<&'static std::panic::Location<'static>>,
        }
        impl std::error::Error for $error_type {
            fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
                self.source.as_deref().map(|s| s as _)
            }
        }
        impl std::fmt::Display for $error_type {
            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 $crate::WithContext for $error_type {
            fn with_context(mut self, context: $crate::Frame) -> Self {
                self.source = Some(context.source);
                self.location = Some(context.location);
                self
            }
            fn location(&self) -> std::option::Option<&'static std::panic::Location<'static>> {
                self.location
            }
        }
        impl
            From<(
                std::borrow::Cow<'static, str>,
                $crate::Frame,
            )> for $error_type
        {
            fn from(
                value: (
                    std::borrow::Cow<'static, str>,
                    $crate::Frame,
                ),
            ) -> Self {
                Self {
                    message: value.0,
                    source: Some(value.1.source),
                    location: Some(value.1.location),
                }
            }
        }
    };
}

#[cfg(test)]
mod tests {
    use crate::ext::ErrorExt;

    impl_context_error!(Error);

    #[test]
    fn impl_error() {
        let errors = Error {
            message: "Failed to do something".into(),
            source: Some(
                Error {
                    message: "Failed due to internal error".into(),
                    source: Some(std::io::Error::other("Failed to perform IO").into()),
                    location: None,
                }
                .into(),
            ),
            location: None,
        };
        assert_eq!(
            errors.report().to_string(),
            "Failed to do something\n|-- Failed due to internal error\n`-- Failed to perform IO"
        );
    }
}