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}