use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use trait_kit::prelude::*;
use limiteron::integrations::kit::LimiteronModule;
use crate::core::error::SdForgeError;
use crate::domain::rate_limiter::ForgeRateLimiter;
use crate::integrations::limiteron_adapter::LimiteronForgeAdapter;
pub struct SdforgeModule;
impl ModuleMeta for SdforgeModule {
const NAME: &'static str = "sdforge";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: OnceLock<Vec<(&'static str, TypeId)>> = OnceLock::new();
DEPS.get_or_init(|| vec![("limiteron", TypeId::of::<LimiteronModule>())])
.as_slice()
}
}
impl AsyncAutoBuilder for SdforgeModule {
type Capability = Arc<dyn ForgeRateLimiter + Send + Sync>;
type Error = SdForgeError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let governor = kit
.require::<LimiteronModule>()
.map_err(|e| SdForgeError::internal(format!("require LimiteronModule: {e}")))?;
let adapter = LimiteronForgeAdapter::new(governor);
Ok(Arc::new(adapter) as Arc<dyn ForgeRateLimiter + Send + Sync>)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use limiteron::config::{
Action, ActionConfig, FlowControlConfig, LimiterConfig, Matcher, Rule,
};
fn make_minimal_valid_config() -> FlowControlConfig {
let mut config = FlowControlConfig::default();
config.rules.push(Rule {
id: "default".to_string(),
name: "default rule".to_string(),
priority: 0,
matchers: vec![Matcher::Ip {
ip_ranges: vec!["0.0.0.0/0".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
});
config
}
#[test]
fn sdforge_module_meta_name() {
assert_eq!(SdforgeModule::NAME, "sdforge");
}
#[test]
fn sdforge_module_meta_dependencies() {
let deps = SdforgeModule::dependencies();
assert_eq!(deps.len(), 1, "SdforgeModule should depend on 1 module");
assert_eq!(deps[0].0, "limiteron", "dep name should be 'limiteron'");
assert_eq!(
deps[0].1,
TypeId::of::<LimiteronModule>(),
"dep TypeId should match LimiteronModule"
);
}
#[test]
fn sdforge_module_satisfies_async_auto_builder_bounds() {
fn assert_cap<T: Clone + Send + Sync + 'static>() {}
assert_cap::<Arc<dyn ForgeRateLimiter + Send + Sync>>();
fn assert_err<T: std::error::Error + Send + 'static>() {}
assert_err::<SdForgeError>();
}
#[tokio::test]
async fn sdforge_module_build_returns_rate_limiter() {
let mut kit = AsyncKit::new();
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>()
.expect("register LimiteronModule");
kit.register::<SdforgeModule>()
.expect("register SdforgeModule");
let kit = kit.build().await.expect("AsyncKit::build");
let limiter: Arc<dyn ForgeRateLimiter + Send + Sync> = kit
.require::<SdforgeModule>()
.expect("require SdforgeModule");
let allowed = limiter
.check("1.2.3.4")
.await
.expect("check should succeed");
assert!(allowed, "first request must be allowed");
}
#[tokio::test]
async fn sdforge_module_build_fails_without_limiteron() {
let mut kit = AsyncKit::new();
kit.set_config(make_minimal_valid_config());
kit.register::<SdforgeModule>()
.expect("register SdforgeModule");
let err = kit.build().await.expect_err("build should fail");
let msg = err.to_string();
assert!(
msg.contains("limiteron"),
"error should mention limiteron dependency, got: {msg}"
);
}
}