use crate::domain::rate_limiter::{ForgeError, ForgeRateLimiter};
use limiteron::{Decision, Governor, RequestContext};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub struct LimiteronForgeAdapter {
governor: Arc<Governor>,
}
impl LimiteronForgeAdapter {
#[must_use]
pub fn new(governor: Arc<Governor>) -> Self {
Self { governor }
}
}
impl ForgeRateLimiter for LimiteronForgeAdapter {
fn check<'a>(
&'a self,
key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<bool, ForgeError>> + Send + 'a>> {
Box::pin(async move {
let mut ctx = RequestContext::new();
ctx.client_ip = Some(key.to_string());
let decision = self
.governor
.check(&ctx)
.await
.map_err(ForgeError::internal)?;
match decision {
Decision::Allowed(_) => Ok(true),
Decision::Rejected(_) | Decision::Banned(_) => Ok(false),
}
})
}
fn record<'a>(
&'a self,
_key: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), ForgeError>> + Send + 'a>> {
Box::pin(async move { Ok(()) })
}
}
#[cfg(test)]
mod tests {
use super::*; use crate::domain::rate_limiter::ForgeRateLimiter;
use limiteron::config::{GlobalConfig, Matcher, Rule};
use limiteron::storage::{MemoryBanStorage, MemoryStorage};
use limiteron::{
ActionConfig, BanStorage, FlowControlConfig, Governor, LimiterConfig, Storage,
};
use std::sync::Arc;
fn make_minimal_config() -> FlowControlConfig {
FlowControlConfig {
version: "0.1.0".to_string(),
global: GlobalConfig::default(),
rules: vec![Rule {
id: "test-default".to_string(),
name: "test default rule".to_string(),
priority: 100,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["0.0.0.0/0".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 1,
refill_rate: 1,
}],
action: ActionConfig::default(),
}],
}
}
async fn make_governor() -> Governor {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
Governor::builder()
.with_config(make_minimal_config())
.with_storage(storage)
.with_ban_storage(ban_storage)
.build()
.await
.expect("minimal config must produce a valid Governor")
}
#[tokio::test]
async fn limiteron_forge_adapter_new_accepts_arc_governor() {
let governor = make_governor().await;
let _adapter = LimiteronForgeAdapter::new(Arc::new(governor));
}
#[tokio::test]
async fn check_returns_ok_true_when_allowed() {
let governor = make_governor().await;
let adapter = LimiteronForgeAdapter::new(Arc::new(governor));
let allowed = adapter
.check("1.2.3.4")
.await
.expect("check should succeed");
assert!(
allowed,
"first request must be allowed by the 100-token bucket"
);
}
#[tokio::test]
async fn record_is_noop_returning_ok() {
let governor = make_governor().await;
let adapter = LimiteronForgeAdapter::new(Arc::new(governor));
adapter
.record("1.2.3.4")
.await
.expect("record should be a no-op Ok");
}
#[tokio::test]
async fn adapter_is_object_safe_as_forge_rate_limiter() {
let governor = make_governor().await;
let adapter = LimiteronForgeAdapter::new(Arc::new(governor));
let limiter: Arc<dyn ForgeRateLimiter + Send + Sync> = Arc::new(adapter);
let allowed = limiter
.check("5.6.7.8")
.await
.expect("check via dyn dispatch");
assert!(allowed, "dyn-dispatched check must return Ok(true)");
}
#[test]
fn adapter_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<LimiteronForgeAdapter>();
}
#[tokio::test]
async fn check_returns_ok_false_when_rejected() {
let governor = Arc::new(make_governor().await);
let adapter = LimiteronForgeAdapter::new(governor.clone());
let allowed = adapter.check("9.9.9.9").await.expect("first check");
assert!(allowed, "first request must be allowed");
governor.clear_l1_cache().await;
let allowed = adapter
.check("9.9.9.9")
.await
.expect("check should not error even when rejected");
assert!(!allowed, "second request must be rejected (Ok(false))");
}
}