traced_error 0.1.1

Typed errors with automatic trace frames and structured context for Rust error propagation
Documentation
use smallvec::{SmallVec, smallvec};
use std::error::Error as StdError;
use std::fmt;
use std::panic::Location;

pub use traced_macro::traced;

/// 挂在 `Traced` 上的一条上下文数据。`payload` 类型擦除为 `Display + Send + Sync`,
/// 既可以是字符串、也可以是任意带 `Display` 的领域对象(e.g. 一个 RequestId newtype)。
pub struct ContextItem {
    pub loc: &'static Location<'static>,
    pub payload: Box<dyn fmt::Display + Send + Sync>,
}

/// 错误 + 调用轨迹 + 上下文数据。`E` 是类型化的真错误。
///
/// - `trace`: 一串 `&'static Location`,每个 `?` 经过都会 push 一帧(零分配)。
/// - `context`: 用户通过 `.ctx(...)` / `.with_ctx(|| ...)` 显式挂上去的数据。
pub struct Traced<E> {
    pub error: E,
    pub trace: SmallVec<[&'static Location<'static>; 4]>,
    pub context: SmallVec<[ContextItem; 2]>,
}

impl<E> Traced<E> {
    #[inline]
    #[track_caller]
    pub fn new(error: E) -> Self {
        Traced {
            error,
            trace: smallvec![Location::caller()],
            context: SmallVec::new(),
        }
    }

    /// 不记录任何 trace 帧的构造——给 `Context::ctx` 用,避免和 `?` 重复记帧。
    #[inline]
    pub fn bare(error: E) -> Self {
        Traced { error, trace: SmallVec::new(), context: SmallVec::new() }
    }

    #[inline]
    pub fn append(mut self, loc: &'static Location<'static>) -> Self {
        self.trace.push(loc);
        self
    }

    /// 类型转换 + 继承轨迹和上下文(都不丢)。跨类型 `?` 展开到这里。
    #[inline]
    pub fn map_err<F>(self, f: impl FnOnce(E) -> F) -> Traced<F> {
        Traced {
            error: f(self.error),
            trace: self.trace,
            context: self.context,
        }
    }

    /// 在 `Traced` 上直接挂一条数据。`#[track_caller]` 让 loc 指到调用 `.ctx()` 那一行。
    #[inline]
    #[track_caller]
    pub fn ctx<C: fmt::Display + Send + Sync + 'static>(mut self, c: C) -> Self {
        self.context.push(ContextItem {
            loc: Location::caller(),
            payload: Box::new(c),
        });
        self
    }
}

fn fmt_traced<E>(
    e: &E,
    trace: &[&'static Location<'static>],
    context: &[ContextItem],
    f: &mut fmt::Formatter<'_>,
    write_err: impl FnOnce(&E, &mut fmt::Formatter<'_>) -> fmt::Result,
) -> fmt::Result {
    write_err(e, f)?;
    writeln!(f)?;
    for (i, loc) in trace.iter().enumerate() {
        writeln!(f, "  {:>2}: at {}:{}:{}", i, loc.file(), loc.line(), loc.column())?;
    }
    if !context.is_empty() {
        writeln!(f, "context:")?;
        for item in context {
            writeln!(
                f,
                "   - {}  (at {}:{}:{})",
                item.payload,
                item.loc.file(),
                item.loc.line(),
                item.loc.column()
            )?;
        }
    }
    Ok(())
}

impl<E: fmt::Debug> fmt::Debug for Traced<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt_traced(&self.error, &self.trace, &self.context, f, |e, f| {
            write!(f, "{:?}", e)
        })
    }
}

impl<E: fmt::Display> fmt::Display for Traced<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt_traced(&self.error, &self.trace, &self.context, f, |e, f| {
            write!(f, "{}", e)
        })
    }
}

impl<E> StdError for Traced<E>
where
    E: StdError + 'static,
{
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(&self.error)
    }
}

