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}