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