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