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 {}