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/// ```
32/// use qubit_json::decode::{
33///     DiagnosticPolicy, JsonDecoder, JsonNumberPolicy,
34///     JsonValidatedDecodeError, JsonValidatedDecodeOptions,
35/// };
36/// use std::error::Error;
37///
38/// let options = JsonValidatedDecodeOptions::builder().build();
39/// let error = JsonDecoder::unlimited()
40///     .with_diagnostic_policy(DiagnosticPolicy::Redacted)
41///     .decode_unique_validated_str("1", &options, |_, token| {
42///         Err::<JsonNumberPolicy, _>(std::io::Error::other(format!(
43///             "policy-sensitive-marker:{}", token.raw(),
44///         )))
45///     })
46///     .unwrap_err();
47/// let sentinel = "policy-sensitive-marker";
48/// assert!(!error.to_string().contains(sentinel));
49/// assert!(!format!("{error:?}").contains(sentinel));
50/// let source = Error::source(&error).expect("caller policy error is preserved");
51/// assert!(source.to_string().contains(sentinel));
52/// assert!(matches!(error, JsonValidatedDecodeError::Policy { .. }));
53/// ```
54#[non_exhaustive]
55pub enum JsonValidatedDecodeError<E, R = JsonResource, Q = usize>
56where
57    Q: ResourceQuantity,
58{
59    /// A decoder, syntax, budget, or input failure.
60    Decode {
61        /// Structured decoding failure.
62        error: JsonDecodeError<R, Q>,
63        /// Grammar location associated with the failure, when known.
64        context: Option<JsonDecodeContext>,
65    },
66    /// The caller's number policy rejected a source token.
67    Policy {
68        /// Error returned by the caller's policy, preserved as the source.
69        ///
70        /// This value is not redacted by [`super::DiagnosticPolicy`]. Its
71        /// formatting is omitted from the outer error's `Display` and `Debug`,
72        /// but it remains accessible through [`Error::source`]. The caller is
73        /// responsible for keeping sensitive data out of this error and its
74        /// logs.
75        source: E,
76        /// Byte offset of the rejected number token.
77        offset: usize,
78    },
79    /// Exact-safe-integer conversion failed.
80    ExactSafeInteger {
81        /// Exact conversion failure category.
82        source: JsonNumberTokenError,
83        /// Byte offset of the number token.
84        offset: usize,
85    },
86    /// An additional validated decode limit was exceeded.
87    Limit {
88        /// Limit that rejected the document.
89        limit: JsonValidatedLimit,
90        /// Configured maximum.
91        maximum: usize,
92        /// Value observed when the limit was checked.
93        observed: usize,
94        /// Byte offset associated with the rejected token.
95        offset: usize,
96    },
97}
98
99impl<E, R, Q> fmt::Debug for JsonValidatedDecodeError<E, R, Q>
100where
101    R: fmt::Debug,
102    Q: ResourceQuantity,
103{
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::Decode { error, context } => f
107                .debug_struct("Decode")
108                .field("error", error)
109                .field("context", context)
110                .finish(),
111            Self::Policy { offset, .. } => f.debug_struct("Policy").field("offset", offset).finish_non_exhaustive(),
112            Self::ExactSafeInteger { source, offset } => f
113                .debug_struct("ExactSafeInteger")
114                .field("source", source)
115                .field("offset", offset)
116                .finish(),
117            Self::Limit {
118                limit,
119                maximum,
120                observed,
121                offset,
122            } => f
123                .debug_struct("Limit")
124                .field("limit", limit)
125                .field("maximum", maximum)
126                .field("observed", observed)
127                .field("offset", offset)
128                .finish(),
129        }
130    }
131}
132
133impl<E, R, Q> fmt::Display for JsonValidatedDecodeError<E, R, Q>
134where
135    R: fmt::Debug,
136    Q: ResourceQuantity,
137{
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        match self {
140            Self::Decode { error, context } => write!(f, "JSON decode failed ({context:?}): {error}"),
141            Self::Policy { offset, .. } => write!(f, "JSON number policy rejected token at byte {offset}"),
142            Self::ExactSafeInteger { source, offset } => {
143                write!(f, "exact safe integer conversion failed at byte {offset}: {source:?}")
144            }
145            Self::Limit {
146                limit,
147                maximum,
148                observed,
149                offset,
150            } => write!(
151                f,
152                "JSON limit {limit:?} exceeded at byte {offset}: maximum {maximum}, observed {observed}"
153            ),
154        }
155    }
156}
157
158impl<E, R, Q> Error for JsonValidatedDecodeError<E, R, Q>
159where
160    E: Error + 'static,
161    R: fmt::Debug + 'static,
162    Q: ResourceQuantity + 'static,
163{
164    fn source(&self) -> Option<&(dyn Error + 'static)> {
165        match self {
166            Self::Decode { error, .. } => Some(error),
167            Self::Policy { source, .. } => Some(source),
168            Self::ExactSafeInteger { source, .. } => Some(source),
169            Self::Limit { .. } => None,
170        }
171    }
172}