1use std::time::Duration;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[non_exhaustive]
15pub enum Denial {
16 QuotaExceeded {
18 limit: u64,
20 retry_after: Duration,
22 },
23 StorageCapacity {
25 retry_after: Option<Duration>,
27 },
28}
29
30impl Denial {
31 pub const fn retry_after(&self) -> Option<Duration> {
34 match self {
35 Self::QuotaExceeded { retry_after, .. } => Some(*retry_after),
36 Self::StorageCapacity { retry_after } => *retry_after,
37 }
38 }
39
40 pub const fn retry_after_seconds(&self) -> Option<u64> {
46 match self.retry_after() {
47 Some(duration) => Some(ceil_seconds(duration)),
48 None => None,
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54struct Allowance {
55 limit: u64,
56 remaining: u64,
57 reset_after: Duration,
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61enum Outcome {
62 Allowed(Allowance),
63 Denied(Denial),
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct Decision {
73 outcome: Outcome,
74}
75
76impl Decision {
77 pub const fn allowed(limit: u64, remaining: u64, reset_after: Duration) -> Self {
83 Self {
84 outcome: Outcome::Allowed(Allowance {
85 limit,
86 remaining,
87 reset_after,
88 }),
89 }
90 }
91
92 pub const fn denied(denial: Denial) -> Self {
94 Self {
95 outcome: Outcome::Denied(denial),
96 }
97 }
98
99 pub const fn is_allowed(&self) -> bool {
101 matches!(self.outcome, Outcome::Allowed(_))
102 }
103
104 pub const fn is_denied(&self) -> bool {
106 !self.is_allowed()
107 }
108
109 pub const fn limit(&self) -> Option<u64> {
114 match self.outcome {
115 Outcome::Allowed(allowance) => Some(allowance.limit),
116 Outcome::Denied(Denial::QuotaExceeded { limit, .. }) => Some(limit),
117 Outcome::Denied(Denial::StorageCapacity { .. }) => None,
118 }
119 }
120
121 pub const fn remaining(&self) -> Option<u64> {
123 match self.outcome {
124 Outcome::Allowed(allowance) => Some(allowance.remaining),
125 Outcome::Denied(_) => None,
126 }
127 }
128
129 pub const fn reset_after(&self) -> Option<Duration> {
132 match self.outcome {
133 Outcome::Allowed(allowance) => Some(allowance.reset_after),
134 Outcome::Denied(_) => None,
135 }
136 }
137
138 pub const fn retry_after(&self) -> Option<Duration> {
141 match self.outcome {
142 Outcome::Allowed(_) => None,
143 Outcome::Denied(denial) => denial.retry_after(),
144 }
145 }
146
147 pub const fn retry_after_seconds(&self) -> Option<u64> {
149 match self.outcome {
150 Outcome::Allowed(_) => None,
151 Outcome::Denied(denial) => denial.retry_after_seconds(),
152 }
153 }
154
155 pub const fn denial(&self) -> Option<&Denial> {
157 match &self.outcome {
158 Outcome::Allowed(_) => None,
159 Outcome::Denied(denial) => Some(denial),
160 }
161 }
162}
163
164#[derive(Clone, Debug, Eq, PartialEq)]
170pub enum BatchDecision {
171 Allowed(Vec<Decision>),
173 Denied {
175 index: usize,
177 denial: Denial,
179 },
180}
181
182impl BatchDecision {
183 pub fn try_into_single_decision(self) -> Result<Decision, Self> {
193 match self {
194 Self::Allowed(decisions) if matches!(decisions.as_slice(), [decision] if decision.is_allowed()) => {
195 Ok(decisions[0])
196 }
197 Self::Denied { index: 0, denial } => Ok(Decision::denied(denial)),
198 batch => Err(batch),
199 }
200 }
201}
202
203const fn ceil_seconds(duration: Duration) -> u64 {
204 let seconds = duration.as_secs();
205 if duration.subsec_nanos() == 0 {
206 seconds
207 } else {
208 seconds.saturating_add(1)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use std::time::Duration;
215
216 use super::{BatchDecision, Decision, Denial};
217
218 #[test]
219 fn allowed_decision_exposes_remaining_and_reset() {
220 let decision = Decision::allowed(8, 7, Duration::from_millis(59_999));
221
222 assert!(decision.is_allowed());
223 assert!(!decision.is_denied());
224 assert_eq!(decision.limit(), Some(8));
225 assert_eq!(decision.remaining(), Some(7));
226 assert_eq!(decision.reset_after(), Some(Duration::from_millis(59_999)));
227 assert_eq!(decision.retry_after(), None);
228 assert_eq!(decision.denial(), None);
229 }
230
231 #[test]
232 fn quota_denial_exposes_exact_and_ceiling_retry_duration() {
233 let denial = Denial::QuotaExceeded {
234 limit: 8,
235 retry_after: Duration::from_millis(1_001),
236 };
237 let decision = Decision::denied(denial);
238
239 assert!(decision.is_denied());
240 assert_eq!(decision.limit(), Some(8));
241 assert_eq!(decision.remaining(), None);
242 assert_eq!(decision.reset_after(), None);
243 assert_eq!(decision.retry_after(), Some(Duration::from_millis(1_001)));
244 assert_eq!(decision.retry_after_seconds(), Some(2));
245 assert_eq!(decision.denial(), Some(&denial));
246 assert!(matches!(
247 denial,
248 Denial::QuotaExceeded {
249 limit: 8,
250 retry_after
251 } if retry_after == Duration::from_millis(1_001)
252 ));
253 }
254
255 #[test]
256 fn retry_after_seconds_preserves_exact_seconds() {
257 let denial = Denial::QuotaExceeded {
258 limit: 1,
259 retry_after: Duration::from_secs(3),
260 };
261
262 assert_eq!(denial.retry_after_seconds(), Some(3));
263 }
264
265 #[test]
266 fn retry_after_seconds_saturates_without_losing_exact_duration() {
267 let duration = Duration::new(u64::MAX, 1);
268 let denial = Denial::QuotaExceeded {
269 limit: 1,
270 retry_after: duration,
271 };
272
273 assert_eq!(denial.retry_after(), Some(duration));
274 assert_eq!(denial.retry_after_seconds(), Some(u64::MAX));
275 }
276
277 #[test]
278 fn storage_capacity_retry_can_be_unknown() {
279 let denial = Denial::StorageCapacity { retry_after: None };
280 let decision = Decision::denied(denial);
281
282 assert!(matches!(
283 denial,
284 Denial::StorageCapacity { retry_after: None }
285 ));
286 assert_eq!(denial.retry_after(), None);
287 assert_eq!(denial.retry_after_seconds(), None);
288 assert_eq!(decision.limit(), None);
289 assert_eq!(decision.retry_after(), None);
290 }
291
292 #[test]
293 fn batch_of_one_converts_to_a_single_decision() {
294 let allowed = Decision::allowed(8, 7, Duration::from_secs(60));
295 let denied = Denial::QuotaExceeded {
296 limit: 8,
297 retry_after: Duration::from_secs(60),
298 };
299
300 assert_eq!(
301 BatchDecision::Allowed(vec![allowed]).try_into_single_decision(),
302 Ok(allowed)
303 );
304 assert_eq!(
305 BatchDecision::Denied {
306 index: 0,
307 denial: denied
308 }
309 .try_into_single_decision(),
310 Ok(Decision::denied(denied))
311 );
312 }
313
314 #[test]
315 fn malformed_batch_of_one_is_rejected() {
316 let decision = Decision::allowed(8, 7, Duration::from_secs(60));
317 let denial = Denial::QuotaExceeded {
318 limit: 8,
319 retry_after: Duration::from_secs(60),
320 };
321
322 assert!(
323 BatchDecision::Allowed(Vec::new())
324 .try_into_single_decision()
325 .is_err()
326 );
327 assert!(
328 BatchDecision::Allowed(vec![decision, decision])
329 .try_into_single_decision()
330 .is_err()
331 );
332 assert!(
333 BatchDecision::Allowed(vec![Decision::denied(denial)])
334 .try_into_single_decision()
335 .is_err()
336 );
337 assert!(
338 BatchDecision::Denied { index: 1, denial }
339 .try_into_single_decision()
340 .is_err()
341 );
342 }
343}