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 /// Creates a session borrowing only a caller-owned value budget.
94 #[inline]
95 #[must_use]
96 pub fn borrowing_value(value: &'a mut JsonValueBudget<R, Q>) -> Self {
97 Self {
98 storage: EncodeStorage::Borrowed { output: None, value },
99 }
100 }
101
102 /// Creates a session borrowing caller-owned output and value budgets.
103 ///
104 /// # Parameters
105 ///
106 /// * `output` - Output supplied to this operation.
107 /// * `value` - Caller-owned JSON value budget to update after a successful
108 /// encode attempt.
109 ///
110 /// # Returns
111 ///
112 /// Creates a session borrowing caller-owned output and value budgets.
113 #[inline]
114 #[must_use]
115 pub fn borrowing_output(output: &'a mut ResourceBudget<R, Q>, value: &'a mut JsonValueBudget<R, Q>) -> Self {
116 Self {
117 storage: EncodeStorage::Borrowed {
118 output: Some(output),
119 value,
120 },
121 }
122 }
123
124 /// Creates a session that owns budgets initialized from immutable limits.
125 ///
126 /// Resource identities in the limits may borrow caller data. The owned
127 /// budgets do not extend the lifetime of those identities.
128 ///
129 /// # Parameters
130 ///
131 /// * `limits` - Immutable limits used to initialize owned accounting
132 /// budgets.
133 ///
134 /// # Returns
135 ///
136 /// A session owning its budgets and retaining the limits' resource
137 /// identities.
138 #[inline]
139 #[must_use]
140 pub fn from_limits(limits: JsonEncodeLimits<R, Q>) -> Self {
141 let output = limits.output_bytes_limit().cloned().map(ResourceBudget::from_limit);
142 let value = JsonValueBudget::new(limits.into_value_limits());
143 Self {
144 storage: EncodeStorage::Owned { output, value },
145 }
146 }
147
148 /// Starts accounting for one complete JSON value.
149 ///
150 /// The returned attempt charges accepted output immediately, but publishes
151 /// staged JSON value accounting only after a successful `commit`. A
152 /// value-admission failure poisons commit; dropping the attempt rolls back
153 /// only the staged value state.
154 ///
155 /// # Returns
156 ///
157 /// Starts accounting for one complete JSON value.
158 #[must_use]
159 pub fn begin_value(&mut self) -> JsonEncodeAttempt<'_, R, Q> {
160 let (output, value) = self.storage.split();
161 JsonEncodeAttempt::new(output, value.transaction())
162 }
163
164 /// Returns the output budget when configured.
165 ///
166 /// # Returns
167 ///
168 /// Returns the output budget when configured.
169 ///
170 /// `None` indicates that the corresponding limit or budget dimension is
171 /// unconfigured.
172 #[inline]
173 pub fn output_budget(&self) -> Option<&ResourceBudget<R, Q>> {
174 match &self.storage {
175 EncodeStorage::Owned { output, .. } => output.as_ref(),
176 EncodeStorage::Borrowed { output, .. } => output.as_deref(),
177 }
178 }
179
180 /// Returns the configured output-byte maximum.
181 ///
182 /// # Returns
183 ///
184 /// Returns the configured output-byte maximum.
185 ///
186 /// `None` indicates that the corresponding limit or budget dimension is
187 /// unconfigured.
188 #[inline]
189 pub fn max_output_bytes(&self) -> Option<Q> {
190 self.output_budget().map(ResourceBudget::limit)
191 }
192
193 /// Returns the value budget for read-only inspection.
194 ///
195 /// # Returns
196 ///
197 /// Returns the value budget for read-only inspection.
198 #[must_use]
199 #[inline]
200 pub fn value_budget(&self) -> &JsonValueBudget<R, Q> {
201 match &self.storage {
202 EncodeStorage::Owned { value, .. } => value,
203 EncodeStorage::Borrowed { value, .. } => value,
204 }
205 }
206}