Skip to main content

qubit_json/decode/
json_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 caller-owned resource accounting.
9
10use qubit_budget::ResourceQuantity;
11use qubit_budget::json::JsonDecodeLimits;
12use qubit_budget::json::JsonDecodeSession;
13use qubit_budget::json::JsonResource;
14use qubit_io::Input;
15use serde::Deserialize;
16use serde::de::DeserializeOwned;
17use serde::de::DeserializeSeed;
18
19use super::DiagnosticPolicy;
20use super::JsonDecodeError;
21use super::JsonRootKind;
22use super::internal::JsonDecodeEngine;
23use super::internal::TypedSeed;
24
25/// Strictly decodes complete JSON documents while retaining cumulative usage.
26///
27/// This facade performs no normalization. It accepts integers from `i64::MIN`
28/// through `u64::MAX`, requires finite floating-point values, supports values
29/// borrowing from its input, and exposes caller-provided Serde seeds.
30///
31/// # Type Parameters
32///
33/// * `'budget` - Lifetime of the resource budget borrowed by the decoder's
34///   cumulative session.
35/// * `R` - Resource identity type tracked by the session.
36/// * `Q` - Quantity type used for resource limits and accounting.
37///
38/// # Examples
39///
40/// ```
41/// use qubit_json::decode::JsonDecoder;
42/// use serde_json::Value;
43///
44/// let mut decoder = JsonDecoder::unlimited();
45/// let value = decoder.decode_str::<Value>(r#"{"ok":true}"#)?;
46/// assert_eq!(value["ok"], true);
47/// # Ok::<(), qubit_json::decode::JsonDecodeError>(())
48/// ```
49#[derive(Debug)]
50pub struct JsonDecoder<'budget, R = JsonResource, Q = usize>
51where
52    Q: ResourceQuantity,
53{
54    /// Diagnostic detail retained for input-derived failures.
55    diagnostic_policy: DiagnosticPolicy,
56    /// Shared generic decoding and accounting core.
57    engine: JsonDecodeEngine<'budget, R, Q>,
58}
59
60impl<R, Q> JsonDecoder<'static, R, Q>
61where
62    R: Clone,
63    Q: ResourceQuantity,
64{
65    /// Creates a decoder with a cumulative session built from explicit limits.
66    ///
67    /// # Parameters
68    ///
69    /// * `limits` - Input and decoded-value limits used by the cumulative
70    ///   session.
71    ///
72    /// # Returns
73    ///
74    /// A decoder whose accounting starts empty and is constrained by `limits`.
75    #[inline(always)]
76    #[must_use]
77    pub fn with_limits(limits: JsonDecodeLimits<R, Q>) -> Self {
78        Self::new(JsonDecodeSession::from_limits(limits))
79    }
80}
81
82impl JsonDecoder<'static, JsonResource, usize> {
83    /// Creates a decoder with no configured input or value limits.
84    ///
85    /// # Returns
86    ///
87    /// A decoder using the standard resource identities with all limits
88    /// disabled.
89    #[inline(always)]
90    #[must_use]
91    pub fn unlimited() -> Self {
92        Self::with_limits(JsonDecodeLimits::new())
93    }
94}
95
96impl<'budget, R, Q> JsonDecoder<'budget, R, Q>
97where
98    R: Clone,
99    Q: ResourceQuantity,
100{
101    /// Creates a strict decoder around a reusable cumulative session.
102    ///
103    /// # Parameters
104    ///
105    /// * `session` - Cumulative session that receives input and decoded-value
106    ///   charges.
107    ///
108    /// # Returns
109    ///
110    /// A decoder that owns `session` until it is consumed by
111    /// [`Self::into_session`].
112    #[inline]
113    #[must_use]
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    /// Configures whether input-derived error sources are retained.
122    ///
123    /// The default is [`DiagnosticPolicy::Redacted`]. Selecting
124    /// [`DiagnosticPolicy::Detailed`] may retain source errors containing
125    /// fragments or structural details derived from the input.
126    ///
127    /// # Parameters
128    ///
129    /// * `policy` - Diagnostic retention policy for failures produced by this
130    ///   decoder.
131    ///
132    /// # Returns
133    ///
134    /// The decoder with the requested policy; its existing session is retained.
135    #[inline(always)]
136    #[must_use]
137    pub const fn with_diagnostic_policy(mut self, policy: DiagnosticPolicy) -> Self {
138        self.diagnostic_policy = policy;
139        self
140    }
141
142    /// Returns the configured diagnostic policy without changing the decoder.
143    ///
144    /// # Returns
145    ///
146    /// The policy used when constructing input-derived decode errors.
147    #[inline(always)]
148    #[must_use]
149    pub const fn diagnostic_policy(&self) -> DiagnosticPolicy {
150        self.diagnostic_policy
151    }
152
153    /// Returns the cumulative session for read-only inspection.
154    ///
155    /// The returned reference is borrowed from the decoder and exposes the
156    /// charges accumulated by completed operations.
157    ///
158    /// # Returns
159    ///
160    /// A shared reference to the decoder's cumulative session.
161    #[inline(always)]
162    #[must_use]
163    pub const fn session(&self) -> &JsonDecodeSession<'budget, R, Q> {
164        self.engine.session()
165    }
166
167    /// Returns mutable access to the cumulative session.
168    ///
169    /// Mutating the session changes the limits and accounting state used by
170    /// subsequent operations.
171    ///
172    /// # Returns
173    ///
174    /// A mutable reference tied to the decoder's lifetime.
175    #[inline(always)]
176    #[must_use]
177    pub const fn session_mut(&mut self) -> &mut JsonDecodeSession<'budget, R, Q> {
178        self.engine.session_mut()
179    }
180
181    /// Consumes the decoder and returns its cumulative session.
182    ///
183    /// This transfers ownership of all accumulated accounting state without
184    /// performing another decode or resetting the session.
185    ///
186    /// # Returns
187    ///
188    /// The session previously owned by this decoder.
189    #[inline(always)]
190    #[must_use]
191    pub fn into_session(self) -> JsonDecodeSession<'budget, R, Q> {
192        self.engine.into_session()
193    }
194
195    /// Decodes one complete JSON string and permits results borrowing `input`.
196    ///
197    /// # Type Parameters
198    ///
199    /// * `T` - Target type deserialized from the complete document.
200    ///
201    /// # Parameters
202    ///
203    /// * `input` - UTF-8 JSON text. The returned value may borrow from it.
204    ///
205    /// # Returns
206    ///
207    /// The deserialized value on success.
208    ///
209    /// # Errors
210    ///
211    /// Returns a structured error when input accounting, UTF-8 validation,
212    /// JSON parsing, or Serde deserialization fails.
213    pub fn decode_str<'de, T>(&mut self, input: &'de str) -> Result<T, JsonDecodeError<R, Q>>
214    where
215        T: Deserialize<'de>,
216    {
217        self.decode_seed_str(TypedSeed::new(), input)
218    }
219
220    /// Decodes one complete UTF-8 JSON byte slice and permits borrowed results.
221    ///
222    /// # Type Parameters
223    ///
224    /// * `T` - Target type deserialized from the complete document.
225    ///
226    /// # Parameters
227    ///
228    /// * `input` - Complete UTF-8 JSON bytes. The returned value may borrow
229    ///   from this slice.
230    ///
231    /// # Returns
232    ///
233    /// The deserialized value on success.
234    ///
235    /// # Errors
236    ///
237    /// Returns a structured error when accounting, UTF-8 validation, JSON
238    /// parsing, or Serde deserialization fails.
239    pub fn decode_utf8<'de, T>(&mut self, input: &'de [u8]) -> Result<T, JsonDecodeError<R, Q>>
240    where
241        T: Deserialize<'de>,
242    {
243        self.decode_seed_utf8(TypedSeed::new(), input)
244    }
245
246    /// Decodes a complete document from an item-oriented byte input through
247    /// EOF.
248    ///
249    /// Without an input-byte limit, the input is read in fixed-size chunks.
250    /// With a limit, each read is bounded by the remaining allowance plus at
251    /// most one probe byte. The probe distinguishes an input ending exactly
252    /// at the limit from an oversized input; it is neither charged nor
253    /// buffered, and cannot be returned to the input source. The complete
254    /// admitted document is buffered in memory because the returned value
255    /// must not borrow from the temporary buffer. Without an input-byte
256    /// limit, this buffering has no implicit size cap.
257    ///
258    /// # Parameters
259    ///
260    /// * `input` - Mutable byte input read through EOF.
261    ///
262    /// # Errors
263    ///
264    /// Returns a structured error for read failures, input-budget rejection,
265    /// invalid UTF-8, invalid JSON, or target deserialization failures.
266    pub fn decode_input<T, I>(&mut self, input: &mut I) -> Result<T, JsonDecodeError<R, Q>>
267    where
268        T: DeserializeOwned,
269        I: Input<Item = u8> + ?Sized,
270    {
271        self.engine.decode_input(input, self.diagnostic_policy)
272    }
273
274    /// Decodes a JSON string through a caller-provided Serde seed.
275    ///
276    /// # Type Parameters
277    ///
278    /// * `S` - Seed controlling construction of the decoded value.
279    ///
280    /// # Parameters
281    ///
282    /// * `seed` - Serde seed used to deserialize the document.
283    /// * `input` - Complete JSON text, which the seed may borrow from.
284    ///
285    /// # Returns
286    ///
287    /// The value produced by `seed`.
288    ///
289    /// # Errors
290    ///
291    /// Returns a structured error when accounting, parsing, or seeded
292    /// deserialization fails.
293    pub fn decode_seed_str<'de, S>(&mut self, seed: S, input: &'de str) -> Result<S::Value, JsonDecodeError<R, Q>>
294    where
295        S: DeserializeSeed<'de>,
296    {
297        self.decode_seed_utf8(seed, input.as_bytes())
298    }
299
300    /// Decodes a UTF-8 byte slice through a caller-provided Serde seed.
301    ///
302    /// # Type Parameters
303    ///
304    /// * `S` - Seed controlling construction of the decoded value.
305    ///
306    /// # Parameters
307    ///
308    /// * `seed` - Serde seed used to deserialize the document.
309    /// * `input` - Complete UTF-8 JSON bytes, which the seed may borrow from.
310    ///
311    /// # Returns
312    ///
313    /// The value produced by `seed`.
314    ///
315    /// # Errors
316    ///
317    /// Returns a structured error when accounting, UTF-8 validation, parsing,
318    /// or seeded deserialization fails.
319    pub fn decode_seed_utf8<'de, S>(&mut self, seed: S, input: &'de [u8]) -> Result<S::Value, JsonDecodeError<R, Q>>
320    where
321        S: DeserializeSeed<'de>,
322    {
323        self.engine.decode_seed_utf8(seed, input, self.diagnostic_policy)
324    }
325
326    /// Decodes a complete JSON string while requiring a top-level object.
327    ///
328    /// The top-level check is performed before the decoded value is committed,
329    /// so an array, scalar, or otherwise valid non-object document is rejected.
330    ///
331    /// # Type Parameters
332    ///
333    /// * `T` - Target type deserialized from the object document.
334    ///
335    /// # Parameters
336    ///
337    /// * `input` - Complete JSON text, which the returned value may borrow.
338    ///
339    /// # Returns
340    ///
341    /// The deserialized object value on success.
342    ///
343    /// # Errors
344    ///
345    /// Returns a structured error for accounting, parsing, top-level-kind, or
346    /// deserialization failures.
347    pub fn decode_object_str<'de, T>(&mut self, input: &'de str) -> Result<T, JsonDecodeError<R, Q>>
348    where
349        T: Deserialize<'de>,
350    {
351        self.decode_object_utf8(input.as_bytes())
352    }
353
354    /// Decodes a complete UTF-8 byte slice while requiring a top-level object.
355    ///
356    /// A syntactically valid array or scalar is rejected by the top-level
357    /// constraint before the decoded value is committed.
358    ///
359    /// # Type Parameters
360    ///
361    /// * `T` - Target type deserialized from the object document.
362    ///
363    /// # Parameters
364    ///
365    /// * `input` - Complete UTF-8 JSON bytes, which the returned value may
366    ///   borrow.
367    ///
368    /// # Returns
369    ///
370    /// The deserialized object value on success.
371    ///
372    /// # Errors
373    ///
374    /// Returns a structured error for accounting, UTF-8 validation, parsing,
375    /// top-level-kind, or deserialization failures.
376    pub fn decode_object_utf8<'de, T>(&mut self, input: &'de [u8]) -> Result<T, JsonDecodeError<R, Q>>
377    where
378        T: Deserialize<'de>,
379    {
380        self.engine.decode_seed_utf8_with_top_level(
381            TypedSeed::new(),
382            input,
383            self.diagnostic_policy,
384            Some(JsonRootKind::Object),
385        )
386    }
387
388    /// Decodes a complete JSON string while requiring a top-level array.
389    ///
390    /// # Type Parameters
391    ///
392    /// * `T` - Element type deserialized from the array.
393    ///
394    /// # Parameters
395    ///
396    /// * `input` - Complete JSON text, which the returned elements may borrow.
397    ///
398    /// # Returns
399    ///
400    /// The decoded array elements on success.
401    ///
402    /// # Errors
403    ///
404    /// Returns a structured error for accounting, parsing, top-level-kind, or
405    /// deserialization failures.
406    pub fn decode_array_str<'de, T>(&mut self, input: &'de str) -> Result<Vec<T>, JsonDecodeError<R, Q>>
407    where
408        T: Deserialize<'de>,
409    {
410        self.decode_array_utf8(input.as_bytes())
411    }
412
413    /// Decodes a complete UTF-8 byte slice while requiring a top-level array.
414    ///
415    /// # Type Parameters
416    ///
417    /// * `T` - Element type deserialized from the array.
418    ///
419    /// # Parameters
420    ///
421    /// * `input` - Complete UTF-8 JSON bytes, which the returned elements may
422    ///   borrow.
423    ///
424    /// # Returns
425    ///
426    /// The decoded array elements on success.
427    ///
428    /// # Errors
429    ///
430    /// Returns a structured error for accounting, UTF-8 validation, parsing,
431    /// top-level-kind, or deserialization failures.
432    pub fn decode_array_utf8<'de, T>(&mut self, input: &'de [u8]) -> Result<Vec<T>, JsonDecodeError<R, Q>>
433    where
434        T: Deserialize<'de>,
435    {
436        self.engine.decode_seed_utf8_with_top_level(
437            TypedSeed::new(),
438            input,
439            self.diagnostic_policy,
440            Some(JsonRootKind::Array),
441        )
442    }
443
444    /// Validates and accounts for one complete JSON string without
445    /// materializing a target value.
446    ///
447    /// # Parameters
448    ///
449    /// * `input` - Complete JSON text to validate and account for.
450    ///
451    /// # Returns
452    ///
453    /// `Ok(())` after the complete document is valid and accounted for.
454    ///
455    /// # Errors
456    ///
457    /// Returns a structured error when accounting or JSON parsing fails. No
458    /// target value is allocated; `str` input is already valid UTF-8.
459    pub fn validate_str(&mut self, input: &str) -> Result<(), JsonDecodeError<R, Q>> {
460        self.validate_utf8(input.as_bytes())
461    }
462
463    /// Validates and accounts for one complete UTF-8 JSON byte slice without
464    /// materializing a target value.
465    ///
466    /// # Parameters
467    ///
468    /// * `input` - Complete UTF-8 JSON bytes to validate and account for.
469    ///
470    /// # Returns
471    ///
472    /// `Ok(())` after the complete document is valid and accounted for.
473    ///
474    /// # Errors
475    ///
476    /// Returns a structured error when accounting, UTF-8 validation, or JSON
477    /// parsing fails. No target value is allocated.
478    pub fn validate_utf8(&mut self, input: &[u8]) -> Result<(), JsonDecodeError<R, Q>> {
479        self.engine.validate_utf8(input, self.diagnostic_policy)
480    }
481}