masterror 0.29.0

Application error types and response mapping
Documentation
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
//
// SPDX-License-Identifier: MIT

use alloc::{borrow::Cow, boxed::Box};
use core::error::Error as CoreError;

#[cfg(feature = "backtrace")]
use {alloc::sync::Arc, std::backtrace::Backtrace};

#[cfg(feature = "backtrace")]
use super::backtrace::capture_backtrace_snapshot;
use super::{
    error::Error,
    types::{CapturedBacktrace, ErrorChain, StoredSource}
};
use crate::app_error::metadata::Metadata;

impl Error {
    /// Borrow the attached metadata.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use masterror::{AppError, field};
    ///
    /// let err = AppError::internal("test").with_field(field::str("key", "value"));
    /// let metadata = err.metadata();
    /// assert!(!metadata.is_empty());
    /// ```
    #[must_use]
    pub fn metadata(&self) -> &Metadata {
        &self.metadata
    }

    /// Borrow the backtrace, capturing it lazily when the `backtrace` feature
    /// is enabled.
    ///
    /// If a backtrace was previously attached via `with_backtrace()`, returns
    /// that. Otherwise, lazily captures a new backtrace based on
    /// `RUST_BACKTRACE` configuration.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "backtrace")]
    /// # {
    /// use masterror::AppError;
    ///
    /// let err = AppError::internal("test");
    /// let bt = err.backtrace();
    /// # }
    /// ```
    #[must_use]
    pub fn backtrace(&self) -> Option<&CapturedBacktrace> {
        self.capture_backtrace()
    }

    /// Returns a shared Arc reference to the backtrace.
    ///
    /// Internal method for efficient backtrace sharing between errors.
    #[cfg(feature = "backtrace")]
    pub(crate) fn backtrace_shared(&self) -> Option<Arc<Backtrace>> {
        if let Some(backtrace) = self.backtrace.as_ref() {
            return Some(Arc::clone(backtrace));
        }
        self.captured_backtrace
            .get_or_init(capture_backtrace_snapshot)
            .as_ref()
            .map(Arc::clone)
    }

    /// Borrow the source if present.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use masterror::AppError;
    ///
    /// let io_err = std::io::Error::new(std::io::ErrorKind::Other, "boom");
    /// let err = AppError::internal("failed").with_context(io_err);
    /// assert!(err.source_ref().is_some());
    /// # }
    /// ```
    #[must_use]
    pub fn source_ref(&self) -> Option<&(dyn CoreError + Send + Sync + 'static)> {
        self.source.as_ref().map(StoredSource::as_dyn)
    }

