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}