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}