Skip to main content

qubit_budget/json/encode/
json_encode_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 encode 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 encode.
17///
18/// Dropping an attempt rolls back JSON value accounting. Accepted output
19/// 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::JsonEncodeLimits;
30/// use qubit_budget::json::JsonEncodeSession;
31/// use qubit_budget::json::JsonMeasurement;
32///
33/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
34/// let limits = JsonEncodeLimits::builder().max_nodes(1_usize).build();
35/// let mut session = JsonEncodeSession::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 JsonEncodeAttempt<'a, R, Q>
42where
43    Q: ResourceQuantity,
44{
45    /// Budget charged for accepted output bytes.
46    output: Option<&'a mut ResourceBudget<R, Q>>,
47    /// Working JSON value accounting published only by [`Self::commit`].
48    value: JsonValueTransaction<'a, R, Q>,
49}
50
51impl<'a, R, Q> JsonEncodeAttempt<'a, R, Q>
52where
53    R: Clone,
54    Q: ResourceQuantity,
55{
56    /// Creates an attempt from the budgets split out of an encode session.
57    ///
58    /// # Parameters
59    ///
60    /// * `output` - Output supplied to this operation.
61    /// * `value` - Transaction holding the JSON value accounting staged by this
62    ///   attempt.
63    ///
64    /// # Returns
65    ///
66    /// Creates an attempt from the budgets split out of an encode session.
67    #[inline(always)]
68    #[must_use = "the output-byte check result must be handled"]
69    pub(crate) const fn new(
70        output: Option<&'a mut ResourceBudget<R, Q>>,
71        value: JsonValueTransaction<'a, R, Q>,
72    ) -> Self {
73        Self { output, value }
74    }
75
76    /// Checks whether output bytes fit without charging them.
77    ///
78    /// Returns a quantity-conversion or budget error without changing the
79    /// configured output budget. An absent output budget is ignored.
80    ///
81    /// # Parameters
82    ///
83    /// * `amount` - Number of output bytes to check without charging.
84    ///
85    /// # Returns
86    ///
87    /// `Ok(())` when the operation completes successfully.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
92    /// or a configured limit rejects it.
93    #[inline]
94    #[must_use = "the output-byte check result must be handled"]
95    pub fn check_output_bytes(&self, amount: usize) -> Result<(), MeasuredBudgetError<R, Q>> {
96        match self.output.as_deref() {
97            Some(budget) => budget.check_available_usize(amount),
98            None => Ok(()),
99        }
100    }
101
102    /// Charges accepted output bytes immediately when the budget is set.
103    ///
104    /// Returns a quantity-conversion or budget error without changing the
105    /// configured output budget on failure. An absent budget is ignored.
106    ///
107    /// # Parameters
108    ///
109    /// * `amount` - Number of accepted output bytes to charge immediately.
110    ///
111    /// # Returns
112    ///
113    /// `Ok(())` when the operation completes successfully.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
118    /// or a configured limit rejects it.
119    #[inline]
120    pub fn try_consume_output_bytes(&mut self, amount: usize) -> Result<(), MeasuredBudgetError<R, Q>> {
121        match self.output.as_deref_mut() {
122            Some(budget) => budget.try_consume_usize(amount),
123            None => Ok(()),
124        }
125    }
126
127    /// Stages one JSON measurement for publication by [`Self::commit`].
128    ///
129    /// Returns the transaction's conversion or value-limit error. A failure
130    /// leaves this attempt's working value state and output charges unchanged,
131    /// and poisons later value admissions and commit. Output failures alone do
132    /// not poison the value transaction.
133    ///
134    /// # Parameters
135    ///
136    /// * `measurement` - Native JSON measurement to convert or admit.
137    ///
138    /// # Returns
139    ///
140    /// `Ok(())` when the operation completes successfully.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`MeasuredBudgetError`] when a native measurement cannot fit `Q`
145    /// or a configured limit rejects it.
146    #[inline]
147    pub fn try_admit(&mut self, measurement: JsonMeasurement) -> Result<(), MeasuredBudgetError<R, Q>> {
148        self.value.try_admit(measurement)
149    }
150
151    /// Returns the output budget while the attempt exclusively owns it.
152    ///
153    /// # Returns
154    ///
155    /// Returns the output budget while the attempt exclusively owns it.
156    ///
157    /// `None` indicates that the corresponding limit or budget dimension is
158    /// unconfigured.
159    #[must_use]
160    #[inline(always)]
161    pub fn output_budget(&self) -> Option<&ResourceBudget<R, Q>> {
162        self.output.as_deref()
163    }
164
165    /// Splits this attempt into immediate output and staged value accounting.
166    ///
167    /// The output budget, when configured, records accepted bytes immediately.
168    /// The returned transaction keeps its value changes staged until this
169    /// attempt is committed. Dropping the attempt rolls back only that value
170    /// state.
171    ///
172    /// # Returns
173    ///
174    /// Splits this attempt into immediate output and staged value accounting.
175    ///
176    /// A `None` output budget indicates that output-byte accounting is
177    /// unconfigured.
178    #[must_use]
179    #[inline]
180    pub fn split_mut(&mut self) -> (Option<&mut ResourceBudget<R, Q>>, &mut JsonValueTransaction<'a, R, Q>) {
181        (self.output.as_deref_mut(), &mut self.value)
182    }
183
184    /// Returns staged node usage when the node limit is configured.
185    ///
186    /// # Returns
187    ///
188    /// Returns staged node usage when the node limit is configured.
189    ///
190    /// `None` indicates that the corresponding limit or budget dimension is
191    /// unconfigured.
192    #[must_use]
193    #[inline]
194    pub fn used_nodes(&self) -> Option<Q> {
195        self.value.used_nodes()
196    }
197
198    /// Returns staged remaining node capacity when the node limit is set.
199    ///
200    /// # Returns
201    ///
202    /// Returns staged remaining node capacity when the node limit is set.
203    ///
204    /// `None` indicates that the corresponding limit or budget dimension is
205    /// unconfigured.
206    #[must_use]
207    #[inline(always)]
208    pub const fn remaining_nodes(&self) -> Option<Q> {
209        self.value.remaining_nodes()
210    }
211
212    /// Returns staged payload usage when the payload limit is configured.
213    ///
214    /// # Returns
215    ///
216    /// Returns staged payload usage when the payload limit is configured.
217    ///
218    /// `None` indicates that the corresponding limit or budget dimension is
219    /// unconfigured.
220    #[must_use]
221    #[inline]
222    pub fn used_payload_bytes(&self) -> Option<Q> {
223        self.value.used_payload_bytes()
224    }
225
226    /// Returns staged remaining payload capacity when the payload limit is set.
227    ///
228    /// # Returns
229    ///
230    /// Returns staged remaining payload capacity when the payload limit is set.
231    ///
232    /// `None` indicates that the corresponding limit or budget dimension is
233    /// unconfigured.
234    #[must_use]
235    #[inline(always)]
236    pub const fn remaining_payload_bytes(&self) -> Option<Q> {
237        self.value.remaining_payload_bytes()
238    }
239
240    /// Returns the mutable transaction that holds this attempt's value state.
241    ///
242    /// # Returns
243    ///
244    /// Returns the mutable transaction that holds this attempt's value state.
245    #[must_use]
246    #[inline]
247    pub fn value_transaction_mut(&mut self) -> &mut JsonValueTransaction<'a, R, Q> {
248        &mut self.value
249    }
250
251    /// Publishes this attempt's staged value state without rolling back output.
252    ///
253    /// # Returns
254    ///
255    /// `Ok(())` after publishing the staged value state.
256    ///
257    /// # Errors
258    ///
259    /// Returns the first value-admission error when the attempt is poisoned.
260    pub fn commit(self) -> Result<(), MeasuredBudgetError<R, Q>> {
261        self.value.commit()
262    }
263}