qubit_json/decode/jcs_decoder.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//! Stateful strict JSON decoding with JCS binary64 number semantics.
9
10use qubit_budget::ResourceQuantity;
11use qubit_budget::json::JsonDecodeLimits;
12use qubit_budget::json::JsonDecodeSession;
13use qubit_budget::json::JsonResource;
14use qubit_io::Input;
15
16use super::DiagnosticPolicy;
17use super::JcsDecodeError;
18use super::internal::JsonDecodeEngine;
19use crate::value::SafeJsonValue;
20
21/// Decodes strict JSON text into an owned value using finite binary64 numbers.
22/// Construction uses the shared builder and the configured, root-inclusive
23/// `max_depth`; there is no additional fixed container limit. The legacy
24/// methods return a bare `serde_json::Value`; callers accepting deep
25/// documents should use the `decode_value_*` methods to retain iterative
26/// cleanup.
27/// The decoder borrows its accounting session for `'budget`; `R` selects the
28/// resource kind and `Q` is the quantity representation used by that session.
29/// Calls reuse the same cumulative session, so limits apply across all inputs
30/// decoded through this value.
31///
32/// # Type Parameters
33///
34/// * `R` is the resource kind tracked by the session.
35/// * `Q` is the quantity type used to represent resource limits and usage.
36///
37/// # Examples
38///
39/// ```
40/// use qubit_json::decode::JcsDecoder;
41///
42/// let value = JcsDecoder::unlimited().decode_str("1.5").unwrap();
43/// assert_eq!(value.as_f64(), Some(1.5));
44/// ```
45#[derive(Debug)]
46pub struct JcsDecoder<'budget, R = JsonResource, Q = usize>
47where
48 Q: ResourceQuantity,
49{
50 /// Controls whether errors from future decode calls retain input-derived
51 /// details or redact them before returning to the caller.
52 diagnostic_policy: DiagnosticPolicy,
53 /// Performs parsing and resource accounting against the borrowed session.
54 engine: JsonDecodeEngine<'budget, R, Q>,
55}
56
57impl<R, Q> JcsDecoder<'static, R, Q>
58where
59 R: Clone,
60 Q: ResourceQuantity,
61{
62 /// Creates a decoder with the supplied cumulative resource limits.
63 ///
64 /// The limits are moved into a new accounting session and remain shared by
65 /// every decode performed through the returned decoder.
66 ///
67 /// # Parameters
68 ///
69 /// * `limits` contains the resource ceilings for this decoder's session.
70 ///
71 /// # Returns
72 ///
73 /// A decoder that owns a session initialized from `limits`.
74 #[must_use]
75 #[inline]
76 pub fn with_limits(limits: JsonDecodeLimits<R, Q>) -> Self {
77 Self::new(JsonDecodeSession::from_limits(limits))
78 }
79}
80
81impl JcsDecoder<'static, JsonResource, usize> {
82 /// Creates a decoder with no configured resource limits.
83 ///
84 /// Resource ceilings are not configured, so resource usage is not
85 /// limited by this session. Syntax, numeric, and duplicate-key checks
86 /// still apply; the shared builder has no separate fixed depth cap.
87 ///
88 /// # Returns
89 ///
90 /// A decoder backed by a fresh unlimited accounting session.
91 #[must_use]
92 #[inline]
93 pub fn unlimited() -> Self {
94 Self::with_limits(JsonDecodeLimits::new())
95 }
96}
97
98impl<'budget, R, Q> JcsDecoder<'budget, R, Q>
99where
100 R: Clone,
101 Q: ResourceQuantity,
102{
103 /// Creates a decoder around a reusable accounting session.
104 ///
105 /// The session is moved into the decoder and borrowed for `'budget`; its
106 /// cumulative usage and configured limits are retained across decode calls.
107 ///
108 /// # Parameters
109 ///
110 /// * `session` is the accounting state to use for subsequent decodes.
111 ///
112 /// # Returns
113 ///
114 /// A decoder using the supplied session and redacted diagnostics.
115 #[must_use]
116 #[inline]
117 pub const fn new(session: JsonDecodeSession<'budget, R, Q>) -> Self {
118 Self {
119 diagnostic_policy: DiagnosticPolicy::Redacted,
120 engine: JsonDecodeEngine::new(session),
121 }
122 }
123
124 /// Selects how much input-derived error detail is retained.
125 ///
126 /// Detailed diagnostics may retain input text and parser sources; use them
127 /// only when those values are safe to expose. This setting affects errors
128 /// from later decode calls.
129 ///
130 /// # Parameters
131 ///
132 /// * `policy` determines whether decode errors redact input-derived data.
133 ///
134 /// # Returns
135 ///
136 /// This decoder with the selected diagnostic policy.
137 #[must_use]
138 #[inline]
139 pub const fn with_diagnostic_policy(mut self, policy: DiagnosticPolicy) -> Self {
140 self.diagnostic_policy = policy;
141 self
142 }
143
144 /// Returns the active diagnostic policy.
145 ///
146 /// # Returns
147 ///
148 /// The policy applied to errors from subsequent decode calls.
149 #[must_use]
150 #[inline]
151 pub const fn diagnostic_policy(&self) -> DiagnosticPolicy {
152 self.diagnostic_policy
153 }
154
155 /// Returns the cumulative accounting session.
156 ///
157 /// The borrow is tied to this decoder and permits inspecting limits and
158 /// usage without transferring ownership of the session.
159 ///
160 /// # Returns
161 ///
162 /// A shared reference to the session used by this decoder.
163 #[must_use]
164 #[inline]
165 pub const fn session(&self) -> &JsonDecodeSession<'budget, R, Q> {
166 self.engine.session()
167 }
168
169 /// Returns mutable access to the cumulative accounting session.
170 ///
171 /// The borrow is tied to this decoder and allows callers to inspect or
172 /// adjust session accounting before another decode.
173 ///
174 /// # Returns
175 ///
176 /// A mutable reference to the session used by this decoder.
177 #[must_use]
178 #[inline]
179 pub const fn session_mut(&mut self) -> &mut JsonDecodeSession<'budget, R, Q> {
180 self.engine.session_mut()
181 }
182
183 /// Consumes this decoder and returns its accounting session.
184 ///
185 /// This transfers the accumulated resource state to the caller so it can
186 /// be inspected or reused by another decoder.
187 ///
188 /// # Returns
189 ///
190 /// The session previously owned by this decoder.
191 #[must_use]
192 #[inline]
193 pub fn into_session(self) -> JsonDecodeSession<'budget, R, Q> {
194 self.engine.into_session()
195 }
196
197 /// Decodes one complete JSON string into an owned value.
198 ///
199 /// The string is interpreted as strict JSON with JCS binary64 number
200 /// semantics. Resource usage is charged to this decoder's cumulative
201 /// session.
202 ///
203 /// # Parameters
204 ///
205 /// * `input` is the complete UTF-8 JSON document to decode.
206 ///
207 /// # Returns
208 ///
209 /// The owned JSON value on success.
210 ///
211 /// # Errors
212 ///
213 /// Returns `JcsDecodeError` when syntax, JCS number constraints, or the
214 /// configured resource limits reject the document.
215 #[inline]
216 pub fn decode_str(&mut self, input: &str) -> Result<serde_json::Value, JcsDecodeError<R, Q>> {
217 self.decode_utf8(input.as_bytes())
218 }
219
220 /// Decodes a complete JSON string into a tree with protected cleanup.
221 ///
222 /// The returned [`SafeJsonValue`] can be retained and dropped without
223 /// recursive tree cleanup. Resource usage is charged to this decoder's
224 /// cumulative session exactly once. The same root-inclusive configured
225 /// depth limit applies as for the legacy methods.
226 ///
227 /// # Parameters
228 ///
229 /// * `input` is the complete UTF-8 JSON document to decode.
230 ///
231 /// # Returns
232 ///
233 /// The guarded owned JSON value on success.
234 ///
235 /// # Errors
236 ///
237 /// Returns [`JcsDecodeError`] for invalid JSON, unsupported JCS numbers,
238 /// duplicate keys, or resource-limit violations. Accepted input bytes
239 /// remain charged when materialization fails.
240 ///
241 /// # Examples
242 ///
243 /// ```
244 /// use qubit_json::decode::JcsDecoder;
245 /// use qubit_json::encode::JcsEncoder;
246 ///
247 /// let owner = JcsDecoder::unlimited()
248 /// .decode_value_str(r#"{"b":2,"a":1}"#)
249 /// .unwrap();
250 /// let bytes = JcsEncoder::unlimited().to_vec(owner.as_value()).unwrap();
251 /// assert_eq!(bytes, br#"{"a":1,"b":2}"#);
252 /// ```
253 pub fn decode_value_str(&mut self, input: &str) -> Result<SafeJsonValue, JcsDecodeError<R, Q>> {
254 self.decode_value_utf8(input.as_bytes())
255 }
256
257 /// Decodes a complete UTF-8 JSON document into a tree with protected
258 /// cleanup.
259 ///
260 /// The guarded result supports iterative cleanup when dropped, including
261 /// during panic unwinding. Its `as_value()` view can be passed to
262 /// [`crate::encode::JcsEncoder`]. The configured root-inclusive depth limit
263 /// and cumulative accounting session are shared with the legacy methods.
264 ///
265 /// # Parameters
266 ///
267 /// * `input` is the complete UTF-8 JSON document to decode.
268 ///
269 /// # Returns
270 ///
271 /// The guarded owned JSON value on success.
272 ///
273 /// # Errors
274 ///
275 /// Returns [`JcsDecodeError`] for invalid JSON, unsupported JCS numbers,
276 /// duplicate keys, or resource-limit violations. Input usage already
277 /// admitted remains charged when later materialization fails.
278 pub fn decode_value_utf8(&mut self, input: &[u8]) -> Result<SafeJsonValue, JcsDecodeError<R, Q>> {
279 self.engine.decode_jcs_utf8(input, self.diagnostic_policy)
280 }
281
282 /// Decodes one complete UTF-8 JSON document into an owned value.
283 ///
284 /// The input is parsed with strict JSON and JCS binary64 number semantics;
285 /// accepted resource usage is charged to the cumulative session.
286 ///
287 /// # Parameters
288 ///
289 /// * `input` is the complete UTF-8 JSON document to decode.
290 ///
291 /// # Returns
292 ///
293 /// The owned JSON value on success.
294 ///
295 /// # Errors
296 ///
297 /// Returns `JcsDecodeError` for invalid JSON, unsupported JCS numbers, or
298 /// resource-limit violations.
299 pub fn decode_utf8(&mut self, input: &[u8]) -> Result<serde_json::Value, JcsDecodeError<R, Q>> {
300 self.decode_value_utf8(input).map(SafeJsonValue::into_value)
301 }
302
303 /// Reads a complete byte input through EOF into a guarded JSON value.
304 ///
305 /// The implementation buffers the full input before decoding. With a
306 /// finite input budget it may read one uncharged probe byte beyond the
307 /// limit to detect overflow; admitted bytes remain charged if decoding
308 /// later fails. The guarded result supports iterative cleanup on drop.
309 ///
310 /// # Type Parameters
311 ///
312 /// * `I` is the byte input implementation; it may be unsized.
313 ///
314 /// # Parameters
315 ///
316 /// * `input` supplies the document bytes and is read through EOF or the
317 /// first input-budget overflow probe.
318 ///
319 /// # Returns
320 ///
321 /// The guarded owned JSON value on success.
322 ///
323 /// # Errors
324 ///
325 /// Returns [`JcsDecodeError`] for input read failures, invalid JSON,
326 /// duplicate keys, JCS number violations, or resource-limit violations.
327 pub fn decode_value_input<I>(&mut self, input: &mut I) -> Result<SafeJsonValue, JcsDecodeError<R, Q>>
328 where
329 I: Input<Item = u8> + ?Sized,
330 {
331 self.engine.decode_jcs_input(input, self.diagnostic_policy)
332 }
333
334 /// Reads a complete byte input through EOF and decodes it into an owned
335 /// value.
336 ///
337 /// A finite input budget permits one uncharged probe byte to detect
338 /// overflow; accepted input bytes remain charged when later decoding
339 /// fails.
340 ///
341 /// # Type Parameters
342 ///
343 /// * `I` is the byte input implementation; it may be unsized.
344 ///
345 /// # Parameters
346 ///
347 /// * `input` supplies the document bytes and is read through EOF.
348 ///
349 /// # Returns
350 ///
351 /// The owned JSON value on success.
352 ///
353 /// # Errors
354 ///
355 /// Returns `JcsDecodeError` for input read failures, invalid JSON, JCS
356 /// number violations, or resource-limit violations.
357 pub fn decode_input<I>(&mut self, input: &mut I) -> Result<serde_json::Value, JcsDecodeError<R, Q>>
358 where
359 I: Input<Item = u8> + ?Sized,
360 {
361 self.decode_value_input(input).map(SafeJsonValue::into_value)
362 }
363}