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}