Skip to main content

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}