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}