qubit_budget/json/encode/json_encode_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 encoding operation.
9
10use super::JsonEncodeAttempt;
11use super::JsonEncodeLimits;
12use super::internal::EncodeStorage;
13use crate::json::JsonResource;
14use crate::json::JsonValueBudget;
15use crate::resource::ResourceBudget;
16use crate::resource::ResourceQuantity;
17
18/// Mutable resource accounting for one JSON encoding operation.
19///
20/// Use [`Self::from_limits`] for a session that owns budgets created from
21/// immutable limits, or [`Self::borrowing_output`] when the caller owns the
22/// output and value budgets. Accepted output bytes are charged immediately;
23/// value measurements are staged until [`JsonEncodeAttempt::commit`].
24///
25/// # Type Parameters
26///
27/// * `R` - Caller-defined resource identity retained by limits and errors.
28/// * `Q` - Exact unsigned quantity used for measurements and accounting.
29///
30/// # Examples
31///
32/// ```
33/// use qubit_budget::json::JsonEncodeLimits;
34/// use qubit_budget::json::JsonEncodeSession;
35/// use qubit_budget::json::JsonMeasurement;
36///
37/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
38/// let limits = JsonEncodeLimits::builder()
39/// .max_output_bytes(4_usize)
40/// .max_nodes(1_usize)
41/// .build();
42/// let mut session = JsonEncodeSession::from_limits(limits);
43/// let mut attempt = session.begin_value();
44/// attempt
45/// .try_consume_output_bytes(4)
46/// .expect("the output should fit");
47/// attempt
48/// .try_admit(JsonMeasurement::Null { depth: 1 })
49/// .expect("the value should fit");
50/// attempt.commit()?;
51/// assert_eq!(session.output_budget().expect("output budget").used(), 4);
52/// # Ok(()) }
53/// ```
54///
55/// An owned session owns its budgets, while resource identities may borrow
56/// caller data. The session remains bounded by the identity's lifetime.
57///
58/// ```compile_fail
59/// use qubit_budget::json::{JsonEncodeLimits, JsonEncodeSession};
60/// use qubit_budget::ResourceLimit;
61///
62/// fn invalid_session<'a>() -> JsonEncodeSession<'a, &'a str> {
63/// let label = String::from("local-output");
64/// let limits = JsonEncodeLimits::<&str, usize>::builder()
65/// .output_bytes_limit(ResourceLimit::new(label.as_str(), 2))
66/// .build();
67/// JsonEncodeSession::from_limits(limits)
68/// }
69/// ```
70#[derive(Debug)]
71pub struct JsonEncodeSession<'a, R = JsonResource, Q = usize>
72where
73 Q: ResourceQuantity,
74{
75 /// Owned or borrowed budgets backing this encode operation.
76 storage: EncodeStorage<'a, R, Q>,
77}
78
79impl<'a, R, Q> JsonEncodeSession<'a, R, Q>
80where
81 R: Clone,
82 Q: ResourceQuantity,
83{
84 /// Creates a session borrowing only a caller-owned value budget.
85 ///
86 /// # Parameters
87 ///
88 /// * `value` - Caller-owned JSON value budget to update after a successful
89 /// encode attempt.
90 ///
91 /// # Returns
92 ///
93 /// A session that borrows `value` and stages its accounting until an
94 /// encode attempt commits.
95 #[inline]
96 #[must_use]
97 pub fn borrowing_value(value: &'a mut JsonValueBudget<R, Q>) -> Self {
98 Self {
99 storage: EncodeStorage::Borrowed { output: None, value },
100 }
101 }
102
103 /// Creates a session borrowing caller-owned output and value budgets.
104 ///
105 /// # Parameters
106 ///
107 /// * `output` - Output supplied to this operation.
108 /// * `value` - Caller-owned JSON value budget to update after a successful
109 /// encode attempt.
110 ///
111 /// # Returns
112 ///
113 /// A session that charges accepted output through `output` immediately
114 /// and stages value accounting through `value` until commit.
115 #[inline]
116 #[must_use]
117 pub fn borrowing_output(output: &'a mut ResourceBudget<R, Q>, value: &'a mut JsonValueBudget<R, Q>) -> Self {
118 Self {
119 storage: EncodeStorage::Borrowed {
120 output: Some(output),
121 value,
122 },
123 }
124 }
125
126 /// Creates a session that owns budgets initialized from immutable limits.
127 ///
128 /// Resource identities in the limits may borrow caller data. The owned
129 /// budgets do not extend the lifetime of those identities.
130 ///
131 /// # Parameters
132 ///
133 /// * `limits` - Immutable limits used to initialize owned accounting
134 /// budgets.
135 ///
136 /// # Returns
137 ///
138 /// A session owning its budgets and retaining the limits' resource
139 /// identities.
140 #[inline]
141 #[must_use]
142 pub fn from_limits(limits: JsonEncodeLimits<R, Q>) -> Self {
143 let output = limits.output_bytes_limit().cloned().map(ResourceBudget::from_limit);
144 let value = JsonValueBudget::new(limits.into_value_limits());
145 Self {
146 storage: EncodeStorage::Owned { output, value },
147 }
148 }
149
150 /// Starts accounting for one complete JSON value.
151 ///
152 /// The returned attempt charges accepted output immediately, but publishes
153 /// staged JSON value accounting only after a successful `commit`. A
154 /// value-admission failure poisons commit; dropping the attempt rolls back
155 /// only the staged value state.
156 ///
157 /// # Returns
158 ///
159 /// An attempt that charges accepted output immediately and stages value
160 /// accounting until [`JsonEncodeAttempt::commit`] succeeds.
161 #[must_use]
162 pub fn begin_value(&mut self) -> JsonEncodeAttempt<'_, R, Q> {
163 let (output, value) = self.storage.split();
164 JsonEncodeAttempt::new(output, value.transaction())
165 }
166
167 /// Returns the output budget when configured.
168 ///
169 /// # Returns
170 ///
171 /// Returns the output budget when configured.
172 ///
173 /// `None` indicates that the corresponding limit or budget dimension is
174 /// unconfigured.
175 #[inline]
176 pub fn output_budget(&self) -> Option<&ResourceBudget<R, Q>> {
177 match &self.storage {
178 EncodeStorage::Owned { output, .. } => output.as_ref(),
179 EncodeStorage::Borrowed { output, .. } => output.as_deref(),
180 }
181 }
182
183 /// Returns the configured output-byte maximum.
184 ///
185 /// # Returns
186 ///
187 /// Returns the configured output-byte maximum.
188 ///
189 /// `None` indicates that the corresponding limit or budget dimension is
190 /// unconfigured.
191 #[inline]
192 pub fn max_output_bytes(&self) -> Option<Q> {
193 self.output_budget().map(ResourceBudget::limit)
194 }
195
196 /// Returns the value budget for read-only inspection.
197 ///
198 /// # Returns
199 ///
200 /// Returns the value budget for read-only inspection.
201 #[must_use]
202 #[inline]
203 pub fn value_budget(&self) -> &JsonValueBudget<R, Q> {
204 match &self.storage {
205 EncodeStorage::Owned { value, .. } => value,
206 EncodeStorage::Borrowed { value, .. } => value,
207 }
208 }
209}