Skip to main content

qubit_budget/resource/error/
budget_error.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//! Defines the aggregate error for finite resource constraints.
9
10use std::fmt::Debug;
11
12use thiserror::Error;
13
14use super::InsufficientBudgetError;
15use super::LimitExceededError;
16use crate::resource::Observation;
17
18/// Aggregate error for APIs that can perform both point and cumulative checks.
19///
20/// APIs with a single failure mode return [`LimitExceededError`] or
21/// [`InsufficientBudgetError`] directly. This type remains the common carrier
22/// for composite operations and measured-value errors. Releasable pool release
23/// failures use the separate [`crate::ResourceReleaseError`] type.
24///
25/// # Type Parameters
26///
27/// * `R` - Caller-defined resource value retained for diagnostics.
28/// * `Q` - Copyable measurement value used by the failed constraint.
29///
30/// # Examples
31///
32/// ```
33/// use qubit_budget::BudgetError;
34/// use qubit_budget::LimitExceededError;
35///
36/// let error = BudgetError::from(LimitExceededError::exact("depth", 3_u64, 2));
37/// assert_eq!(error.configured_limit(), 2);
38/// assert_eq!(error.exact_observed(), Some(3));
39/// ```
40#[must_use]
41#[derive(Debug, Error, Clone, PartialEq, Eq)]
42pub enum BudgetError<R, Q = u64>
43where
44    Q: Copy + Debug,
45{
46    /// A point measurement exceeded its configured maximum.
47    #[error("resource {resource:?} measured {observed}, exceeding the maximum of {maximum:?}")]
48    LimitExceeded {
49        /// Resource associated with the failed point check.
50        resource: R,
51        /// Observed point measurement or safe lower bound.
52        observed: Observation<Q>,
53        /// Configured inclusive point maximum.
54        maximum: Q,
55    },
56
57    /// A cumulative consumption request exceeded the remaining capacity.
58    #[error("resource {resource:?} requested {requested:?}, but only {remaining:?} of {limit:?} remains")]
59    Insufficient {
60        /// Resource associated with the failed consumption request.
61        resource: R,
62        /// Configured finite limit.
63        limit: Q,
64        /// Capacity remaining before the failed request.
65        remaining: Q,
66        /// Quantity requested by the failed operation.
67        requested: Q,
68    },
69}
70
71impl<R, Q> BudgetError<R, Q>
72where
73    Q: Copy + Debug,
74{
75    /// Returns the resource associated with this failure.
76    ///
77    /// # Returns
78    ///
79    /// Returns the resource associated with this failure.
80    #[must_use]
81    #[inline(always)]
82    pub const fn resource(&self) -> &R {
83        match self {
84            Self::LimitExceeded { resource, .. } | Self::Insufficient { resource, .. } => resource,
85        }
86    }
87
88    /// Consumes this error and returns its associated resource.
89    ///
90    /// # Returns
91    ///
92    /// Consumes this error and returns its associated resource.
93    #[inline(always)]
94    #[must_use]
95    pub fn into_resource(self) -> R {
96        match self {
97            Self::LimitExceeded { resource, .. } | Self::Insufficient { resource, .. } => resource,
98        }
99    }
100
101    /// Returns the cumulative limit for budget and pool failures.
102    ///
103    /// Returns `Some(limit)` for [`Self::Insufficient`], or `None` for a
104    /// point-limit failure.
105    ///
106    /// # Returns
107    ///
108    /// Returns the cumulative limit for budget and pool failures.
109    #[must_use]
110    #[inline(always)]
111    pub const fn limit(&self) -> Option<Q> {
112        match self {
113            Self::LimitExceeded { .. } => None,
114            Self::Insufficient { limit, .. } => Some(*limit),
115        }
116    }
117
118    /// Returns the observation for a point-limit failure.
119    ///
120    /// Returns `Some(observed)` for [`Self::LimitExceeded`], or `None` for a
121    /// cumulative-budget or pool failure.
122    ///
123    /// # Returns
124    ///
125    /// Returns the observation for a point-limit failure.
126    #[must_use]
127    #[inline(always)]
128    pub const fn observation(&self) -> Option<Observation<Q>> {
129        match self {
130            Self::LimitExceeded { observed, .. } => Some(*observed),
131            Self::Insufficient { .. } => None,
132        }
133    }
134
135    /// Returns the exact point measurement when the observation is exact.
136    ///
137    /// # Returns
138    ///
139    /// Returns the exact point measurement when the observation is exact.
140    ///
141    /// `None` indicates that the observation is only a lower bound.
142    #[inline(always)]
143    #[must_use]
144    pub const fn exact_observed(&self) -> Option<Q> {
145        match self.observation() {
146            Some(Observation::Exact(value)) => Some(value),
147            Some(Observation::AtLeast(_)) | None => None,
148        }
149    }
150
151    /// Returns the safe lower bound of a point measurement.
152    ///
153    /// # Returns
154    ///
155    /// Returns the safe lower bound of a point measurement.
156    ///
157    /// `None` indicates that this is a cumulative-budget failure rather than a
158    /// point-limit failure.
159    #[inline(always)]
160    #[must_use]
161    pub const fn observed_lower_bound(&self) -> Option<Q> {
162        match self.observation() {
163            Some(observed) => Some(observed.lower_bound()),
164            None => None,
165        }
166    }
167
168    /// Returns the configured maximum for a point-limit failure.
169    ///
170    /// Returns `Some(maximum)` for [`Self::LimitExceeded`], or `None` for a
171    /// cumulative-budget or pool failure.
172    ///
173    /// # Returns
174    ///
175    /// Returns the configured maximum for a point-limit failure.
176    #[inline(always)]
177    #[must_use]
178    pub const fn maximum(&self) -> Option<Q> {
179        match self {
180            Self::LimitExceeded { maximum, .. } => Some(*maximum),
181            Self::Insufficient { .. } => None,
182        }
183    }
184
185    /// Returns the remaining capacity for a cumulative-budget failure.
186    ///
187    /// Returns `Some(remaining)` for [`Self::Insufficient`], including failed
188    /// pool acquisitions, or `None` for a point-limit failure.
189    ///
190    /// # Returns
191    ///
192    /// Returns the remaining capacity for a cumulative-budget failure.
193    #[inline(always)]
194    #[must_use]
195    pub const fn remaining(&self) -> Option<Q> {
196        match self {
197            Self::Insufficient { remaining, .. } => Some(*remaining),
198            Self::LimitExceeded { .. } => None,
199        }
200    }
201
202    /// Returns the requested quantity for a cumulative budget failure.
203    ///
204    /// Returns `Some(requested)` for [`Self::Insufficient`], or `None` for a
205    /// point-limit failure.
206    ///
207    /// # Returns
208    ///
209    /// Returns the requested quantity for a cumulative budget failure.
210    #[inline(always)]
211    #[must_use]
212    pub const fn requested(&self) -> Option<Q> {
213        match self {
214            Self::LimitExceeded { .. } => None,
215            Self::Insufficient { requested, .. } => Some(*requested),
216        }
217    }
218}
219
220impl<R, Q> BudgetError<R, Q>
221where
222    Q: crate::ResourceQuantity,
223{
224    /// Returns the configured limit for either point or cumulative failures.
225    ///
226    /// # Returns
227    ///
228    /// Returns the configured limit for either point or cumulative failures.
229    #[must_use]
230    #[inline]
231    pub const fn configured_limit(&self) -> Q {
232        match self {
233            Self::LimitExceeded { maximum, .. } => *maximum,
234            Self::Insufficient { limit, .. } => *limit,
235        }
236    }
237
238    /// Returns cumulative usage before a failed request, when applicable.
239    ///
240    /// # Returns
241    ///
242    /// Returns cumulative usage before a failed request, when applicable.
243    ///
244    /// `None` indicates that this is a point-limit failure with no cumulative
245    /// usage.
246    #[must_use]
247    #[inline]
248    pub fn used(&self) -> Option<Q> {
249        match self {
250            Self::LimitExceeded { .. } => None,
251            Self::Insufficient { limit, remaining, .. } => Some(*limit - *remaining),
252        }
253    }
254}
255
256impl<R, Q> From<LimitExceededError<R, Q>> for BudgetError<R, Q>
257where
258    Q: Copy + Debug,
259{
260    /// Converts a precise point-limit failure into an aggregate error.
261    ///
262    /// # Parameters
263    ///
264    /// * `error` - Precise point-limit failure to convert.
265    ///
266    /// # Returns
267    ///
268    /// Converts a precise point-limit failure into an aggregate error.
269    #[inline(always)]
270    fn from(error: LimitExceededError<R, Q>) -> Self {
271        let LimitExceededError {
272            resource,
273            observed,
274            maximum,
275        } = error;
276        Self::LimitExceeded {
277            resource,
278            observed,
279            maximum,
280        }
281    }
282}
283
284impl<R, Q> From<InsufficientBudgetError<R, Q>> for BudgetError<R, Q>
285where
286    Q: Copy + Debug,
287{
288    /// Converts a precise cumulative-budget failure into an aggregate error.
289    ///
290    /// # Parameters
291    ///
292    /// * `error` - Precise cumulative-budget failure to convert.
293    ///
294    /// # Returns
295    ///
296    /// Converts a precise cumulative-budget failure into an aggregate error.
297    #[inline(always)]
298    fn from(error: InsufficientBudgetError<R, Q>) -> Self {
299        let InsufficientBudgetError {
300            resource,
301            limit,
302            remaining,
303            requested,
304        } = error;
305        Self::Insufficient {
306            resource,
307            limit,
308            remaining,
309            requested,
310        }
311    }
312}