Skip to main content

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}