/// 在 `Result<T, Traced<E>>` 上挂 context(保留 trace 和已有 context)。
pub trait Context<T, E>: Sized {
    fn ctx<C: fmt::Display + Send + Sync + 'static>(self, c: C) -> Result<T, Traced<E>>;
    fn with_ctx<C, G>(self, g: G) -> Result<T, Traced<E>>
    where
        C: fmt::Display + Send + Sync + 'static,
        G: FnOnce() -> C;
}

impl<T, E> Context<T, E> for Result<T, Traced<E>> {
    #[inline]
    #[track_caller]
    fn ctx<C: fmt::Display + Send + Sync + 'static>(self, c: C) -> Result<T, Traced<E>> {
        match self {
            Ok(v) => Ok(v),
            Err(t) => Err(t.ctx(c)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_ctx<C, G>(self, g: G) -> Result<T, Traced<E>>
    where
        C: fmt::Display + Send + Sync + 'static,
        G: FnOnce() -> C,
    {
        match self {
            Ok(v) => Ok(v),
            Err(t) => Err(t.ctx(g())),
        }
    }
}

/// 在 `Result<T, F>`(裸错误)上挂 context,把错误提升进 `Traced<F>`(不做类型转换、不记
/// trace 帧——类型转换和记帧都让后续的 `?` 去做)。
///
/// 方法名故意和 [`Context`] 不同:两个 trait 都用 `.ctx` 时 Rust 方法解析在确认 trait
/// bounds 之前就会判 ambiguous,所以这里用 `ctx_lift` / `with_ctx_lift`。
pub trait ContextRaw<T, F>: Sized {
    fn ctx_lift<C: fmt::Display + Send + Sync + 'static>(self, c: C) -> Result<T, Traced<F>>;
    fn with_ctx_lift<C, G>(self, g: G) -> Result<T, Traced<F>>
    where
        C: fmt::Display + Send + Sync + 'static,
        G: FnOnce() -> C;
}

impl<T, F> ContextRaw<T, F> for Result<T, F> {
    #[inline]
    #[track_caller]
    fn ctx_lift<C: fmt::Display + Send + Sync + 'static>(self, c: C) -> Result<T, Traced<F>> {
        match self {
            Ok(v) => Ok(v),
            Err(e) => Err(Traced::bare(e).ctx(c)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_ctx_lift<C, G>(self, g: G) -> Result<T, Traced<F>>
    where
        C: fmt::Display + Send + Sync + 'static,
        G: FnOnce() -> C,
    {
        match self {
            Ok(v) => Ok(v),
            Err(e) => Err(Traced::bare(e).ctx(g())),
        }
    }
}

/// 把两个 trait 一起 glob import 的便捷出口
pub mod prelude {
    pub use super::{Context, ContextRaw, Traced, traced};
}

/// 宏展开内部辅助。inherent vs trait 方法解析优先级实现 autoref-style specialization:
/// - Err 是 `Traced<F>` → inherent `__resolve` → 追加一帧 (保留 trace 和 context)。
/// - Err 是裸 `F` → fallback trait → 新建 `Traced<E>` 写入首帧 (context 为空)。
pub mod __macro {
    use super::Traced;
    use smallvec::{SmallVec, smallvec};
    use std::panic::Location;

    pub struct WrapErr<F>(pub F);

    impl<F> WrapErr<Traced<F>> {
        #[inline]
        pub fn __resolve<E: From<F>>(self, loc: &'static Location<'static>) -> Traced<E> {
            self.0.map_err(E::from).append(loc)
        }
    }

    pub trait ResolveFallback {
        type Inner;
        fn __resolve<E: From<Self::Inner>>(
            self,
            loc: &'static Location<'static>,
        ) -> Traced<E>;
    }

    impl<F> ResolveFallback for WrapErr<F> {
        type Inner = F;
        #[inline]
        fn __resolve<E: From<F>>(self, loc: &'static Location<'static>) -> Traced<E> {
            Traced {
                error: E::from(self.0),
                trace: smallvec![loc],
                context: SmallVec::new(),
            }
        }
    }
}