use crate::config::rate_limit::{RateLimitTier, TierLimits};
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimitDecision {
pub allowed: bool,
pub limit: u32,
pub remaining: u32,
pub reset_at: u64,
pub retry_after: Option<u32>,
}
impl RateLimitDecision {
pub fn allow(limit: u32, remaining: u32, reset_at: u64) -> Self {
Self {
allowed: true,
limit,
remaining,
reset_at,
retry_after: None,
}
}
pub fn deny(limit: u32, reset_at: u64, retry_after: u32) -> Self {
Self {
allowed: false,
limit,
remaining: 0,
reset_at,
retry_after: Some(retry_after),
}
}
#[cfg(test)]
pub fn validate_invariants(&self) -> Result<(), String> {
if self.remaining > self.limit {
return Err(format!(
"remaining ({}) > limit ({})",
self.remaining, self.limit
));
}
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
if self.reset_at <= now {
return Err(format!(
"reset_at ({}) is not in the future (now: {})",
self.reset_at, now
));
}
if !self.allowed && self.retry_after.is_none() {
return Err("retry_after must be Some when allowed is false".to_string());
}
if self.allowed && self.retry_after.is_some() {
return Err("retry_after must be None when allowed is true".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuotaUsage {
pub minute: u32,
pub hour: u32,
pub day: u32,
pub month: u32,
}
impl QuotaUsage {
pub fn new() -> Self {
Self::default()
}
pub fn exceeds(&self, limits: &TierLimits) -> bool {
self.minute >= limits.requests_per_minute
|| self.hour >= limits.requests_per_hour
|| self.day >= limits.requests_per_day
}
pub fn increment(&mut self) {
self.minute += 1;
self.hour += 1;
self.day += 1;
self.month += 1;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Window {
Minute,
Hour,
Day,
Month,
}
impl Window {
pub fn duration_secs(&self) -> u64 {
match self {
Window::Minute => 60,
Window::Hour => 3600,
Window::Day => 86400,
Window::Month => 2_592_000, }
}
pub fn next_reset(&self, now: SystemTime) -> SystemTime {
let duration = std::time::Duration::from_secs(self.duration_secs());
now + duration
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rate_limit_decision_allow() {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let decision = RateLimitDecision::allow(100, 95, now + 60);
assert!(decision.allowed);
assert_eq!(decision.limit, 100);
assert_eq!(decision.remaining, 95);
assert_eq!(decision.reset_at, now + 60);
assert!(decision.retry_after.is_none());
assert!(decision.validate_invariants().is_ok());
}
#[test]
fn test_rate_limit_decision_deny() {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let decision = RateLimitDecision::deny(100, now + 60, 60);
assert!(!decision.allowed);
assert_eq!(decision.limit, 100);
assert_eq!(decision.remaining, 0);
assert_eq!(decision.reset_at, now + 60);
assert_eq!(decision.retry_after, Some(60));
assert!(decision.validate_invariants().is_ok());
}
#[test]
fn test_rate_limit_decision_invariant_remaining_le_limit() {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let invalid_decision = RateLimitDecision {
allowed: true,
limit: 100,
remaining: 150, reset_at: now + 60,
retry_after: None,
};
assert!(invalid_decision.validate_invariants().is_err());
}
#[test]
fn test_quota_usage_new() {
let usage = QuotaUsage::new();
assert_eq!(usage.minute, 0);
assert_eq!(usage.hour, 0);
assert_eq!(usage.day, 0);
assert_eq!(usage.month, 0);
}
#[test]
fn test_quota_usage_increment() {
let mut usage = QuotaUsage::new();
usage.increment();
assert_eq!(usage.minute, 1);
assert_eq!(usage.hour, 1);
assert_eq!(usage.day, 1);
assert_eq!(usage.month, 1);
}
#[test]
fn test_quota_usage_exceeds() {
use crate::config::rate_limit::TierLimits;
let limits = TierLimits {
requests_per_minute: 10,
requests_per_hour: 100,
requests_per_day: 1000,
max_concurrent: 5,
};
let mut usage = QuotaUsage::new();
assert!(!usage.exceeds(&limits));
usage.minute = 10;
assert!(usage.exceeds(&limits));
usage.minute = 5;
usage.hour = 100;
assert!(usage.exceeds(&limits));
usage.hour = 50;
usage.day = 1000;
assert!(usage.exceeds(&limits));
}
#[test]
fn test_window_duration_secs() {
assert_eq!(Window::Minute.duration_secs(), 60);
assert_eq!(Window::Hour.duration_secs(), 3600);
assert_eq!(Window::Day.duration_secs(), 86400);
assert_eq!(Window::Month.duration_secs(), 2_592_000);
}
#[test]
fn test_window_next_reset() {
let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1000);
let next_minute = Window::Minute.next_reset(now);
let expected_minute = now + std::time::Duration::from_secs(60);
assert_eq!(next_minute, expected_minute);
let next_hour = Window::Hour.next_reset(now);
let expected_hour = now + std::time::Duration::from_secs(3600);
assert_eq!(next_hour, expected_hour);
}
}