Skip to main content

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}