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}