Skip to main content

better_bucket/
decision.rs

1//! The outcome of an acquire attempt.
2
3use core::time::Duration;
4
5/// The result of attempting to take tokens from a bucket.
6///
7/// Returned by [`Bucket::acquire`](crate::Bucket::acquire). The acquire path is
8/// infallible — there is no error case, only an allow/deny outcome — so this is
9/// a plain enum rather than a `Result`. When the request is denied, the
10/// decision carries how long the caller should wait before enough tokens will
11/// have accrued, which is exactly what a downstream limiter (e.g. `rate-net`)
12/// needs to populate a `Retry-After`.
13///
14/// `#[non_exhaustive]` so future variants can be added without breaking callers;
15/// match with a wildcard arm, or use the [`is_allowed`](Self::is_allowed) /
16/// [`retry_after`](Self::retry_after) helpers.
17///
18/// # Examples
19///
20/// ```
21/// use better_bucket::{Bucket, Decision};
22///
23/// let bucket = Bucket::per_second(1);
24/// match bucket.acquire(1) {
25///     Decision::Allowed => { /* serve the request */ }
26///     Decision::Denied { retry_after } => {
27///         // tell the caller when to come back
28///         let _ = retry_after;
29///     }
30///     _ => {}
31/// }
32/// ```
33#[must_use]
34#[non_exhaustive]
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Decision {
37    /// The tokens were granted and have been deducted from the bucket.
38    Allowed,
39    /// The request was refused because the bucket did not hold enough tokens.
40    Denied {
41        /// The minimum wait until the bucket will hold enough tokens to grant
42        /// the same request. [`Duration::MAX`] means the request can never
43        /// succeed (it asked for more tokens than the bucket's capacity).
44        retry_after: Duration,
45    },
46}
47
48impl Decision {
49    /// Returns `true` if the request was granted.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use better_bucket::Decision;
55    ///
56    /// assert!(Decision::Allowed.is_allowed());
57    /// ```
58    #[must_use]
59    pub const fn is_allowed(&self) -> bool {
60        matches!(self, Self::Allowed)
61    }
62
63    /// Returns `true` if the request was refused.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use better_bucket::Decision;
69    /// use std::time::Duration;
70    ///
71    /// let denied = Decision::Denied { retry_after: Duration::from_millis(250) };
72    /// assert!(denied.is_denied());
73    /// ```
74    #[must_use]
75    pub const fn is_denied(&self) -> bool {
76        !self.is_allowed()
77    }
78
79    /// Returns the wait until the request would succeed, or `None` if it was
80    /// allowed.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use better_bucket::Decision;
86    /// use std::time::Duration;
87    ///
88    /// let denied = Decision::Denied { retry_after: Duration::from_millis(250) };
89    /// assert_eq!(denied.retry_after(), Some(Duration::from_millis(250)));
90    /// assert_eq!(Decision::Allowed.retry_after(), None);
91    /// ```
92    #[must_use]
93    pub const fn retry_after(&self) -> Option<Duration> {
94        match self {
95            Self::Denied { retry_after } => Some(*retry_after),
96            _ => None,
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::Decision;
104    use core::time::Duration;
105
106    #[test]
107    fn test_allowed_predicates() {
108        let allowed = Decision::Allowed;
109        assert!(allowed.is_allowed());
110        assert!(!allowed.is_denied());
111        assert_eq!(allowed.retry_after(), None);
112    }
113
114    #[test]
115    fn test_denied_predicates() {
116        let denied = Decision::Denied {
117            retry_after: Duration::from_secs(2),
118        };
119        assert!(denied.is_denied());
120        assert!(!denied.is_allowed());
121        assert_eq!(denied.retry_after(), Some(Duration::from_secs(2)));
122    }
123}