Skip to main content

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(
279        &mut self,
280        input: &[u8],
281    ) -> Result<SafeJsonValue, JcsDecodeError<R, Q>> {
282        self.engine.decode_jcs_utf8(input, self.diagnostic_policy)
283    }
284
285    /// Decodes one complete UTF-8 JSON document into an owned value.
286    ///
287    /// The input is parsed with strict JSON and JCS binary64 number semantics;
288    /// accepted resource usage is charged to the cumulative session.
289    ///
290    /// # Parameters
291    ///
292    /// * `input` is the complete UTF-8 JSON document to decode.
293    ///
294    /// # Returns
295    ///
296    /// The owned JSON value on success.
297    ///
298    /// # Errors
299    ///
300    /// Returns `JcsDecodeError` for invalid JSON, unsupported JCS numbers, or
301    /// resource-limit violations.
302    pub fn decode_utf8(&mut self, input: &[u8]) -> Result<serde_json::Value, JcsDecodeError<R, Q>> {
303        self.decode_value_utf8(input).map(SafeJsonValue::into_value)
304    }
305
306    /// Reads a complete byte input through EOF into a guarded JSON value.
307    ///
308    /// The implementation buffers the full input before decoding. With a
309    /// finite input budget it may read one uncharged probe byte beyond the
310    /// limit to detect overflow; admitted bytes remain charged if decoding
311    /// later fails. The guarded result supports iterative cleanup on drop.
312    ///
313    /// # Type Parameters
314    ///
315    /// * `I` is the byte input implementation; it may be unsized.
316    ///
317    /// # Parameters
318    ///
319    /// * `input` supplies the document bytes and is read through EOF or the
320    ///   first input-budget overflow probe.
321    ///
322    /// # Returns
323    ///
324    /// The guarded owned JSON value on success.
325    ///
326    /// # Errors
327    ///
328    /// Returns [`JcsDecodeError`] for input read failures, invalid JSON,
329    /// duplicate keys, JCS number violations, or resource-limit violations.
330    pub fn decode_value_input<I>(
331        &mut self,
332        input: &mut I,
333    ) -> Result<SafeJsonValue, JcsDecodeError<R, Q>>
334    where
335        I: Input<Item = u8> + ?Sized,
336    {
337        self.engine.decode_jcs_input(input, self.diagnostic_policy)
338    }
339
340    /// Reads a complete byte input through EOF and decodes it into an owned
341    /// value.
342    ///
343    /// A finite input budget permits one uncharged probe byte to detect
344    /// overflow; accepted input bytes remain charged when later decoding
345    /// fails.
346    ///
347    /// # Type Parameters
348    ///
349    /// * `I` is the byte input implementation; it may be unsized.
350    ///
351    /// # Parameters
352    ///
353    /// * `input` supplies the document bytes and is read through EOF.
354    ///
355    /// # Returns
356    ///
357    /// The owned JSON value on success.
358    ///
359    /// # Errors
360    ///
361    /// Returns `JcsDecodeError` for input read failures, invalid JSON, JCS
362    /// number violations, or resource-limit violations.
363    pub fn decode_input<I>(
364        &mut self,
365        input: &mut I,
366    ) -> Result<serde_json::Value, JcsDecodeError<R, Q>>
367    where
368        I: Input<Item = u8> + ?Sized,
369    {
370        self.decode_value_input(input)
371            .map(SafeJsonValue::into_value)
372    }
373}