Skip to main content

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}