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}