Skip to main content

qubit_validator/binding/
execution_error.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8
9//! Structured validation execution failures.
10
11use std::error::Error;
12use std::fmt;
13
14use super::ExecutionErrorKind;
15use crate::ValidatorId;
16
17/// An execution error with a safe public diagnostic surface.
18///
19/// Its public formatting and metadata never include raw validation input. An
20/// underlying cause can be retained for an explicitly trusted diagnostic
21/// consumer. The standard error chain deliberately does not expose that cause.
22/// The path is an opaque structured location: the caller or integrating layer
23/// defines its coordinate system. This type does not add path prefixes.
24///
25/// # Examples
26///
27/// ```
28/// use qubit_validator::{ExecutionError, ExecutionErrorKind};
29///
30/// let error = ExecutionError::new(ExecutionErrorKind::InputTypeMismatch);
31/// assert_eq!(error.kind(), ExecutionErrorKind::InputTypeMismatch);
32/// assert!(std::error::Error::source(&error).is_none());
33/// ```
34#[must_use]
35pub struct ExecutionError {
36    /// Stable category describing the execution failure.
37    kind: ExecutionErrorKind,
38    /// Structured location of the failure, without rendered raw keys.
39    path: super::ValidationPath,
40    /// Optional rule identifier associated with the failure.
41    rule_id: Option<ValidatorId>,
42    /// Optional dependency name associated with the failure.
43    dependency: Option<&'static str>,
44    /// Optional cause available through the explicit trusted accessor.
45    trusted_source: Option<Box<dyn Error + Send + Sync + 'static>>,
46}
47
48impl ExecutionError {
49    /// Creates an execution error at the root path.
50    ///
51    /// # Parameters
52    ///
53    /// - `kind`: Stable category describing why execution failed.
54    ///
55    /// # Returns
56    ///
57    /// A new error without an associated rule, dependency, or source.
58    #[inline]
59    pub fn new(kind: ExecutionErrorKind) -> Self {
60        Self {
61            kind,
62            path: super::ValidationPath::root(),
63            rule_id: None,
64            dependency: None,
65            trusted_source: None,
66        }
67    }
68
69    /// Returns the error kind.
70    ///
71    /// # Returns
72    ///
73    /// The stable category describing this execution failure.
74    #[inline]
75    pub const fn kind(&self) -> ExecutionErrorKind {
76        self.kind
77    }
78
79    /// Returns the associated rule identifier, if any.
80    ///
81    /// # Returns
82    ///
83    /// Returns `Some` when a rule has been attached, or `None` otherwise.
84    #[inline]
85    pub const fn rule_id(&self) -> Option<ValidatorId> {
86        self.rule_id
87    }
88
89    /// Returns the associated dependency slot name, if the error concerns one.
90    ///
91    /// # Returns
92    ///
93    /// Returns `Some` when a dependency has been attached, or `None` otherwise.
94    #[inline]
95    pub const fn dependency(&self) -> Option<&'static str> {
96        self.dependency
97    }
98
99    /// Returns the structured error path unchanged.
100    ///
101    /// The path remains structured and does not expose the raw input value.
102    /// Its coordinate system is defined by the caller or integrating layer.
103    #[must_use]
104    #[inline]
105    pub const fn path(&self) -> &super::ValidationPath {
106        &self.path
107    }
108
109    /// Returns the retained cause to a caller that is trusted to inspect it.
110    ///
111    /// `None` means no owned cause was supplied. This accessor may reveal
112    /// details unsuitable for ordinary logs or user-facing output.
113    ///
114    /// # Returns
115    ///
116    /// `Some` when a trusted cause was retained, or `None` otherwise.
117    pub fn trusted_source(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
118        self.trusted_source.as_deref()
119    }
120
121    /// Associates a rule identifier with this error.
122    ///
123    /// # Parameters
124    ///
125    /// - `rule_id`: Identifier of the rule whose execution failed.
126    ///
127    /// # Returns
128    ///
129    /// This error with the rule identifier attached.
130    #[inline]
131    pub const fn with_rule(mut self, rule_id: ValidatorId) -> Self {
132        self.rule_id = Some(rule_id);
133        self
134    }
135
136    /// Associates a dependency slot name with this error.
137    ///
138    /// When an integrating layer uses dependency metadata for location
139    /// handling, the name must match a slot declared for the current rule.
140    ///
141    /// # Parameters
142    ///
143    /// - `dependency`: Static name of the declared dependency slot involved in
144    ///   the failure.
145    ///
146    /// # Returns
147    ///
148    /// This error with the dependency name attached.
149    #[inline]
150    pub const fn with_dependency(mut self, dependency: &'static str) -> Self {
151        self.dependency = Some(dependency);
152        self
153    }
154
155    /// Retains an owned cause for an explicitly trusted diagnostic consumer.
156    ///
157    /// Ordinary formatting and [`std::error::Error::source`] never expose this
158    /// value. Callers must avoid forwarding it to untrusted logs or clients.
159    ///
160    /// # Type Parameters
161    ///
162    /// - `E`: The owned source error type retained behind the standard error
163    ///   trait object.
164    ///
165    /// # Parameters
166    ///
167    /// - `source`: Owned, thread-safe source error retained for this failure.
168    ///
169    /// # Returns
170    ///
171    /// This error with the trusted cause attached.
172    pub fn with_trusted_source<E>(mut self, source: E) -> Self
173    where
174        E: Error + Send + Sync + 'static,
175    {
176        self.trusted_source = Some(Box::new(source));
177        self
178    }
179
180    /// Replaces this error's structured path.
181    ///
182    /// This method does not concatenate or normalize paths. Integrations that
183    /// distinguish ordinary rule errors from dependency errors must apply the
184    /// appropriate coordinate convention at their own boundary.
185    ///
186    /// # Parameters
187    ///
188    /// - `path`: Structured location, in the coordinate system selected by the
189    ///   caller, that does not render raw input keys.
190    ///
191    /// # Returns
192    ///
193    /// This error with the supplied path attached.
194    #[inline]
195    pub fn with_path(mut self, path: super::ValidationPath) -> Self {
196        self.path = path;
197        self
198    }
199}
200
201impl fmt::Debug for ExecutionError {
202    /// Formats structural metadata without exposing raw input or source text.
203    ///
204    /// # Parameters
205    ///
206    /// - `formatter`: Destination formatter that receives the structural
207    ///   representation.
208    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
209        formatter
210            .debug_struct("ExecutionError")
211            .field("kind", &self.kind)
212            .field("rule_id", &self.rule_id)
213            .field("has_dependency", &self.dependency.is_some())
214            .field("has_trusted_source", &self.trusted_source.is_some())
215            .finish_non_exhaustive()
216    }
217}
218
219impl fmt::Display for ExecutionError {
220    /// Formats a stable summary without exposing raw input or source text.
221    ///
222    /// # Parameters
223    ///
224    /// - `formatter`: Destination formatter that receives the stable summary.
225    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(formatter, "validation execution failed: {}", self.kind)
227    }
228}
229
230impl Error for ExecutionError {}