qubit_json/decode/json_validated_decode_error.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Structured errors from validated JSON decoding.
9
10use std::error::Error;
11use std::fmt;
12
13use qubit_budget::ResourceQuantity;
14use qubit_budget::json::JsonResource;
15
16use super::JsonDecodeContext;
17use super::JsonDecodeError;
18use super::JsonNumberTokenError;
19use super::JsonValidatedLimit;
20
21/// A validated decoding failure with source offset and privacy-safe outer
22/// formatting.
23///
24/// The `Policy` variant preserves the caller's error unchanged. Its contents
25/// are omitted from this error's [`Display`](fmt::Display) and
26/// [`Debug`](fmt::Debug) output, but are available through [`Error::source`].
27/// This caller-provided source is outside [`super::DiagnosticPolicy`]'s
28/// redaction boundary; callers must choose safe policy error contents and
29/// logging behavior themselves.
30///
31/// # Type Parameters
32///
33/// * `E` - Error returned by the caller's number policy; it must implement
34/// [`Error`] when this value is used as an error source.
35/// * `R` - Resource identity carried by the underlying decode error; defaults
36/// to [`JsonResource`], and must implement [`fmt::Debug`] for formatting.
37/// * `Q` - Resource quantity accepted by [`ResourceQuantity`], which limits
38/// quantities to supported unsigned integer types; defaults to `usize`.
39///
40/// # Examples
41///
42/// ```
43/// use qubit_json::decode::{
44/// DiagnosticPolicy, JsonDecoder, JsonNumberPolicy,
45/// JsonValidatedDecodeError, JsonValidatedDecodeOptions,
46/// };
47/// use std::error::Error;
48///
49/// let options = JsonValidatedDecodeOptions::builder().build();
50/// let error = JsonDecoder::unlimited()
51/// .with_diagnostic_policy(DiagnosticPolicy::Redacted)
52/// .decode_unique_validated_str("1", &options, |_, token| {
53/// Err::<JsonNumberPolicy, _>(std::io::Error::other(format!(
54/// "policy-sensitive-marker:{}", token.raw(),
55/// )))
56/// })
57/// .unwrap_err();
58/// let sentinel = "policy-sensitive-marker";
59/// assert!(!error.to_string().contains(sentinel));
60/// assert!(!format!("{error:?}").contains(sentinel));
61/// let source = Error::source(&error).expect("caller policy error is preserved");
62/// assert!(source.to_string().contains(sentinel));
63/// assert!(matches!(error, JsonValidatedDecodeError::Policy { .. }));
64/// ```
65#[must_use]
66#[non_exhaustive]
67pub enum JsonValidatedDecodeError<E, R = JsonResource, Q = usize>
68where
69 Q: ResourceQuantity,
70{
71 /// A decoder, syntax, budget, or input failure.
72 Decode {
73 /// Structured decoding failure.
74 error: JsonDecodeError<R, Q>,
75 /// Grammar location associated with the failure, when known.
76 context: Option<JsonDecodeContext>,
77 },
78 /// The caller's number policy rejected a source token.
79 Policy {
80 /// Error returned by the caller's policy, preserved as the source.
81 ///
82 /// This value is not redacted by [`super::DiagnosticPolicy`]. Its
83 /// formatting is omitted from the outer error's `Display` and `Debug`,
84 /// but it remains accessible through [`Error::source`]. The caller is
85 /// responsible for keeping sensitive data out of this error and its
86 /// logs.
87 source: E,
88 /// Byte offset of the rejected number token.
89 offset: usize,
90 },
91 /// Exact-safe-integer conversion failed.
92 ExactSafeInteger {
93 /// Exact conversion failure category.
94 source: JsonNumberTokenError,
95 /// Byte offset of the number token.
96 offset: usize,
97 },
98 /// An additional validated decode limit was exceeded.
99 Limit {
100 /// Limit that rejected the document.
101 limit: JsonValidatedLimit,
102 /// Configured maximum.
103 maximum: usize,
104 /// Value observed when the limit was checked.
105 observed: usize,
106 /// Byte offset associated with the rejected token.
107 offset: usize,
108 },
109}
110
111impl<E, R, Q> fmt::Debug for JsonValidatedDecodeError<E, R, Q>
112where
113 R: fmt::Debug,
114 Q: ResourceQuantity,
115{
116 /// Formats this error for debugging while omitting caller policy details.
117 ///
118 /// The `Policy` variant's source is intentionally excluded so its
119 /// potentially sensitive contents do not appear in debug output.
120 ///
121 /// # Parameters
122 ///
123 /// * `f` - Formatter that receives the safe debug representation.
124 ///
125 /// # Returns
126 ///
127 /// Returns `Ok(())` after formatting, or the formatter's error if writing
128 /// fails.
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::Decode { error, context } => f
132 .debug_struct("Decode")
133 .field("error", error)
134 .field("context", context)
135 .finish(),
136 Self::Policy { offset, .. } => f
137 .debug_struct("Policy")
138 .field("offset", offset)
139 .finish_non_exhaustive(),
140 Self::ExactSafeInteger { source, offset } => f
141 .debug_struct("ExactSafeInteger")
142 .field("source", source)
143 .field("offset", offset)
144 .finish(),
145 Self::Limit {
146 limit,
147 maximum,
148 observed,
149 offset,
150 } => f
151 .debug_struct("Limit")
152 .field("limit", limit)
153 .field("maximum", maximum)
154 .field("observed", observed)
155 .field("offset", offset)
156 .finish(),
157 }
158 }
159}
160
161impl<E, R, Q> fmt::Display for JsonValidatedDecodeError<E, R, Q>
162where
163 R: fmt::Debug,
164 Q: ResourceQuantity,
165{
166 /// Formats the failure category and safe location details for users.
167 ///
168 /// Caller policy error contents are intentionally omitted from the
169 /// formatted message.
170 ///
171 /// # Parameters
172 ///
173 /// * `f` - Formatter that receives the failure category and location.
174 ///
175 /// # Returns
176 ///
177 /// Returns `Ok(())` after formatting, or the formatter's error if writing
178 /// fails.
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 match self {
181 Self::Decode { error, context } => {
182 write!(f, "JSON decode failed ({context:?}): {error}")
183 }
184 Self::Policy { offset, .. } => {
185 write!(f, "JSON number policy rejected token at byte {offset}")
186 }
187 Self::ExactSafeInteger { source, offset } => {
188 write!(
189 f,
190 "exact safe integer conversion failed at byte {offset}: {source:?}"
191 )
192 }
193 Self::Limit {
194 limit,
195 maximum,
196 observed,
197 offset,
198 } => write!(
199 f,
200 "JSON limit {limit:?} exceeded at byte {offset}: maximum {maximum}, observed {observed}"
201 ),
202 }
203 }
204}
205
206impl<E, R, Q> Error for JsonValidatedDecodeError<E, R, Q>
207where
208 E: Error + 'static,
209 R: fmt::Debug + 'static,
210 Q: ResourceQuantity + 'static,
211{
212 /// Returns the underlying decoding or policy error when one is available.
213 ///
214 /// The caller's policy error remains accessible as a source even though
215 /// this type omits its contents from `Display` and `Debug` output.
216 ///
217 /// # Returns
218 ///
219 /// Returns the wrapped decoding or policy error for those variants, the
220 /// exact-safe-integer conversion error for that variant, or `None` when
221 /// the failure has no underlying error.
222 fn source(&self) -> Option<&(dyn Error + 'static)> {
223 match self {
224 Self::Decode { error, .. } => Some(error),
225 Self::Policy { source, .. } => Some(source),
226 Self::ExactSafeInteger { source, .. } => Some(source),
227 Self::Limit { .. } => None,
228 }
229 }
230}