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