    /// Human-readable message or the kind fallback.
    ///
    /// Returns the error message if set, otherwise returns the error kind's
    /// default label.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use masterror::{AppError, AppErrorKind};
    ///
    /// let err = AppError::new(AppErrorKind::BadRequest, "custom message");
    /// assert_eq!(err.render_message(), "custom message");
    ///
    /// let bare_err = AppError::bare(AppErrorKind::NotFound);
    /// assert!(!bare_err.render_message().is_empty());
    /// ```
    #[must_use]
    pub fn render_message(&self) -> Cow<'_, str> {
        match &self.message {
            Some(msg) => Cow::Borrowed(msg.as_ref()),
            None => Cow::Borrowed(self.kind.label())
        }
    }

    /// Emit telemetry (`tracing` event, metrics counter, backtrace capture).
    ///
    /// Downstream code can call this to guarantee telemetry after mutating the
    /// error. It is automatically invoked by constructors and conversions.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use masterror::AppError;
    ///
    /// let err = AppError::internal("test");
    /// err.log();
    /// ```
    pub fn log(&self) {
        self.emit_telemetry();
    }

    /// Returns an iterator over the error chain, starting with this error.
    ///
    /// The iterator yields references to each error in the source chain,
    /// walking through [`source()`](CoreError::source) until reaching the
    /// root cause.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let app_err = AppError::internal("db down").with_context(io_err);
    ///
    /// let chain: Vec<_> = app_err.chain().collect();
    /// assert_eq!(chain.len(), 2);
    /// # }
    /// ```
    #[must_use]
    pub fn chain(&self) -> ErrorChain<'_> {
        ErrorChain {
            current: Some(self as &(dyn CoreError + 'static))
        }
    }

    /// Returns the lowest-level source error in the chain.
    ///
    /// This traverses the error source chain until it finds an error with no
    /// further source, then returns a reference to it. If this error has no
    /// source, it returns a reference to itself.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let app_err = AppError::internal("db down").with_context(io_err);
    ///
    /// let root = app_err.root_cause();
    /// assert_eq!(root.to_string(), "disk offline");
    /// # }
    /// ```
    #[must_use]
    pub fn root_cause(&self) -> &(dyn CoreError + 'static) {
        self.chain()
            .last()
            .expect("chain always has at least one error")
    }

    /// Check whether the attached source error is of a concrete type.
    ///
    /// Returns `true` if the immediate source (not this error itself, and not
    /// the entire chain) is of type `E`, `false` otherwise.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let app_err = AppError::internal("db down").with_context(io_err);
    ///
    /// assert!(app_err.is::<IoError>());
    ///
    /// let err_without_source = AppError::not_found("missing");
    /// assert!(!err_without_source.is::<IoError>());
    /// # }
    /// ```
    #[must_use]
    pub fn is<E>(&self) -> bool
    where
        E: CoreError + 'static
    {
        self.source_ref().is_some_and(|source| source.is::<E>())
    }

    /// Attempt to take ownership of the source error as a concrete type.
    ///
    /// Succeeds when the immediate source (not this error itself, and not the
    /// entire chain) is of type `E` and the error owns it exclusively, i.e.
    /// the source was attached from an owned value via
    /// [`with_source`](Self::with_source) or
    /// [`with_context`](Self::with_context).
    ///
    /// Sources attached as a shared [`Arc`](alloc::sync::Arc) via
    /// [`with_source_arc`](Self::with_source_arc) are never extracted by
    /// value, even when the `Arc` is uniquely referenced, because exclusive
    /// ownership of an unsized `Arc` cannot be reclaimed without unsafe code.
    /// Use [`downcast_ref`](Self::downcast_ref) to borrow such sources.
    ///
    /// # Errors
    ///
    /// Returns `Err(self)` with the error (including its source) intact when
    /// no source is attached, the source is not of type `E`, or the source is
    /// shared.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let err = AppError::internal("boom").with_context(io_err);
    ///
    /// let io = err.downcast::<IoError>().expect("owned io source");
    /// assert_eq!(io.to_string(), "disk offline");
    /// # }
    /// ```
    ///
    /// A failed downcast returns the original error unchanged:
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::{fmt::Error as FmtError, io::Error as IoError};
    ///
    /// use masterror::AppError;
    ///
    /// let err = AppError::internal("boom").with_context(IoError::other("disk offline"));
    ///
    /// let err = err
    ///     .downcast::<FmtError>()
    ///     .expect_err("source is not FmtError");
    /// assert!(err.is::<IoError>());
    /// # }
    /// ```
    pub fn downcast<E>(mut self) -> Result<Box<E>, Self>
    where
        E: CoreError + 'static
    {
        match self.source.take() {
            Some(StoredSource::Owned(source)) => match source.downcast::<E>() {
                Ok(source) => Ok(source),
                Err(source) => {
                    self.source = Some(StoredSource::Owned(source));
                    Err(self)
                }
            },
            other => {
                self.source = other;
                Err(self)
            }
        }
    }

    /// Attempt to downcast the attached source error to a concrete type by
    /// immutable reference.
    ///
    /// Returns `Some(&E)` if the immediate source is of type `E`, `None`
    /// otherwise (including when no source is attached).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let err = AppError::internal("boom").with_context(io_err);
    ///
    /// if let Some(io) = err.downcast_ref::<IoError>() {
    ///     assert_eq!(io.to_string(), "disk offline");
    /// }
    /// # }
    /// ```
    #[must_use]
    pub fn downcast_ref<E>(&self) -> Option<&E>
    where
        E: CoreError + 'static
    {
        self.source_ref()?.downcast_ref::<E>()
    }

    /// Attempt to downcast the attached source error to a concrete type by
    /// mutable reference.
    ///
    /// Returns `Some(&mut E)` when the immediate source (not this error
    /// itself, and not the entire chain) is of type `E` and mutable access is
    /// possible: the source was attached from an owned value, or it was
    /// attached as a shared [`Arc`](alloc::sync::Arc) that is currently
    /// uniquely referenced.
    ///
    /// Returns `None` when no source is attached, the source is not of type
    /// `E`, or the source is a shared `Arc` with other strong or weak
    /// references, since handing out `&mut E` would alias the other holders.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// use std::io::Error as IoError;
    ///
    /// use masterror::AppError;
    ///
    /// let io_err = IoError::other("disk offline");
    /// let mut err = AppError::internal("boom").with_context(io_err);
    ///
    /// let io = err.downcast_mut::<IoError>().expect("owned io source");
    /// *io = IoError::other("disk replaced");
    /// assert_eq!(
    ///     err.downcast_ref::<IoError>()
    ///         .expect("io source")
    ///         .to_string(),
    ///     "disk replaced"
    /// );
    /// # }
    /// ```
    #[must_use]
    pub fn downcast_mut<E>(&mut self) -> Option<&mut E>
    where
        E: CoreError + 'static
    {
        self.source.as_mut()?.as_dyn_mut()?.downcast_mut::<E>()
    }

    /// Convert the error into a boxed trait object.
    ///
    /// The error is always boxed as-is, never unwrapped to its source, so the
    /// returned trait object keeps the full `Display` output, the kind, the
    /// metadata and the entire [`source()`](CoreError::source) chain of this
    /// error. This mirrors `anyhow::Error::into_boxed_dyn_error` (available
    /// since anyhow 1.0.98) and forms a round-trip with
    /// [`from_boxed`](Self::from_boxed).
    ///
    /// ```rust
    /// use masterror::AppError;
    ///
    /// let err = AppError::internal("db down");
    /// let display = err.to_string();
    /// let boxed = err.into_boxed_dyn_error();
    /// assert_eq!(boxed.to_string(), display);
    /// ```
    ///
    /// Round-trip through [`from_boxed`](Self::from_boxed) keeps the chain:
    ///
    /// ```rust
    /// use core::error::Error;
    ///
    /// use masterror::{AppError, AppErrorKind};
    ///
    /// let source: Box<dyn Error + Send + Sync> = "root cause".into();
    /// let err = AppError::from_boxed(AppErrorKind::Internal, source);
    /// let restored = AppError::from_boxed(AppErrorKind::Internal, err.into_boxed_dyn_error());
    /// assert_eq!(restored.chain().count(), 3);
    /// assert_eq!(restored.root_cause().to_string(), "root cause");
    /// ```
    #[must_use]
    pub fn into_boxed_dyn_error(self) -> Box<dyn CoreError + Send + Sync + 'static> {
        Box::new(self)
    }
}