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

//! Functions for adding context to errors.
//!
//! This module provides the core functions that enable context propagation:
//! [`expect_error`] for use with [`scoped_error::Error`](crate::Error) or custom
//! error types, and [`expect_error_fn`] for advanced use cases.
//!
//! These functions work by:
//! 1. Executing a closure that returns `Result<T, Frame>`
//! 2. If the result is `Err`, attaching the context message and location
//! 3. Returning `Result<T, E>` where `E` is the target error type
//!
//! # How It Works
//!
//! The `?` operator automatically converts errors to `Frame` via the
//! `From` impl in [`error`](crate::error), capturing the source location.
//! The conversion functions then wrap this context with an additional message.

use std::borrow::Cow;

use crate::error::Frame;
use crate::error::WithContext;

/// Low-level function for adding context with a custom error constructor.
///
/// This function is the foundation for other conversion functions. It allows
/// complete control over how the final error is constructed.
///
/// # Type Parameters
///
/// - `F`: A function that constructs the target error type `E`
/// - `T`: The success type
/// - `E`: The target error type (must implement [`WithContext`])
///
/// # Example
///
/// ```
/// # use scoped_error::impl_context_error;
/// use scoped_error::{expect_error_fn, WithContext, Frame};
///
/// # impl_context_error!(MyError);
/// # impl MyError {
/// #   fn new(msg: &'static str) -> MyError {
/// #     MyError { message: msg.into(), source: None, location: None }
/// #   }
/// # }
///
/// # fn operation_that_might_fail() -> Result<(), MyError> { Ok(()) }
///
/// fn do_work() -> Result<(), MyError> {
///     let err = || MyError::new("custom");
///
///     expect_error_fn(err, || {
///         operation_that_might_fail()?;
///         Ok(())
///     })
/// }
/// ```
#[inline(always)]
pub fn expect_error_fn<F, T, E>(err: F, body: impl FnOnce() -> Result<T, Frame>) -> Result<T, E>
where
    F: FnOnce() -> E,
    E: WithContext,
{
    body().map_err(|context| err().with_context(context))
}

/// Add context to errors, returning a custom error type.
///
/// # Type Parameters
///
/// - `T`: The success type
/// - `E`: The custom error type
///
/// # Example
///
/// The simplest way to add context is using it with the ready to use
/// [`scoped_error::Error`](crate::Error) type when you don't need a custom error type.
///
/// ```
/// use scoped_error::{Error, expect_error};
///
/// fn read_file() -> Result<String, Error> {
///     expect_error("Failed to read configuration", || {
///         let cfg = std::fs::read_to_string("config.toml")?;
///         Ok(cfg)
///     })
/// }
/// ```
///
/// Use this function with custom error types that implement
/// `From<(Cow<'static, str>, Frame)>`. The [`impl_context_error!`](crate::impl_context_error)
/// macro generates this implementation for you.
///
/// ```
/// use scoped_error::{expect_error, impl_context_error};
///
/// impl_context_error!(MyError);
///
/// fn do_work() -> Result<String, MyError> {
///     expect_error("Failed to do work", || {
///         let cfg = std::fs::read_to_string("config.toml")?;
///         Ok(cfg)
///     })
/// }
/// ```
#[inline(always)]
pub fn expect_error<T, E>(
    msg: impl Into<Cow<'static, str>>,
    body: impl FnOnce() -> Result<T, Frame>,
) -> Result<T, E>
where
    E: From<(Cow<'static, str>, Frame)>,
{
    body().map_err(|context| (msg.into(), context).into())
}