1use std::time::Duration;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum Denial {
20 QuotaExceeded {
22 capacity: u64,
24 retry_after: Duration,
26 },
27 StorageCapacity {
29 retry_after: Option<Duration>,
31 },
32}
33
34impl Denial {
35 pub const fn retry_after(&self) -> Option<Duration> {
38 match self {
39 Self::QuotaExceeded { retry_after, .. } => Some(*retry_after),
40 Self::StorageCapacity { retry_after } => *retry_after,
41 }
42 }
43
44 pub const fn retry_after_seconds(&self) -> Option<u64> {
50 match self.retry_after() {
51 Some(duration) => Some(ceil_seconds(duration)),
52 None => None,
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58struct Allowance {
59 capacity: u64,
60 available: u64,
61 replenishes_after: Duration,
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65enum Outcome {
66 Allowed(Allowance),
67 Denied(Denial),
68 ShadowDenied(Denial),
69}
70
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub struct Decision {
81 outcome: Outcome,
82}
83
84impl Decision {
85 pub const fn allowed(capacity: u64, available: u64, replenishes_after: Duration) -> Self {
91 Self {
92 outcome: Outcome::Allowed(Allowance {
93 capacity,
94 available,
95 replenishes_after,
96 }),
97 }
98 }
99
100 pub const fn denied(denial: Denial) -> Self {
102 Self {
103 outcome: Outcome::Denied(denial),
104 }
105 }
106
107 pub const fn shadow_denied(denial: Denial) -> Self {
112 Self {
113 outcome: Outcome::ShadowDenied(denial),
114 }
115 }
116
117 pub const fn is_allowed(&self) -> bool {
122 self.permits_request()
123 }
124
125 pub const fn is_denied(&self) -> bool {
127 self.is_enforced_denial()
128 }
129
130 pub const fn permits_request(&self) -> bool {
132 !matches!(self.outcome, Outcome::Denied(_))
133 }
134
135 pub const fn would_deny(&self) -> bool {
137 !matches!(self.outcome, Outcome::Allowed(_))
138 }
139
140 pub const fn is_enforced_denial(&self) -> bool {
142 matches!(self.outcome, Outcome::Denied(_))
143 }
144
145 pub const fn is_shadow_denied(&self) -> bool {
147 matches!(self.outcome, Outcome::ShadowDenied(_))
148 }
149
150 pub const fn capacity(&self) -> Option<u64> {
155 match self.outcome {
156 Outcome::Allowed(allowance) => Some(allowance.capacity),
157 Outcome::Denied(Denial::QuotaExceeded { capacity, .. })
158 | Outcome::ShadowDenied(Denial::QuotaExceeded { capacity, .. }) => Some(capacity),
159 Outcome::Denied(Denial::StorageCapacity { .. })
160 | Outcome::ShadowDenied(Denial::StorageCapacity { .. }) => None,
161 }
162 }
163
164 pub const fn available(&self) -> Option<u64> {
166 match self.outcome {
167 Outcome::Allowed(allowance) => Some(allowance.available),
168 Outcome::Denied(_) | Outcome::ShadowDenied(_) => None,
169 }
170 }
171
172 pub const fn replenishes_after(&self) -> Option<Duration> {
174 match self.outcome {
175 Outcome::Allowed(allowance) => Some(allowance.replenishes_after),
176 Outcome::Denied(_) | Outcome::ShadowDenied(_) => None,
177 }
178 }
179
180 pub const fn retry_after(&self) -> Option<Duration> {
183 match self.outcome {
184 Outcome::Allowed(_) => None,
185 Outcome::Denied(denial) | Outcome::ShadowDenied(denial) => denial.retry_after(),
186 }
187 }
188
189 pub const fn retry_after_seconds(&self) -> Option<u64> {
191 match self.outcome {
192 Outcome::Allowed(_) => None,
193 Outcome::Denied(denial) | Outcome::ShadowDenied(denial) => denial.retry_after_seconds(),
194 }
195 }
196
197 pub const fn denial(&self) -> Option<&Denial> {
199 match &self.outcome {
200 Outcome::Allowed(_) => None,
201 Outcome::Denied(denial) | Outcome::ShadowDenied(denial) => Some(denial),
202 }
203 }
204
205 const fn was_consumed(&self) -> bool {
206 matches!(self.outcome, Outcome::Allowed(_))
207 }
208}
209
210#[derive(Clone, Debug, Eq, PartialEq)]
218#[non_exhaustive]
219pub enum BatchDecision {
220 Allowed(Vec<Decision>),
222 Denied {
224 index: usize,
226 denial: Denial,
228 },
229 ShadowDenied {
232 index: usize,
234 denial: Denial,
236 },
237}
238
239impl BatchDecision {
240 pub const fn permits_request(&self) -> bool {
242 !matches!(self, Self::Denied { .. })
243 }
244
245 pub const fn would_deny(&self) -> bool {
247 !matches!(self, Self::Allowed(_))
248 }
249
250 pub const fn is_enforced_denial(&self) -> bool {
252 matches!(self, Self::Denied { .. })
253 }
254
255 pub const fn is_shadow_denied(&self) -> bool {
257 matches!(self, Self::ShadowDenied { .. })
258 }
259
260 pub fn try_into_single_decision(self) -> Result<Decision, Self> {
270 match self {
271 Self::Allowed(decisions) if matches!(decisions.as_slice(), [decision] if decision.was_consumed()) => {
272 Ok(decisions[0])
273 }
274 Self::Denied { index: 0, denial } => Ok(Decision::denied(denial)),
275 Self::ShadowDenied { index: 0, denial } => Ok(Decision::shadow_denied(denial)),
276 batch => Err(batch),
277 }
278 }
279}
280
281#[cfg(feature = "serde")]
282fn validate_denial(denial: &Denial) -> Result<(), &'static str> {
283 match denial {
284 Denial::QuotaExceeded { capacity, .. } => {
285 if *capacity == 0 {
286 return Err("a quota denial capacity must be greater than zero");
287 }
288 if *capacity > crate::MAX_LIMIT {
289 return Err("a quota denial capacity exceeds the portable maximum");
290 }
291 }
292 Denial::StorageCapacity { .. } => {}
293 }
294 Ok(())
295}
296
297#[cfg(feature = "serde")]
298fn validate_decision(decision: &Decision) -> Result<(), &'static str> {
299 match decision.outcome {
300 Outcome::Allowed(allowance) => {
301 if allowance.capacity == 0 {
302 return Err("an allowed decision capacity must be greater than zero");
303 }
304 if allowance.capacity > crate::MAX_LIMIT {
305 return Err("an allowed decision capacity exceeds the portable maximum");
306 }
307 if allowance.available > allowance.capacity {
308 return Err("allowed decision available quota exceeds its capacity");
309 }
310 Ok(())
311 }
312 Outcome::Denied(denial) => validate_denial(&denial),
313 Outcome::ShadowDenied(denial) => {
314 if !matches!(denial, Denial::QuotaExceeded { .. }) {
315 return Err("only quota exhaustion can be shadowed");
316 }
317 validate_denial(&denial)
318 }
319 }
320}
321
322#[cfg(feature = "serde")]
323fn validate_batch_decision(batch: &BatchDecision) -> Result<(), &'static str> {
324 match batch {
325 BatchDecision::Allowed(decisions) => {
326 if decisions.iter().any(|decision| !decision.was_consumed()) {
327 return Err("an allowed batch can contain only consumed allowed decisions");
328 }
329 for decision in decisions {
330 validate_decision(decision)?;
331 }
332 Ok(())
333 }
334 BatchDecision::Denied { denial, .. } => validate_denial(denial),
335 BatchDecision::ShadowDenied { denial, .. } => {
336 if !matches!(denial, Denial::QuotaExceeded { .. }) {
337 return Err("only quota exhaustion can be shadowed");
338 }
339 validate_denial(denial)
340 }
341 }
342}
343
344#[cfg(feature = "serde")]
345#[derive(serde::Serialize)]
346#[serde(tag = "reason", rename_all = "snake_case")]
347enum DenialRef {
348 QuotaExceeded {
349 capacity: u64,
350 retry_after: Duration,
351 },
352 StorageCapacity {
353 retry_after: Option<Duration>,
354 },
355}
356
357#[cfg(feature = "serde")]
358#[derive(serde::Deserialize)]
359#[serde(tag = "reason", rename_all = "snake_case", deny_unknown_fields)]
360enum DenialWire {
361 QuotaExceeded {
362 capacity: u64,
363 retry_after: Duration,
364 },
365 StorageCapacity {
366 retry_after: Option<Duration>,
367 },
368}
369
370#[cfg(feature = "serde")]
371impl serde::Serialize for Denial {
372 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
373 where
374 S: serde::Serializer,
375 {
376 validate_denial(self).map_err(<S::Error as serde::ser::Error>::custom)?;
377 let wire = match *self {
378 Self::QuotaExceeded {
379 capacity,
380 retry_after,
381 } => DenialRef::QuotaExceeded {
382 capacity,
383 retry_after,
384 },
385 Self::StorageCapacity { retry_after } => DenialRef::StorageCapacity { retry_after },
386 };
387 serde::Serialize::serialize(&wire, serializer)
388 }
389}
390
391#[cfg(feature = "serde")]
392impl<'de> serde::Deserialize<'de> for Denial {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: serde::Deserializer<'de>,
396 {
397 let wire = <DenialWire as serde::Deserialize>::deserialize(deserializer)?;
398 let denial = match wire {
399 DenialWire::QuotaExceeded {
400 capacity,
401 retry_after,
402 } => Self::QuotaExceeded {
403 capacity,
404 retry_after,
405 },
406 DenialWire::StorageCapacity { retry_after } => Self::StorageCapacity { retry_after },
407 };
408 validate_denial(&denial).map_err(serde::de::Error::custom)?;
409 Ok(denial)
410 }
411}
412
413#[cfg(feature = "serde")]
414#[derive(serde::Serialize)]
415#[serde(tag = "outcome", rename_all = "snake_case")]
416enum DecisionRef<'a> {
417 Allowed {
418 capacity: u64,
419 available: u64,
420 replenishes_after: Duration,
421 },
422 Denied {
423 denial: &'a Denial,
424 },
425 ShadowDenied {
426 denial: &'a Denial,
427 },
428}
429
430#[cfg(feature = "serde")]
431#[derive(serde::Deserialize)]
432#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
433enum DecisionWire {
434 Allowed {
435 capacity: u64,
436 available: u64,
437 replenishes_after: Duration,
438 },
439 Denied {
440 denial: Denial,
441 },
442 ShadowDenied {
443 denial: Denial,
444 },
445}
446
447#[cfg(feature = "serde")]
448impl serde::Serialize for Decision {
449 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
450 where
451 S: serde::Serializer,
452 {
453 validate_decision(self).map_err(<S::Error as serde::ser::Error>::custom)?;
454 let wire = match &self.outcome {
455 Outcome::Allowed(allowance) => DecisionRef::Allowed {
456 capacity: allowance.capacity,
457 available: allowance.available,
458 replenishes_after: allowance.replenishes_after,
459 },
460 Outcome::Denied(denial) => DecisionRef::Denied { denial },
461 Outcome::ShadowDenied(denial) => DecisionRef::ShadowDenied { denial },
462 };
463 serde::Serialize::serialize(&wire, serializer)
464 }
465}
466
467#[cfg(feature = "serde")]
468impl<'de> serde::Deserialize<'de> for Decision {
469 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
470 where
471 D: serde::Deserializer<'de>,
472 {
473 let wire = <DecisionWire as serde::Deserialize>::deserialize(deserializer)?;
474 let decision = match wire {
475 DecisionWire::Allowed {
476 capacity,
477 available,
478 replenishes_after,
479 } => Self::allowed(capacity, available, replenishes_after),
480 DecisionWire::Denied { denial } => Self::denied(denial),
481 DecisionWire::ShadowDenied { denial } => Self::shadow_denied(denial),
482 };
483 validate_decision(&decision).map_err(serde::de::Error::custom)?;
484 Ok(decision)
485 }
486}
487
488#[cfg(feature = "serde")]
489#[derive(serde::Serialize)]
490#[serde(tag = "outcome", rename_all = "snake_case")]
491enum BatchDecisionRef<'a> {
492 Allowed { decisions: &'a [Decision] },
493 Denied { index: usize, denial: &'a Denial },
494 ShadowDenied { index: usize, denial: &'a Denial },
495}
496
497#[cfg(feature = "serde")]
498#[derive(serde::Deserialize)]
499#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
500enum BatchDecisionWire {
501 Allowed { decisions: Vec<Decision> },
502 Denied { index: usize, denial: Denial },
503 ShadowDenied { index: usize, denial: Denial },
504}
505
506#[cfg(feature = "serde")]
507impl serde::Serialize for BatchDecision {
508 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
509 where
510 S: serde::Serializer,
511 {
512 validate_batch_decision(self).map_err(<S::Error as serde::ser::Error>::custom)?;
513 let wire = match self {
514 Self::Allowed(decisions) => BatchDecisionRef::Allowed { decisions },
515 Self::Denied { index, denial } => BatchDecisionRef::Denied {
516 index: *index,
517 denial,
518 },
519 Self::ShadowDenied { index, denial } => BatchDecisionRef::ShadowDenied {
520 index: *index,
521 denial,
522 },
523 };
524 serde::Serialize::serialize(&wire, serializer)
525 }
526}
527
528#[cfg(feature = "serde")]
529impl<'de> serde::Deserialize<'de> for BatchDecision {
530 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
531 where
532 D: serde::Deserializer<'de>,
533 {
534 let wire = <BatchDecisionWire as serde::Deserialize>::deserialize(deserializer)?;
535 let batch = match wire {
536 BatchDecisionWire::Allowed { decisions } => Self::Allowed(decisions),
537 BatchDecisionWire::Denied { index, denial } => Self::Denied { index, denial },
538 BatchDecisionWire::ShadowDenied { index, denial } => {
539 Self::ShadowDenied { index, denial }
540 }
541 };
542 validate_batch_decision(&batch).map_err(serde::de::Error::custom)?;
543 Ok(batch)
544 }
545}
546
547const fn ceil_seconds(duration: Duration) -> u64 {
548 let seconds = duration.as_secs();
549 if duration.subsec_nanos() == 0 {
550 seconds
551 } else {
552 seconds.saturating_add(1)
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use std::time::Duration;
559
560 use super::{BatchDecision, Decision, Denial};
561
562 #[test]
563 fn allowed_decision_exposes_available_and_replenishment() {
564 let decision = Decision::allowed(8, 7, Duration::from_millis(59_999));
565
566 assert!(decision.is_allowed());
567 assert!(!decision.is_denied());
568 assert_eq!(decision.capacity(), Some(8));
569 assert_eq!(decision.available(), Some(7));
570 assert_eq!(
571 decision.replenishes_after(),
572 Some(Duration::from_millis(59_999))
573 );
574 assert_eq!(decision.retry_after(), None);
575 assert_eq!(decision.denial(), None);
576 }
577
578 #[test]
579 fn quota_denial_exposes_exact_and_ceiling_retry_duration() {
580 let denial = Denial::QuotaExceeded {
581 capacity: 8,
582 retry_after: Duration::from_millis(1_001),
583 };
584 let decision = Decision::denied(denial);
585
586 assert!(decision.is_denied());
587 assert_eq!(decision.capacity(), Some(8));
588 assert_eq!(decision.available(), None);
589 assert_eq!(decision.replenishes_after(), None);
590 assert_eq!(decision.retry_after(), Some(Duration::from_millis(1_001)));
591 assert_eq!(decision.retry_after_seconds(), Some(2));
592 assert_eq!(decision.denial(), Some(&denial));
593 assert!(matches!(
594 denial,
595 Denial::QuotaExceeded {
596 capacity: 8,
597 retry_after
598 } if retry_after == Duration::from_millis(1_001)
599 ));
600 }
601
602 #[test]
603 fn retry_after_seconds_preserves_exact_seconds() {
604 let denial = Denial::QuotaExceeded {
605 capacity: 1,
606 retry_after: Duration::from_secs(3),
607 };
608
609 assert_eq!(denial.retry_after_seconds(), Some(3));
610 }
611
612 #[test]
613 fn retry_after_seconds_saturates_without_losing_exact_duration() {
614 let duration = Duration::new(u64::MAX, 1);
615 let denial = Denial::QuotaExceeded {
616 capacity: 1,
617 retry_after: duration,
618 };
619
620 assert_eq!(denial.retry_after(), Some(duration));
621 assert_eq!(denial.retry_after_seconds(), Some(u64::MAX));
622 }
623
624 #[test]
625 fn storage_capacity_retry_can_be_unknown() {
626 let denial = Denial::StorageCapacity { retry_after: None };
627 let decision = Decision::denied(denial);
628
629 assert!(matches!(
630 denial,
631 Denial::StorageCapacity { retry_after: None }
632 ));
633 assert_eq!(denial.retry_after(), None);
634 assert_eq!(denial.retry_after_seconds(), None);
635 assert_eq!(decision.capacity(), None);
636 assert_eq!(decision.retry_after(), None);
637 }
638
639 #[test]
640 fn batch_of_one_converts_to_a_single_decision() {
641 let allowed = Decision::allowed(8, 7, Duration::from_secs(60));
642 let denied = Denial::QuotaExceeded {
643 capacity: 8,
644 retry_after: Duration::from_secs(60),
645 };
646
647 assert_eq!(
648 BatchDecision::Allowed(vec![allowed]).try_into_single_decision(),
649 Ok(allowed)
650 );
651 assert_eq!(
652 BatchDecision::Denied {
653 index: 0,
654 denial: denied
655 }
656 .try_into_single_decision(),
657 Ok(Decision::denied(denied))
658 );
659 }
660
661 #[test]
662 fn malformed_batch_of_one_is_rejected() {
663 let decision = Decision::allowed(8, 7, Duration::from_secs(60));
664 let denial = Denial::QuotaExceeded {
665 capacity: 8,
666 retry_after: Duration::from_secs(60),
667 };
668
669 assert!(
670 BatchDecision::Allowed(Vec::new())
671 .try_into_single_decision()
672 .is_err()
673 );
674 assert!(
675 BatchDecision::Allowed(vec![decision, decision])
676 .try_into_single_decision()
677 .is_err()
678 );
679 assert!(
680 BatchDecision::Allowed(vec![Decision::denied(denial)])
681 .try_into_single_decision()
682 .is_err()
683 );
684 assert!(
685 BatchDecision::Denied { index: 1, denial }
686 .try_into_single_decision()
687 .is_err()
688 );
689 }
690
691 #[test]
692 fn shadow_denial_permits_the_request_without_claiming_consumption() {
693 let denial = Denial::QuotaExceeded {
694 capacity: 8,
695 retry_after: Duration::from_secs(30),
696 };
697 let decision = Decision::shadow_denied(denial);
698
699 assert!(decision.is_allowed());
700 assert!(!decision.is_denied());
701 assert!(decision.permits_request());
702 assert!(decision.would_deny());
703 assert!(decision.is_shadow_denied());
704 assert_eq!(decision.available(), None);
705 assert_eq!(decision.retry_after(), Some(Duration::from_secs(30)));
706 assert_eq!(
707 BatchDecision::ShadowDenied { index: 0, denial }.try_into_single_decision(),
708 Ok(decision)
709 );
710 }
711}