use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct Decision {
allowed: bool,
info: RateLimitInfo,
}
impl Decision {
pub fn allowed(info: RateLimitInfo) -> Self {
Self {
allowed: true,
info,
}
}
pub fn denied(info: RateLimitInfo) -> Self {
Self {
allowed: false,
info,
}
}
pub fn is_allowed(&self) -> bool {
self.allowed
}
pub fn is_denied(&self) -> bool {
!self.allowed
}
pub fn info(&self) -> &RateLimitInfo {
&self.info
}
pub fn into_info(self) -> RateLimitInfo {
self.info
}
}
#[derive(Debug, Clone)]
pub struct RateLimitInfo {
pub limit: u64,
pub remaining: u64,
pub reset_at: Instant,
pub window_start: Instant,
pub retry_after: Option<Duration>,
pub algorithm: Option<&'static str>,
pub metadata: Option<DecisionMetadata>,
}
impl RateLimitInfo {
pub fn new(limit: u64, remaining: u64, reset_at: Instant, window_start: Instant) -> Self {
Self {
limit,
remaining,
reset_at,
window_start,
retry_after: None,
algorithm: None,
metadata: None,
}
}
pub fn with_retry_after(mut self, duration: Duration) -> Self {
self.retry_after = Some(duration);
self
}
pub fn with_algorithm(mut self, name: &'static str) -> Self {
self.algorithm = Some(name);
self
}
pub fn with_metadata(mut self, metadata: DecisionMetadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn time_until_reset(&self) -> Duration {
self.reset_at.saturating_duration_since(Instant::now())
}
pub fn reset_seconds(&self) -> u64 {
self.time_until_reset().as_secs()
}
pub fn to_headers(&self) -> Vec<(&'static str, String)> {
let mut headers = vec![
("X-RateLimit-Limit", self.limit.to_string()),
("X-RateLimit-Remaining", self.remaining.to_string()),
("X-RateLimit-Reset", self.reset_seconds().to_string()),
];
if let Some(retry_after) = self.retry_after {
headers.push(("Retry-After", retry_after.as_secs().to_string()));
}
if let Some(algorithm) = self.algorithm {
headers.push(("X-RateLimit-Policy", algorithm.to_string()));
}
headers
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionMetadata {
pub key: Option<String>,
pub route: Option<String>,
pub tokens_consumed: Option<f64>,
pub tokens_available: Option<f64>,
pub tat: Option<u64>,
}
impl DecisionMetadata {
pub fn new() -> Self {
Self {
key: None,
route: None,
tokens_consumed: None,
tokens_available: None,
tat: None,
}
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_route(mut self, route: impl Into<String>) -> Self {
self.route = Some(route.into());
self
}
pub fn with_tokens_consumed(mut self, tokens: f64) -> Self {
self.tokens_consumed = Some(tokens);
self
}
pub fn with_tokens_available(mut self, tokens: f64) -> Self {
self.tokens_available = Some(tokens);
self
}
pub fn with_tat(mut self, tat: u64) -> Self {
self.tat = Some(tat);
self
}
}
impl Default for DecisionMetadata {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decision_allowed() {
let info = RateLimitInfo::new(100, 99, Instant::now(), Instant::now());
let decision = Decision::allowed(info);
assert!(decision.is_allowed());
assert!(!decision.is_denied());
assert_eq!(decision.info().limit, 100);
assert_eq!(decision.info().remaining, 99);
}
#[test]
fn test_decision_denied() {
let info = RateLimitInfo::new(100, 0, Instant::now(), Instant::now())
.with_retry_after(Duration::from_secs(30));
let decision = Decision::denied(info);
assert!(decision.is_denied());
assert!(!decision.is_allowed());
assert_eq!(decision.info().remaining, 0);
assert_eq!(decision.info().retry_after, Some(Duration::from_secs(30)));
}
#[test]
fn test_rate_limit_info_headers() {
let reset = Instant::now() + Duration::from_secs(60);
let info = RateLimitInfo::new(100, 50, reset, Instant::now())
.with_algorithm("gcra")
.with_retry_after(Duration::from_secs(10));
let headers = info.to_headers();
assert!(headers.iter().any(|(k, v)| *k == "X-RateLimit-Limit" && v == "100"));
assert!(headers.iter().any(|(k, v)| *k == "X-RateLimit-Remaining" && v == "50"));
assert!(headers.iter().any(|(k, _)| *k == "X-RateLimit-Reset"));
assert!(headers.iter().any(|(k, v)| *k == "Retry-After" && v == "10"));
assert!(headers.iter().any(|(k, v)| *k == "X-RateLimit-Policy" && v == "gcra"));
}
#[test]
fn test_decision_metadata() {
let metadata = DecisionMetadata::new()
.with_key("user:123")
.with_route("/api/data")
.with_tokens_available(5.5);
assert_eq!(metadata.key, Some("user:123".into()));
assert_eq!(metadata.route, Some("/api/data".into()));
assert_eq!(metadata.tokens_available, Some(5.5));
}
}