use crate::constants::MAX_COST;
use crate::error::LimiteronError;
use async_trait::async_trait;
#[cfg(feature = "distributed")]
use std::time::Duration;
pub(crate) fn validate_cost(cost: u64) -> Result<u64, LimiteronError> {
if cost == 0 {
return Err(LimiteronError::ConfigError(
"Cost cannot be zero".to_string(),
));
}
if cost > MAX_COST {
return Err(LimiteronError::ConfigError(format!(
"Cost exceeds maximum limit ({})",
MAX_COST
)));
}
Ok(cost)
}
#[async_trait]
pub trait Limiter: Send + Sync {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError>;
async fn peek(&self, cost: u64) -> Result<RateLimitSnapshot, LimiteronError> {
let _ = cost;
Err(LimiteronError::Other(
"peek is not supported by this limiter".to_string(),
))
}
async fn remaining(&self) -> Result<RateLimitSnapshot, LimiteronError> {
Err(LimiteronError::Other(
"remaining is not supported by this limiter".to_string(),
))
}
async fn check(&self, _key: &str) -> Result<(), LimiteronError> {
if self.allow(1).await? {
Ok(())
} else {
Err(LimiteronError::LimitError(
"rate limit exceeded".to_string(),
))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RateLimitSnapshot {
pub limit: u64,
pub remaining: u64,
pub reset_secs: u64,
}
impl RateLimitSnapshot {
pub fn allows(&self, cost: u64) -> bool {
self.remaining >= cost
}
pub fn headers(&self) -> [(&'static str, String); 3] {
[
("RateLimit-Limit", self.limit.to_string()),
("RateLimit-Remaining", self.remaining.to_string()),
("RateLimit-Reset", self.reset_secs.to_string()),
]
}
}
#[cfg(feature = "distributed")]
#[async_trait]
pub trait DistributedLimiter: Limiter {
async fn incr(&self, key: &str, amount: u64) -> Result<u64, LimiteronError>;
async fn incr_with_ttl(
&self,
key: &str,
amount: u64,
ttl: Duration,
) -> Result<u64, LimiteronError>;
async fn get_count(&self, key: &str) -> Result<u64, LimiteronError>;
async fn reset(&self, key: &str) -> Result<(), LimiteronError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_cost_zero() {
let result = validate_cost(0);
assert!(result.is_err());
match result {
Err(LimiteronError::ConfigError(msg)) => {
assert!(msg.contains("Cost cannot be zero"))
}
_ => panic!("expected ConfigError for zero cost"),
}
}
#[test]
fn test_validate_cost_exceeds_max() {
let result = validate_cost(crate::constants::MAX_COST + 1);
assert!(result.is_err());
match result {
Err(LimiteronError::ConfigError(msg)) => {
assert!(msg.contains("Cost exceeds maximum limit"))
}
_ => panic!("expected ConfigError for exceeding max cost"),
}
}
#[test]
fn test_validate_cost_valid() {
assert_eq!(validate_cost(1).unwrap(), 1);
assert_eq!(
validate_cost(crate::constants::MAX_COST).unwrap(),
crate::constants::MAX_COST
);
}
#[tokio::test]
async fn test_limiter_check_default_impl() {
struct AllowAllLimiter;
#[async_trait]
impl Limiter for AllowAllLimiter {
async fn allow(&self, _cost: u64) -> Result<bool, LimiteronError> {
Ok(true)
}
}
let limiter = AllowAllLimiter;
assert!(limiter.check("any_key").await.is_ok());
}
#[tokio::test]
async fn test_limiter_check_default_impl_propagates_error() {
struct ErrorLimiter;
#[async_trait]
impl Limiter for ErrorLimiter {
async fn allow(&self, _cost: u64) -> Result<bool, LimiteronError> {
Err(LimiteronError::LimitError("denied".to_string()))
}
}
let limiter = ErrorLimiter;
let result = limiter.check("any_key").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_limiter_check_default_impl_rejects_on_allow_false() {
struct DenyAllLimiter;
#[async_trait]
impl Limiter for DenyAllLimiter {
async fn allow(&self, _cost: u64) -> Result<bool, LimiteronError> {
Ok(false)
}
}
let limiter = DenyAllLimiter;
let result = limiter.check("any_key").await;
match result {
Err(LimiteronError::LimitError(msg)) => {
assert!(msg.contains("rate limit exceeded"))
}
other => panic!(
"expected Err(LimitError) for rejection, got {:?}",
other.is_ok()
),
}
}
#[tokio::test]
async fn test_t603_peek_default_impl_returns_unsupported() {
struct BareLimiter;
#[async_trait]
impl Limiter for BareLimiter {
async fn allow(&self, _cost: u64) -> Result<bool, LimiteronError> {
Ok(true)
}
}
let limiter = BareLimiter;
assert!(
limiter.peek(1).await.is_err(),
"默认 peek 应返回 Err(未支持)"
);
assert!(
limiter.remaining().await.is_err(),
"默认 remaining 应返回 Err(未支持)"
);
}
#[test]
fn test_t603_rate_limit_snapshot_headers_and_allows() {
let snapshot = RateLimitSnapshot {
limit: 100,
remaining: 42,
reset_secs: 7,
};
assert!(snapshot.allows(42), "remaining == cost 应判定可行");
assert!(snapshot.allows(1));
assert!(!snapshot.allows(43), "remaining < cost 应判定不可行");
let headers = snapshot.headers();
assert_eq!(headers[0], ("RateLimit-Limit", "100".to_string()));
assert_eq!(headers[1], ("RateLimit-Remaining", "42".to_string()));
assert_eq!(headers[2], ("RateLimit-Reset", "7".to_string()));
}
#[tokio::test]
async fn test_t603_peek_is_zero_side_effect() {
use crate::limiters::TokenBucketLimiter;
let limiter = TokenBucketLimiter::new(100, 10);
let before = limiter.peek(10).await.unwrap();
assert_eq!(before.limit, 100);
assert_eq!(before.remaining, 100, "满桶 peek 剩余应为容量");
assert!(before.allows(10));
assert!(limiter.allow(10).await.unwrap());
let after = limiter.peek(10).await.unwrap();
assert_eq!(after.remaining, 90, "peek 不得重复扣减,allow 恰好扣减一次");
assert_eq!(after.limit, 100);
assert!(after.reset_secs <= 1, "缺口 10/速率 10 → 重置 ≤1s");
}
#[tokio::test]
async fn test_t603_remaining_reflects_state() {
use crate::limiters::{FixedWindowLimiter, TokenBucketLimiter};
use std::time::Duration;
let tb = TokenBucketLimiter::new(50, 5);
Limiter::allow(&tb, 20).await.unwrap();
let snap = tb.remaining().await.unwrap();
assert_eq!(snap.remaining, 30);
assert_eq!(snap.limit, 50);
let fw = FixedWindowLimiter::new(Duration::from_secs(60), 10);
Limiter::allow(&fw, 4).await.unwrap();
let snap = fw.remaining().await.unwrap();
assert_eq!(snap.remaining, 6, "固定窗口 remaining = max - count");
assert_eq!(snap.limit, 10);
assert!(
snap.reset_secs <= 60 && snap.reset_secs > 0,
"重置时间应在窗口内"
);
}
#[tokio::test]
async fn test_t603_peek_zero_cost_is_valid() {
use crate::limiters::TokenBucketLimiter;
let limiter = TokenBucketLimiter::new(100, 10);
assert!(limiter.peek(0).await.is_err());
}
}