Skip to main content

qubit_budget/json/decode/
json_decode_session.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//! Tracks mutable accounting for one JSON decoding operation.
9
10use super::JsonDecodeAttempt;
11use super::JsonDecodeLimits;
12use super::internal::DecodeStorage;
13use crate::json::JsonResource;
14use crate::json::JsonValueBudget;
15use crate::resource::ResourceBudget;
16use crate::resource::ResourceQuantity;
17
18/// Mutable resource accounting for one JSON decoding operation.
19///
20/// Use [`Self::from_limits`] for a session that owns budgets created from
21/// immutable limits, or one of the `borrowing_*` constructors when the caller
22/// owns the budgets. Create an attempt with [`Self::begin_value`] for each
23/// complete value; input charges are immediate, while value accounting is
24/// committed by [`JsonDecodeAttempt::commit`].
25///
26/// # Type Parameters
27///
28/// * `R` - Caller-defined resource identity retained by limits and errors.
29/// * `Q` - Exact unsigned quantity used for measurements and accounting.
30///
31/// # Examples
32///
33/// ```
34/// use qubit_budget::json::JsonDecodeLimits;
35/// use qubit_budget::json::JsonDecodeSession;
36/// use qubit_budget::json::JsonMeasurement;
37///
38/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
39/// let limits = JsonDecodeLimits::builder()
40///     .max_input_bytes(4_usize)
41///     .max_nodes(1_usize)
42///     .build();
43/// let mut session = JsonDecodeSession::from_limits(limits);
44/// let mut attempt = session.begin_value();
45/// attempt
46///     .try_consume_input_bytes(4)
47///     .expect("the input should fit");
48/// attempt
49///     .try_admit(JsonMeasurement::Null { depth: 1 })
50///     .expect("the value should fit");
51/// attempt.commit()?;
52/// assert_eq!(session.input_budget().expect("input budget").used(), 4);
53/// # Ok(()) }
54/// ```
55#[derive(Debug)]
56pub struct JsonDecodeSession<'a, R = JsonResource, Q = usize>
57where
58    Q: ResourceQuantity,
59{
60    /// Owned or borrowed budgets backing this decode operation.
61    storage: DecodeStorage<'a, R, Q>,
62}
63
64impl<'a, R, Q> JsonDecodeSession<'a, R, Q>
65where
66    R: Clone,
67    Q: ResourceQuantity,
68{
69    /// Creates a session borrowing only a caller-owned value budget.
70    ///
71    /// # Parameters
72    ///
73    /// * `value` - Caller-owned JSON value budget to update after a successful
74    ///   decode attempt.
75    ///
76    /// # Returns
77    ///
78    /// Creates a session borrowing only a caller-owned value budget.
79    #[inline]
80    #[must_use]
81    pub fn borrowing_value(value: &'a mut JsonValueBudget<R, Q>) -> Self {
82        Self {
83            storage: DecodeStorage::Borrowed {
84                input: None,
85                normalized_input: None,
86                value,
87            },
88        }
89    }
90
91    /// Creates a session borrowing caller-owned raw-input and value budgets.
92    ///
93    /// # Parameters
94    ///
95    /// * `input` - Input supplied to this operation.
96    /// * `value` - Caller-owned JSON value budget to update after a successful
97    ///   decode attempt.
98    ///
99    /// # Returns
100    ///
101    /// Creates a session borrowing caller-owned raw-input and value budgets.
102    #[inline]
103    #[must_use]
104    pub fn borrowing_input(input: &'a mut ResourceBudget<R, Q>, value: &'a mut JsonValueBudget<R, Q>) -> Self {
105        Self {
106            storage: DecodeStorage::Borrowed {
107                input: Some(input),
108                normalized_input: None,
109                value,
110            },
111        }
112    }
113
114    /// Creates a session borrowing all caller-owned decode budgets.
115    ///
116    /// # Parameters
117    ///
118    /// * `input` - Input supplied to this operation.
119    /// * `normalized_input` - Optional caller-owned normalized-input budget.
120    /// * `value` - Caller-owned JSON value budget to update after a successful
121    ///   decode attempt.
122    ///
123    /// # Returns
124    ///
125    /// Creates a session borrowing all caller-owned decode budgets.
126    #[inline]
127    #[must_use]
128    pub fn borrowing_all(
129        input: &'a mut ResourceBudget<R, Q>,
130        normalized_input: &'a mut ResourceBudget<R, Q>,
131        value: &'a mut JsonValueBudget<R, Q>,
132    ) -> Self {
133        Self {
134            storage: DecodeStorage::Borrowed {
135                input: Some(input),
136                normalized_input: Some(normalized_input),
137                value,
138            },
139        }
140    }
141
142    /// Starts accounting for one complete JSON value.
143    ///
144    /// The returned attempt charges raw and normalized input immediately, but
145    /// publishes staged JSON value accounting only after a successful `commit`.
146    /// A value-admission failure poisons commit; dropping the attempt rolls
147    /// back only the staged value state.
148    ///
149    /// # Returns
150    ///
151    /// Starts accounting for one complete JSON value.
152    #[must_use]
153    pub fn begin_value(&mut self) -> JsonDecodeAttempt<'_, R, Q> {
154        let (input, normalized_input, value) = self.storage.split();
155        JsonDecodeAttempt::new(input, normalized_input, value.transaction())
156    }
157
158    /// Returns the raw input budget when configured.
159    ///
160    /// # Returns
161    ///
162    /// Returns the raw input budget when configured.
163    ///
164    /// `None` indicates that the corresponding limit or budget dimension is
165    /// unconfigured.
166    #[must_use]
167    #[inline(always)]
168    pub fn input_budget(&self) -> Option<&ResourceBudget<R, Q>> {
169        match &self.storage {
170            DecodeStorage::Owned { input, .. } => input.as_ref(),
171            DecodeStorage::Borrowed { input, .. } => input.as_deref(),
172        }
173    }
174
175    /// Returns the configured raw input-byte maximum.
176    ///
177    /// # Returns
178    ///
179    /// Returns the configured raw input-byte maximum.
180    ///
181    /// `None` indicates that the corresponding limit or budget dimension is
182    /// unconfigured.
183    #[must_use]
184    #[inline(always)]
185    pub fn max_input_bytes(&self) -> Option<Q> {
186        self.input_budget().map(ResourceBudget::limit)
187    }
188
189    /// Returns the configured normalized input-byte maximum.
190    ///
191    /// # Returns
192    ///
193    /// Returns the configured normalized input-byte maximum.
194    ///
195    /// `None` indicates that the corresponding limit or budget dimension is
196    /// unconfigured.
197    #[must_use]
198    #[inline(always)]
199    pub fn max_normalized_input_bytes(&self) -> Option<Q> {
200        self.normalized_input_budget().map(ResourceBudget::limit)
201    }
202
203    /// Returns the normalized input budget when configured.
204    ///
205    /// # Returns
206    ///
207    /// Returns the normalized input budget when configured.
208    ///
209    /// `None` indicates that the corresponding limit or budget dimension is
210    /// unconfigured.
211    #[must_use]
212    #[inline(always)]
213    pub fn normalized_input_budget(&self) -> Option<&ResourceBudget<R, Q>> {
214        match &self.storage {
215            DecodeStorage::Owned { normalized_input, .. } => normalized_input.as_ref(),
216            DecodeStorage::Borrowed { normalized_input, .. } => normalized_input.as_deref(),
217        }
218    }
219
220    /// Returns the value budget for read-only inspection.
221    ///
222    /// # Returns
223    ///
224    /// Returns the value budget for read-only inspection.
225    #[must_use]
226    #[inline(always)]
227    pub fn value_budget(&self) -> &JsonValueBudget<R, Q> {
228        match &self.storage {
229            DecodeStorage::Owned { value, .. } => value,
230            DecodeStorage::Borrowed { value, .. } => value,
231        }
232    }
233}
234
235impl<R, Q> JsonDecodeSession<'static, R, Q>
236where
237    R: Clone,
238    Q: ResourceQuantity,
239{
240    /// Creates a session that owns budgets initialized from immutable limits.
241    ///
242    /// # Parameters
243    ///
244    /// * `limits` - Immutable decoding limits used to initialize owned
245    ///   accounting budgets.
246    ///
247    /// # Returns
248    ///
249    /// Creates a session that owns budgets initialized from immutable limits.
250    #[inline]
251    #[must_use]
252    pub fn from_limits(limits: JsonDecodeLimits<R, Q>) -> Self {
253        let input = limits.input_bytes_limit().cloned().map(ResourceBudget::from_limit);
254        let normalized_input = limits
255            .normalized_input_bytes_limit()
256            .cloned()
257            .map(ResourceBudget::from_limit);
258        let value = JsonValueBudget::new(limits.into_value_limits());
259        Self {
260            storage: DecodeStorage::Owned {
261                input,
262                normalized_input,
263                value,
264            },
265        }
266    }
267}