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 check(&self, _key: &str) -> Result<(), LimiteronError> {
self.allow(1).await?;
Ok(())
}
}
#[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());
}
}