qubit-validator 0.1.2

Type-safe validation traits and registries for Qubit applications
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================

//! Structured validation execution failures.

use std::error::Error;
use std::fmt;

use super::ExecutionErrorKind;
use crate::ValidatorId;

/// An execution error with a safe public diagnostic surface.
///
/// Its public formatting and metadata never include raw validation input. An
/// underlying cause can be retained for an explicitly trusted diagnostic
/// consumer. The standard error chain deliberately does not expose that cause.
/// The path is an opaque structured location: the caller or integrating layer
/// defines its coordinate system. This type does not add path prefixes.
///
/// # Examples
///
/// ```
/// use qubit_validator::{ExecutionError, ExecutionErrorKind};
///
/// let error = ExecutionError::new(ExecutionErrorKind::InputTypeMismatch);
/// assert_eq!(error.kind(), ExecutionErrorKind::InputTypeMismatch);
/// assert!(std::error::Error::source(&error).is_none());
/// ```
#[must_use]
pub struct ExecutionError {
    /// Stable category describing the execution failure.
    kind: ExecutionErrorKind,
    /// Structured location of the failure, without rendered raw keys.
    path: super::ValidationPath,
    /// Optional rule identifier associated with the failure.
    rule_id: Option<ValidatorId>,
    /// Optional dependency name associated with the failure.
    dependency: Option<&'static str>,
    /// Optional cause available through the explicit trusted accessor.
    trusted_source: Option<Box<dyn Error + Send + Sync + 'static>>,
}

impl ExecutionError {
    /// Creates an execution error at the root path.
    ///
    /// # Parameters
    ///
    /// - `kind`: Stable category describing why execution failed.
    ///
    /// # Returns
    ///
    /// A new error without an associated rule, dependency, or source.
    #[inline]
    pub fn new(kind: ExecutionErrorKind) -> Self {
        Self {
            kind,
            path: super::ValidationPath::root(),
            rule_id: None,
            dependency: None,
            trusted_source: None,
        }
    }

    /// Returns the error kind.
    ///
    /// # Returns
    ///
    /// The stable category describing this execution failure.
    #[inline]
    pub const fn kind(&self) -> ExecutionErrorKind {
        self.kind
    }

    /// Returns the associated rule identifier, if any.
    ///
    /// # Returns
    ///
    /// Returns `Some` when a rule has been attached, or `None` otherwise.
    #[inline]
    pub const fn rule_id(&self) -> Option<ValidatorId> {
        self.rule_id
    }

    /// Returns the associated dependency slot name, if the error concerns one.
    ///
    /// # Returns
    ///
    /// Returns `Some` when a dependency has been attached, or `None` otherwise.
    #[inline]
    pub const fn dependency(&self) -> Option<&'static str> {
        self.dependency
    }

    /// Returns the structured error path unchanged.
    ///
    /// The path remains structured and does not expose the raw input value.
    /// Its coordinate system is defined by the caller or integrating layer.
    #[must_use]
    #[inline]
    pub const fn path(&self) -> &super::ValidationPath {
        &self.path
    }

    /// Returns the retained cause to a caller that is trusted to inspect it.
    ///
    /// `None` means no owned cause was supplied. This accessor may reveal
    /// details unsuitable for ordinary logs or user-facing output.
    ///
    /// # Returns
    ///
    /// `Some` when a trusted cause was retained, or `None` otherwise.
    pub fn trusted_source(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
        self.trusted_source.as_deref()
    }

    /// Associates a rule identifier with this error.
    ///
    /// # Parameters
    ///
    /// - `rule_id`: Identifier of the rule whose execution failed.
    ///
    /// # Returns
    ///
    /// This error with the rule identifier attached.
    #[inline]
    pub const fn with_rule(mut self, rule_id: ValidatorId) -> Self {
        self.rule_id = Some(rule_id);
        self
    }

    /// Associates a dependency slot name with this error.
    ///
    /// When an integrating layer uses dependency metadata for location
    /// handling, the name must match a slot declared for the current rule.
    ///
    /// # Parameters
    ///
    /// - `dependency`: Static name of the declared dependency slot involved in
    ///   the failure.
    ///
    /// # Returns
    ///
    /// This error with the dependency name attached.
    #[inline]
    pub const fn with_dependency(mut self, dependency: &'static str) -> Self {
        self.dependency = Some(dependency);
        self
    }

    /// Retains an owned cause for an explicitly trusted diagnostic consumer.
    ///
    /// Ordinary formatting and [`std::error::Error::source`] never expose this
    /// value. Callers must avoid forwarding it to untrusted logs or clients.
    ///
    /// # Type Parameters
    ///
    /// - `E`: The owned source error type retained behind the standard error
    ///   trait object.
    ///
    /// # Parameters
    ///
    /// - `source`: Owned, thread-safe source error retained for this failure.
    ///
    /// # Returns
    ///
    /// This error with the trusted cause attached.
    pub fn with_trusted_source<E>(mut self, source: E) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        self.trusted_source = Some(Box::new(source));
        self
    }

    /// Replaces this error's structured path.
    ///
    /// This method does not concatenate or normalize paths. Integrations that
    /// distinguish ordinary rule errors from dependency errors must apply the
    /// appropriate coordinate convention at their own boundary.
    ///
    /// # Parameters
    ///
    /// - `path`: Structured location, in the coordinate system selected by the
    ///   caller, that does not render raw input keys.
    ///
    /// # Returns
    ///
    /// This error with the supplied path attached.
    #[inline]
    pub fn with_path(mut self, path: super::ValidationPath) -> Self {
        self.path = path;
        self
    }
}

impl fmt::Debug for ExecutionError {
    /// Formats structural metadata without exposing raw input or source text.
    ///
    /// # Parameters
    ///
    /// - `formatter`: Destination formatter that receives the structural
    ///   representation.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ExecutionError")
            .field("kind", &self.kind)
            .field("rule_id", &self.rule_id)
            .field("has_dependency", &self.dependency.is_some())
            .field("has_trusted_source", &self.trusted_source.is_some())
            .finish_non_exhaustive()
    }
}

impl fmt::Display for ExecutionError {
    /// Formats a stable summary without exposing raw input or source text.
    ///
    /// # Parameters
    ///
    /// - `formatter`: Destination formatter that receives the stable summary.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "validation execution failed: {}", self.kind)
    }
}

impl Error for ExecutionError {}