Skip to main content

qubit_budget/json/decode/
json_decode_attempt.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//! Provides transactional value accounting for one JSON decode attempt.
9
10use crate::json::JsonMeasurement;
11use crate::json::JsonValueTransaction;
12use crate::resource::MeasuredBudgetError;
13use crate::resource::ResourceBudget;
14use crate::resource::ResourceQuantity;
15
16/// I/O accounting and transactional value admission for one JSON decode.
17///
18/// Dropping an attempt rolls back JSON value accounting. Raw and normalized
19/// input charges remain committed, including while unwinding from a panic.
20///
21/// # Type Parameters
22///
23/// * `R` - Caller-defined resource identity retained by limits and errors.
24/// * `Q` - Exact unsigned quantity used for measurements and accounting.
25///
26/// # Examples
27///
28/// ```
29/// use qubit_budget::json::JsonDecodeLimits;
30/// use qubit_budget::json::JsonDecodeSession;
31/// use qubit_budget::json::JsonMeasurement;
32///
33/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
34/// let limits = JsonDecodeLimits::builder().max_nodes(1_usize).build();
35/// let mut session = JsonDecodeSession::from_limits(limits);
36/// let mut attempt = session.begin_value();
37/// attempt.try_admit(JsonMeasurement::Null { depth: 1 }).expect("null should fit");
38/// attempt.commit()?;
39/// # Ok(()) }
40/// ```
41pub struct JsonDecodeAttempt<'a, R, Q>
42where
43    Q: ResourceQuantity,
44{
45    /// Budget charged for raw input bytes.
46    input: Option<&'a mut ResourceBudget<R, Q>>,
47    /// Budget charged for normalized input bytes.
48    normalized_input: Option<&'a mut ResourceBudget<R, Q>>,
49    /// Working JSON value accounting published only by [`Self::commit`].
50    value: JsonValueTransaction<'a, R, Q>,
51}
52
53impl<'a, R, Q> JsonDecodeAttempt<'a, R, Q>
54where
55    R: Clone,
56    Q: ResourceQuantity,
57{
58    /// Creates an attempt from the budgets split out of a decode session.
59    ///
60    /// # Parameters
61    ///
62    /// * `input` - Input supplied to this operation.
63    /// * `normalized_input` - Optional normalized-input budget borrowed for
64    ///   immediate charges.
65    /// * `value` - Transaction holding the JSON value accounting staged by this
66    ///   attempt.
67    ///
68    /// # Returns
69    ///
70    /// Creates an attempt from the budgets split out of a decode session.
71    #[inline(always)]
72    #[must_use]
73    pub(crate) const fn new(
74        input: Option<&'a mut ResourceBudget<R, Q>>,
75        normalized_input: Option<&'a mut ResourceBudget<R, Q>>,
76        value: JsonValueTransaction<'a, R, Q>,
77    ) -> Self {
78        Self {
79            input,
80            normalized_input,
81            value,
82        }
83    }
84
85    /// Charges raw input bytes immediately when that budget is configured.
86    ///
87    /// Returns a quantity-conversion or budget error without changing the
88    /// configured input budget on failure. An absent input budget is ignored.
89    ///
90    /// # Parameters
91    ///
92    /// * `amount` - Number of raw input bytes to charge immediately.
93    ///
94    /// # Returns
95    ///
96    /// `Ok(())` when the operation completes successfully.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
101    /// or a configured limit rejects it.
102    #[inline]
103    pub fn try_consume_input_bytes(&mut self, amount: usize) -> Result<(), MeasuredBudgetError<R, Q>> {
104        consume_bytes(self.input.as_deref_mut(), amount)
105    }
106
107    /// Charges normalized input bytes immediately when that budget is set.
108    ///
109    /// Returns a quantity-conversion or budget error without changing the
110    /// configured normalized budget on failure. An absent budget is ignored.
111    ///
112    /// # Parameters
113    ///
114    /// * `amount` - Number of normalized input bytes to charge immediately.
115    ///
116    /// # Returns
117    ///
118    /// `Ok(())` when the operation completes successfully.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
123    /// or a configured limit rejects it.
124    #[inline]
125    pub fn try_consume_normalized_input_bytes(&mut self, amount: usize) -> Result<(), MeasuredBudgetError<R, Q>> {
126        consume_bytes(self.normalized_input.as_deref_mut(), amount)
127    }
128
129    /// Stages one JSON measurement for publication by [`Self::commit`].
130    ///
131    /// Returns the transaction's conversion or value-limit error. A failure
132    /// leaves this attempt's working value state and all I/O charges unchanged,
133    /// and poisons later value admissions and commit. I/O failures alone do
134    /// not poison the value transaction.
135    ///
136    /// # Parameters
137    ///
138    /// * `measurement` - Native JSON measurement to convert or admit.
139    ///
140    /// # Returns
141    ///
142    /// `Ok(())` when the operation completes successfully.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
147    /// or a configured limit rejects it.
148    #[inline]
149    pub fn try_admit(&mut self, measurement: JsonMeasurement) -> Result<(), MeasuredBudgetError<R, Q>> {
150        self.value.try_admit(measurement)
151    }
152
153    /// Returns the raw input budget while the attempt exclusively owns it.
154    ///
155    /// # Returns
156    ///
157    /// Returns the raw input budget while the attempt exclusively owns it.
158    ///
159    /// `None` indicates that the corresponding limit or budget dimension is
160    /// unconfigured.
161    #[must_use]
162    #[inline(always)]
163    pub fn input_budget(&self) -> Option<&ResourceBudget<R, Q>> {
164        self.input.as_deref()
165    }
166
167    /// Returns the normalized input budget while the attempt owns it.
168    ///
169    /// # Returns
170    ///
171    /// Returns the normalized input budget while the attempt owns it.
172    ///
173    /// `None` indicates that the corresponding limit or budget dimension is
174    /// unconfigured.
175    #[must_use]
176    #[inline(always)]
177    pub fn normalized_input_budget(&self) -> Option<&ResourceBudget<R, Q>> {
178        self.normalized_input.as_deref()
179    }
180
181    /// Returns staged node usage when the node limit is configured.
182    ///
183    /// # Returns
184    ///
185    /// Returns staged node usage when the node limit is configured.
186    ///
187    /// `None` indicates that the corresponding limit or budget dimension is
188    /// unconfigured.
189    #[must_use]
190    #[inline]
191    pub fn used_nodes(&self) -> Option<Q> {
192        self.value.used_nodes()
193    }
194
195    /// Returns staged remaining node capacity when the node limit is set.
196    ///
197    /// # Returns
198    ///
199    /// Returns staged remaining node capacity when the node limit is set.
200    ///
201    /// `None` indicates that the corresponding limit or budget dimension is
202    /// unconfigured.
203    #[must_use]
204    #[inline(always)]
205    pub const fn remaining_nodes(&self) -> Option<Q> {
206        self.value.remaining_nodes()
207    }
208
209    /// Returns staged payload usage when the payload limit is configured.
210    ///
211    /// # Returns
212    ///
213    /// Returns staged payload usage when the payload limit is configured.
214    ///
215    /// `None` indicates that the corresponding limit or budget dimension is
216    /// unconfigured.
217    #[must_use]
218    #[inline]
219    pub fn used_payload_bytes(&self) -> Option<Q> {
220        self.value.used_payload_bytes()
221    }
222
223    /// Returns staged remaining payload capacity when the payload limit is set.
224    ///
225    /// # Returns
226    ///
227    /// Returns staged remaining payload capacity when the payload limit is set.
228    ///
229    /// `None` indicates that the corresponding limit or budget dimension is
230    /// unconfigured.
231    #[must_use]
232    #[inline(always)]
233    pub const fn remaining_payload_bytes(&self) -> Option<Q> {
234        self.value.remaining_payload_bytes()
235    }
236
237    /// Returns the mutable transaction that holds this attempt's value state.
238    ///
239    /// # Returns
240    ///
241    /// Returns the mutable transaction that holds this attempt's value state.
242    #[must_use]
243    #[inline]
244    pub fn value_transaction_mut(&mut self) -> &mut JsonValueTransaction<'a, R, Q> {
245        &mut self.value
246    }
247
248    /// Publishes this attempt's staged value state without rolling back I/O.
249    ///
250    /// # Returns
251    ///
252    /// `Ok(())` after publishing the staged value state.
253    ///
254    /// # Errors
255    ///
256    /// Returns the first value-admission error when the attempt is poisoned.
257    pub fn commit(self) -> Result<(), MeasuredBudgetError<R, Q>> {
258        self.value.commit()
259    }
260}
261
262/// Converts and immediately consumes native bytes when a budget is present.
263///
264/// # Type Parameters
265///
266/// * `R` - Caller-defined resource identity retained by limits and errors.
267/// * `Q` - Exact unsigned quantity used for measurements and accounting.
268///
269/// # Parameters
270///
271/// * `budget` - Optional cumulative byte budget to charge.
272/// * `amount` - Native byte count to convert and charge when `budget` exists.
273///
274/// # Returns
275///
276/// `Ok(())` when the operation completes successfully.
277///
278/// # Errors
279///
280/// Returns [`MeasuredBudgetError`] when `amount` cannot fit `Q` or exceeds the
281/// budget's remaining capacity.
282#[inline]
283fn consume_bytes<R, Q>(
284    budget: Option<&mut ResourceBudget<R, Q>>,
285    amount: usize,
286) -> Result<(), MeasuredBudgetError<R, Q>>
287where
288    R: Clone,
289    Q: ResourceQuantity,
290{
291    match budget {
292        Some(budget) => budget.try_consume_usize(amount),
293        None => Ok(()),
294    }
295}