qubit_budget/structure/structure_budget.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Enforces structural limits during one processing session.
9
10use super::StructureLimits;
11use super::StructureResource;
12use crate::resource::BudgetError;
13use crate::resource::ResourceBudget;
14use crate::resource::ResourceQuantity;
15use crate::resource::check_limit;
16
17/// Mutable structural accounting for one processing session.
18///
19/// `R` and `Q` mirror [`StructureLimits`]. Point limits do not accumulate
20/// between calls; node charges consume the session's finite node budget.
21///
22/// Obtain a `StructureBudget` from [`StructureLimits::budget`] after building
23/// the limits. The budget is intended to be kept for one processing session.
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::StructureLimits;
34///
35/// let limits = StructureLimits::builder().max_nodes(2).build();
36/// let mut budget = limits.budget();
37/// budget.charge_node().expect("first node should fit");
38/// budget.charge_node().expect("second node should fit");
39/// assert_eq!(budget.used_nodes(), Some(2));
40/// assert!(budget.charge_node().is_err());
41/// ```
42#[derive(Debug, PartialEq, Eq)]
43pub struct StructureBudget<R = StructureResource, Q = usize>
44where
45 Q: ResourceQuantity,
46{
47 /// Immutable point limits for this session.
48 limits: StructureLimits<R, Q>,
49
50 /// Optional cumulative node budget for this session.
51 nodes: Option<ResourceBudget<R, Q>>,
52}
53
54impl<R, Q> StructureBudget<R, Q>
55where
56 R: Clone,
57 Q: ResourceQuantity,
58{
59 /// Creates a fresh budget session from one structural limit configuration.
60 ///
61 /// # Parameters
62 ///
63 /// * `limits` - Immutable structural limits used to initialize this
64 /// accounting session.
65 ///
66 /// # Returns
67 ///
68 /// Creates a fresh budget session from one structural limit configuration.
69 #[inline]
70 #[must_use = "the budget check result must be handled"]
71 pub(crate) fn new(limits: StructureLimits<R, Q>) -> Self {
72 Self {
73 nodes: limits.nodes_limit().cloned().map(ResourceBudget::from_limit),
74 limits,
75 }
76 }
77
78 /// Checks one nesting depth against its configured maximum.
79 ///
80 /// # Parameters
81 ///
82 /// * `actual` - Observed nesting depth to compare with the configured depth
83 /// limit.
84 ///
85 /// # Returns
86 ///
87 /// `Ok(())` when the operation completes successfully.
88 ///
89 /// # Errors
90 ///
91 /// Returns [`BudgetError::LimitExceeded`] when a configured depth limit
92 /// rejects `actual`.
93 #[inline]
94 #[must_use = "the budget check result must be handled"]
95 pub fn check_depth(&self, actual: Q) -> Result<(), BudgetError<R, Q>> {
96 check_limit(self.limits.depth_limit(), actual)
97 }
98
99 /// Charges one processed node to this session's cumulative node budget.
100 ///
101 /// # Returns
102 ///
103 /// `Ok(())` when the operation completes successfully.
104 ///
105 /// # Errors
106 ///
107 /// Returns [`BudgetError::Insufficient`] when the configured node budget
108 /// has fewer than one unit remaining.
109 #[inline]
110 pub fn charge_node(&mut self) -> Result<(), BudgetError<R, Q>> {
111 self.charge_nodes(Q::ONE)
112 }
113
114 /// Charges several processed nodes atomically.
115 ///
116 /// # Parameters
117 ///
118 /// * `amount` - Number of nodes to charge from the cumulative node budget.
119 ///
120 /// # Returns
121 ///
122 /// `Ok(())` when the operation completes successfully.
123 ///
124 /// # Errors
125 ///
126 /// Returns [`BudgetError::Insufficient`] when the configured node budget
127 /// has fewer than `amount` units remaining.
128 #[inline]
129 pub fn charge_nodes(&mut self, amount: Q) -> Result<(), BudgetError<R, Q>> {
130 match &mut self.nodes {
131 Some(nodes) => nodes.try_consume(amount).map_err(BudgetError::from),
132 None => Ok(()),
133 }
134 }
135
136 /// Checks one sequence item count against its configured maximum.
137 ///
138 /// # Parameters
139 ///
140 /// * `actual` - Observed number of direct items in the sequence.
141 ///
142 /// # Returns
143 ///
144 /// `Ok(())` when the operation completes successfully.
145 ///
146 /// # Errors
147 ///
148 /// Returns [`BudgetError::LimitExceeded`] when a configured sequence-item
149 /// limit rejects `actual`.
150 #[inline]
151 #[must_use = "the budget check result must be handled"]
152 pub fn check_sequence_items(&self, actual: Q) -> Result<(), BudgetError<R, Q>> {
153 check_limit(self.limits.sequence_items_limit(), actual)
154 }
155
156 /// Checks one map entry count against its configured maximum.
157 ///
158 /// # Parameters
159 ///
160 /// * `actual` - Observed number of direct entries in the map.
161 ///
162 /// # Returns
163 ///
164 /// `Ok(())` when the operation completes successfully.
165 ///
166 /// # Errors
167 ///
168 /// Returns [`BudgetError::LimitExceeded`] when a configured map-entry
169 /// limit rejects `actual`.
170 #[inline]
171 #[must_use = "the budget check result must be handled"]
172 pub fn check_map_entries(&self, actual: Q) -> Result<(), BudgetError<R, Q>> {
173 check_limit(self.limits.map_entries_limit(), actual)
174 }
175
176 /// Checks one structural key byte length against its configured maximum.
177 ///
178 /// # Parameters
179 ///
180 /// * `actual` - Observed UTF-8 byte length of the structural key.
181 ///
182 /// # Returns
183 ///
184 /// `Ok(())` when the operation completes successfully.
185 ///
186 /// # Errors
187 ///
188 /// Returns [`BudgetError::LimitExceeded`] when a configured key-byte limit
189 /// rejects `actual`.
190 #[inline]
191 #[must_use = "the budget check result must be handled"]
192 pub fn check_key_bytes(&self, actual: Q) -> Result<(), BudgetError<R, Q>> {
193 check_limit(self.limits.key_bytes_limit(), actual)
194 }
195
196 /// Checks a value depth and charges one node as one atomic traversal step.
197 ///
198 /// # Parameters
199 ///
200 /// * `depth` - Root-inclusive nesting depth to validate.
201 ///
202 /// # Returns
203 ///
204 /// `Ok(())` when the operation completes successfully.
205 ///
206 /// # Errors
207 ///
208 /// Returns [`BudgetError::LimitExceeded`] when the depth limit rejects
209 /// `depth`, or [`BudgetError::Insufficient`] when the node budget has no
210 /// remaining unit.
211 #[inline]
212 pub fn enter_node(&mut self, depth: Q) -> Result<(), BudgetError<R, Q>> {
213 self.check_depth(depth)?;
214 self.charge_node()
215 }
216
217 /// Checks a sequence size and charges one node as one atomic traversal
218 /// step.
219 ///
220 /// # Parameters
221 ///
222 /// * `depth` - Root-inclusive nesting depth to validate.
223 /// * `items` - Number of direct sequence or array items.
224 ///
225 /// # Returns
226 ///
227 /// `Ok(())` when the operation completes successfully.
228 ///
229 /// # Errors
230 ///
231 /// Returns [`BudgetError::LimitExceeded`] when the depth or sequence-item
232 /// limit rejects its measurement, or [`BudgetError::Insufficient`] when
233 /// the node budget has no remaining unit.
234 #[inline]
235 pub fn enter_sequence(&mut self, depth: Q, items: Q) -> Result<(), BudgetError<R, Q>> {
236 self.check_depth(depth)?;
237 self.check_sequence_items(items)?;
238 self.charge_node()
239 }
240
241 /// Checks a map size and charges one node as one atomic traversal step.
242 ///
243 /// # Parameters
244 ///
245 /// * `depth` - Root-inclusive nesting depth to validate.
246 /// * `entries` - Number of direct map or object entries.
247 ///
248 /// # Returns
249 ///
250 /// `Ok(())` when the operation completes successfully.
251 ///
252 /// # Errors
253 ///
254 /// Returns [`BudgetError::LimitExceeded`] when the depth or map-entry
255 /// limit rejects its measurement, or [`BudgetError::Insufficient`] when
256 /// the node budget has no remaining unit.
257 #[inline]
258 pub fn enter_map(&mut self, depth: Q, entries: Q) -> Result<(), BudgetError<R, Q>> {
259 self.check_depth(depth)?;
260 self.check_map_entries(entries)?;
261 self.charge_node()
262 }
263
264 /// Returns the immutable limits copied into this session.
265 ///
266 /// # Returns
267 ///
268 /// Returns the immutable limits copied into this session.
269 #[must_use]
270 #[inline(always)]
271 pub const fn limits(&self) -> &StructureLimits<R, Q> {
272 &self.limits
273 }
274
275 /// Returns whether this session has a finite node limit.
276 ///
277 /// # Returns
278 ///
279 /// `true` when the source limits configured a cumulative node maximum.
280 #[must_use]
281 #[inline(always)]
282 pub const fn has_nodes_limit(&self) -> bool {
283 self.nodes.is_some()
284 }
285
286 /// Returns the node capacity remaining in this session.
287 ///
288 /// # Returns
289 ///
290 /// The remaining capacity when a node limit is configured, or `None` for
291 /// an unconfigured node dimension.
292 #[must_use]
293 #[inline(always)]
294 pub const fn remaining_nodes(&self) -> Option<Q> {
295 match &self.nodes {
296 Some(nodes) => Some(nodes.remaining()),
297 None => None,
298 }
299 }
300
301 /// Returns the number of nodes consumed by this session when configured.
302 ///
303 /// # Returns
304 ///
305 /// `Some(used)` contains the cumulative node usage when a node limit is
306 /// configured. `None` indicates an unconfigured node dimension.
307 #[must_use]
308 #[inline(always)]
309 pub fn used_nodes(&self) -> Option<Q> {
310 self.nodes.as_ref().map(ResourceBudget::used)
311 }
312}