better_bucket/builder.rs
1//! Tier-2 builder for explicit bucket configuration.
2
3use core::time::Duration;
4
5use clock_lib::SystemClock;
6
7use crate::bucket::Bucket;
8use crate::config::BucketConfig;
9use crate::error::BucketError;
10
11/// A fluent builder for a [`Bucket`] when the Tier-1 constructors are not enough.
12///
13/// Set the capacity (the burst ceiling), the refill rate, and optionally the
14/// initial fill, then call [`build`](Self::build). Anything left unset keeps its
15/// default, and `build` validates the result through [`BucketConfig::new`], so
16/// an unworkable combination is rejected rather than producing a misbehaving
17/// bucket.
18///
19/// Capacity and burst are the same thing for a token bucket: the bucket holds at
20/// most `capacity` tokens, so the largest single acquire it can ever grant — the
21/// burst — is `capacity`.
22///
23/// For a custom time source, chain [`Bucket::with_clock`] onto the built bucket;
24/// the builder itself always produces a [`SystemClock`](clock_lib::SystemClock)
25/// bucket.
26///
27/// # Examples
28///
29/// ```
30/// use better_bucket::Bucket;
31/// use std::time::Duration;
32///
33/// // Burst up to 1000, refill 50/second, start empty.
34/// let bucket = Bucket::builder()
35/// .capacity(1000)
36/// .refill(50, Duration::from_secs(1))
37/// .initial(0)
38/// .build()?;
39///
40/// assert_eq!(bucket.capacity(), 1000);
41/// assert_eq!(bucket.available(), 0);
42/// # Ok::<(), better_bucket::BucketError>(())
43/// ```
44#[derive(Debug, Clone, Default)]
45#[must_use = "a builder does nothing until `.build()` is called"]
46pub struct BucketBuilder {
47 capacity: u32,
48 refill_amount: u32,
49 refill_period: Duration,
50 initial: Option<u32>,
51}
52
53impl BucketBuilder {
54 /// Starts a builder with every field at its default (which `build` rejects
55 /// until at least a capacity and refill rate are set).
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 /// Sets the capacity — the maximum tokens the bucket holds, and therefore
61 /// the largest burst it can grant at once. Required.
62 ///
63 /// # Examples
64 ///
65 /// ```
66 /// use better_bucket::Bucket;
67 /// use std::time::Duration;
68 ///
69 /// let bucket = Bucket::builder()
70 /// .capacity(200)
71 /// .refill(200, Duration::from_secs(1))
72 /// .build()?;
73 /// assert_eq!(bucket.capacity(), 200);
74 /// # Ok::<(), better_bucket::BucketError>(())
75 /// ```
76 pub fn capacity(mut self, capacity: u32) -> Self {
77 self.capacity = capacity;
78 self
79 }
80
81 /// Sets the sustained refill rate: `amount` tokens every `period`. Required.
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// use better_bucket::Bucket;
87 /// use std::time::Duration;
88 ///
89 /// // 10 tokens every 250ms.
90 /// let bucket = Bucket::builder()
91 /// .capacity(10)
92 /// .refill(10, Duration::from_millis(250))
93 /// .build()?;
94 /// # Ok::<(), better_bucket::BucketError>(())
95 /// ```
96 pub fn refill(mut self, amount: u32, period: Duration) -> Self {
97 self.refill_amount = amount;
98 self.refill_period = period;
99 self
100 }
101
102 /// Sets the initial number of tokens. Defaults to the capacity (the bucket
103 /// starts full); values above the capacity are clamped to it.
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use better_bucket::Bucket;
109 /// use std::time::Duration;
110 ///
111 /// let bucket = Bucket::builder()
112 /// .capacity(100)
113 /// .refill(100, Duration::from_secs(1))
114 /// .initial(0) // start empty instead of full
115 /// .build()?;
116 /// assert_eq!(bucket.available(), 0);
117 /// # Ok::<(), better_bucket::BucketError>(())
118 /// ```
119 pub fn initial(mut self, initial: u32) -> Self {
120 self.initial = Some(initial);
121 self
122 }
123
124 /// Validates the configuration and builds the bucket.
125 ///
126 /// # Errors
127 ///
128 /// Returns a [`BucketError`] for the same reasons as
129 /// [`BucketConfig::new`]: zero capacity, zero refill amount, or zero refill
130 /// period. A freshly created builder fails this way until a capacity and
131 /// refill rate are set.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// use better_bucket::{Bucket, BucketError};
137 ///
138 /// // Nothing configured yet → rejected.
139 /// let err = Bucket::builder().build().unwrap_err();
140 /// assert_eq!(err, BucketError::ZeroCapacity);
141 /// ```
142 pub fn build(self) -> Result<Bucket<SystemClock>, BucketError> {
143 let initial = self.initial.unwrap_or(self.capacity);
144 let config = BucketConfig::new(
145 self.capacity,
146 self.refill_amount,
147 self.refill_period,
148 initial,
149 )?;
150 Ok(Bucket::from_config(config))
151 }
152}
153
154impl Bucket<SystemClock> {
155 /// Starts a [`BucketBuilder`] for explicit configuration.
156 ///
157 /// The Tier-2 entry point, for when [`per_second`](Self::per_second) /
158 /// [`per_duration`](Self::per_duration) are not enough — e.g. a capacity
159 /// and refill rate that differ, or a non-full initial fill.
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// use better_bucket::Bucket;
165 /// use std::time::Duration;
166 ///
167 /// let bucket = Bucket::builder()
168 /// .capacity(500)
169 /// .refill(100, Duration::from_secs(1))
170 /// .build()?;
171 /// # Ok::<(), better_bucket::BucketError>(())
172 /// ```
173 #[must_use = "a builder does nothing until `.build()` is called"]
174 pub fn builder() -> BucketBuilder {
175 BucketBuilder::new()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 #![allow(clippy::unwrap_used)]
182
183 use super::BucketBuilder;
184 use crate::bucket::Bucket;
185 use crate::error::BucketError;
186 use core::time::Duration;
187
188 #[test]
189 fn test_builds_configured_bucket() {
190 let bucket = Bucket::builder()
191 .capacity(500)
192 .refill(100, Duration::from_secs(1))
193 .initial(0)
194 .build()
195 .unwrap();
196 assert_eq!(bucket.capacity(), 500);
197 assert_eq!(bucket.available(), 0);
198 assert_eq!(bucket.config().refill_amount(), 100);
199 }
200
201 #[test]
202 fn test_initial_defaults_to_full() {
203 let bucket = Bucket::builder()
204 .capacity(40)
205 .refill(40, Duration::from_secs(1))
206 .build()
207 .unwrap();
208 assert_eq!(bucket.available(), 40);
209 }
210
211 #[test]
212 fn test_empty_builder_is_rejected() {
213 assert_eq!(
214 BucketBuilder::new().build().unwrap_err(),
215 BucketError::ZeroCapacity
216 );
217 }
218
219 #[test]
220 fn test_missing_refill_is_rejected() {
221 let err = Bucket::builder().capacity(10).build().unwrap_err();
222 assert_eq!(err, BucketError::ZeroRefillAmount);
223 }
224
225 #[test]
226 fn test_zero_period_is_rejected() {
227 let err = Bucket::builder()
228 .capacity(10)
229 .refill(10, Duration::ZERO)
230 .build()
231 .unwrap_err();
232 assert_eq!(err, BucketError::ZeroRefillPeriod);
233 }
234}