Skip to main content

rate_net/
error.rs

1//! Construction-time configuration errors.
2//!
3//! The check path is infallible — it returns a [`Decision`](crate::Decision),
4//! never a `Result`. The only fallible operation is describing a limit: a
5//! [`Quota`](crate::Quota) built from values that cannot describe a working
6//! limiter is rejected up front rather than producing a limiter that silently
7//! misbehaves.
8//!
9//! [`RateLimiterError`] implements [`error_forge::ForgeError`], so it slots into
10//! the portfolio error stack (kinds, captions, the central error hook) the same
11//! way every other domain error does.
12
13use core::fmt;
14
15use error_forge::ForgeError;
16
17/// A limit configuration rejected at construction time.
18///
19/// Returned by [`Quota::rate`](crate::Quota::rate) when the supplied values
20/// cannot describe a working rate limit. Each variant names exactly which
21/// constraint was violated so the caller can correct the specific field.
22///
23/// The enum is `#[non_exhaustive]`: future releases may add validation rules
24/// (and therefore variants) without it being a breaking change, so a `match` on
25/// it must include a wildcard arm.
26///
27/// # Examples
28///
29/// ```
30/// use rate_net::{Quota, RateLimiterError};
31/// use std::time::Duration;
32///
33/// let err = Quota::rate(0, Duration::from_secs(1)).unwrap_err();
34/// assert_eq!(err, RateLimiterError::ZeroQuota);
35/// ```
36#[non_exhaustive]
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum RateLimiterError {
39    /// The quota limit was zero. A limit that admits nothing per period can
40    /// never allow a request; supply a limit of at least `1`.
41    ZeroQuota,
42    /// The quota period was zero. The limit accrues *per period*, so a
43    /// zero-length period is undefined (it implies an infinite rate); supply a
44    /// non-zero [`Duration`](core::time::Duration).
45    ZeroPeriod,
46}
47
48impl fmt::Display for RateLimiterError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        let message = match self {
51            Self::ZeroQuota => "quota limit must be greater than zero",
52            Self::ZeroPeriod => "quota period must be greater than zero",
53        };
54        f.write_str(message)
55    }
56}
57
58impl std::error::Error for RateLimiterError {}
59
60impl ForgeError for RateLimiterError {
61    fn kind(&self) -> &'static str {
62        match self {
63            Self::ZeroQuota => "ZeroQuota",
64            Self::ZeroPeriod => "ZeroPeriod",
65        }
66    }
67
68    fn caption(&self) -> &'static str {
69        "Invalid rate-limit configuration"
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    #![allow(clippy::unwrap_used)]
76
77    use super::RateLimiterError;
78    use error_forge::ForgeError;
79
80    #[test]
81    fn test_display_names_the_violated_constraint() {
82        assert!(RateLimiterError::ZeroQuota.to_string().contains("limit"));
83        assert!(RateLimiterError::ZeroPeriod.to_string().contains("period"));
84    }
85
86    #[test]
87    fn test_forge_kind_matches_variant() {
88        assert_eq!(RateLimiterError::ZeroQuota.kind(), "ZeroQuota");
89        assert_eq!(RateLimiterError::ZeroPeriod.kind(), "ZeroPeriod");
90    }
91
92    #[test]
93    fn test_config_errors_are_not_retryable() {
94        // A bad configuration will not fix itself on retry.
95        assert!(!RateLimiterError::ZeroQuota.is_retryable());
96    }
97}