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.debug_struct("Policy").field("offset", offset).finish_non_exhaustive(),
137 Self::ExactSafeInteger { source, offset } => f
138 .debug_struct("ExactSafeInteger")
139 .field("source", source)
140 .field("offset", offset)
141 .finish(),
142 Self::Limit {
143 limit,
144 maximum,
145 observed,
146 offset,
147 } => f
148 .debug_struct("Limit")
149 .field("limit", limit)
150 .field("maximum", maximum)
151 .field("observed", observed)
152 .field("offset", offset)
153 .finish(),
154 }
155 }
156}
157
158impl<E, R, Q> fmt::Display for JsonValidatedDecodeError<E, R, Q>
159where
160 R: fmt::Debug,
161 Q: ResourceQuantity,
162{
163 /// Formats the failure category and safe location details for users.
164 ///
165 /// Caller policy error contents are intentionally omitted from the
166 /// formatted message.
167 ///
168 /// # Parameters
169 ///
170 /// * `f` - Formatter that receives the failure category and location.
171 ///
172 /// # Returns
173 ///
174 /// Returns `Ok(())` after formatting, or the formatter's error if writing
175 /// fails.
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 Self::Decode { error, context } => {
179 write!(f, "JSON decode failed ({context:?}): {error}")
180 }
181 Self::Policy { offset, .. } => {
182 write!(f, "JSON number policy rejected token at byte {offset}")
183 }
184 Self::ExactSafeInteger { source, offset } => {
185 write!(f, "exact safe integer conversion failed at byte {offset}: {source:?}")
186 }
187 Self::Limit {
188 limit,
189 maximum,
190 observed,
191 offset,
192 } => write!(
193 f,
194 "JSON limit {limit:?} exceeded at byte {offset}: maximum {maximum}, observed {observed}"
195 ),
196 }
197 }
198}
199
200impl<E, R, Q> Error for JsonValidatedDecodeError<E, R, Q>
201where
202 E: Error + 'static,
203 R: fmt::Debug + 'static,
204 Q: ResourceQuantity + 'static,
205{
206 /// Returns the underlying decoding or policy error when one is available.
207 ///
208 /// The caller's policy error remains accessible as a source even though
209 /// this type omits its contents from `Display` and `Debug` output.
210 ///
211 /// # Returns
212 ///
213 /// Returns the wrapped decoding or policy error for those variants, the
214 /// exact-safe-integer conversion error for that variant, or `None` when
215 /// the failure has no underlying error.
216 fn source(&self) -> Option<&(dyn Error + 'static)> {
217 match self {
218 Self::Decode { error, .. } => Some(error),
219 Self::Policy { source, .. } => Some(source),
220 Self::ExactSafeInteger { source, .. } => Some(source),
221 Self::Limit { .. } => None,
222 }
223 }
224}