use std::collections::HashMap;
use crate::{
any_filter::AnyFilter,
factory::{FilterFactory, http_builtin, tcp_builtin},
filter::FilterError,
};
pub struct FilterRegistry {
factories: HashMap<String, FilterFactory>,
}
impl FilterRegistry {
#[must_use]
pub fn with_builtins() -> Self {
let mut factories = HashMap::new();
register_http_builtins(&mut factories);
register_tcp_builtins(&mut factories);
Self { factories }
}
pub fn register(&mut self, name: &str, factory: FilterFactory) -> Result<(), FilterError> {
if self.factories.contains_key(name) {
return Err(format!("duplicate filter name: '{name}'").into());
}
self.factories.insert(name.to_owned(), factory);
Ok(())
}
pub fn create(&self, name: &str, config: &serde_yaml::Value) -> Result<AnyFilter, FilterError> {
let factory = self
.factories
.get(name)
.ok_or_else(|| -> FilterError { format!("unknown filter type: '{name}'").into() })?;
factory.create(config)
}
pub fn available_filters(&self) -> Vec<&str> {
self.factories.keys().map(String::as_str).collect()
}
}
#[expect(clippy::too_many_lines, reason = "one line per filter, will grow")]
fn register_http_builtins(factories: &mut HashMap<String, FilterFactory>) {
use crate::builtins::{
AccessLogFilter, CircuitBreakerFilter, CompressionFilter, CorsFilter, CredentialInjectionFilter, CsrfFilter,
ForwardedHeadersFilter, GrpcDetectionFilter, HeaderFilter, IpAclFilter, JsonBodyFieldFilter, JsonRpcFilter,
PathRewriteFilter, RateLimitFilter, RedirectFilter, RequestIdFilter, StaticResponseFilter, TimeoutFilter,
UrlRewriteFilter,
};
register_http(factories, "access_log", AccessLogFilter::from_config);
register_http(factories, "circuit_breaker", CircuitBreakerFilter::from_config);
register_http(factories, "compression", CompressionFilter::from_config);
register_http(factories, "cors", CorsFilter::from_config);
#[cfg(feature = "cpex-policy-engine")]
register_http(factories, "policy", crate::PolicyFilter::from_config);
register_http(factories, "csrf", CsrfFilter::from_config);
register_http(
factories,
"credential_injection",
CredentialInjectionFilter::from_config,
);
register_http(factories, "headers", HeaderFilter::from_config);
register_http(factories, "forwarded_headers", ForwardedHeadersFilter::from_config);
register_http(factories, "grpc_detection", GrpcDetectionFilter::from_config);
register_http(factories, "guardrails", crate::GuardrailsFilter::from_config);
register_http(factories, "ip_acl", IpAclFilter::from_config);
register_http(factories, "load_balancer", crate::LoadBalancerFilter::from_config);
register_http(factories, "path_rewrite", PathRewriteFilter::from_config);
register_http(factories, "rate_limit", RateLimitFilter::from_config);
register_http(factories, "redirect", RedirectFilter::from_config);
register_http(factories, "request_id", RequestIdFilter::from_config);
register_http(factories, "router", crate::RouterFilter::from_config);
register_http(factories, "static_response", StaticResponseFilter::from_config);
register_http(factories, "timeout", TimeoutFilter::from_config);
register_http(factories, "url_rewrite", UrlRewriteFilter::from_config);
register_http(factories, "json_body_field", JsonBodyFieldFilter::from_config);
register_http(factories, "json_rpc", JsonRpcFilter::from_config);
}
#[expect(clippy::type_complexity, reason = "complex function pointer")]
fn register_http(
factories: &mut HashMap<String, FilterFactory>,
name: &str,
factory_fn: fn(&serde_yaml::Value) -> Result<Box<dyn crate::filter::HttpFilter>, FilterError>,
) {
let prev = factories.insert(name.to_owned(), http_builtin(factory_fn));
debug_assert!(prev.is_none(), "duplicate built-in HTTP filter name: '{name}'");
}
fn register_tcp_builtins(factories: &mut HashMap<String, FilterFactory>) {
register_tcp(factories, "sni_router", crate::builtins::SniRouterFilter::from_config);
register_tcp(
factories,
"tcp_access_log",
crate::builtins::TcpAccessLogFilter::from_config,
);
register_tcp(
factories,
"tcp_load_balancer",
crate::builtins::TcpLoadBalancerFilter::from_config,
);
}
#[expect(clippy::type_complexity, reason = "complex function pointer")]
fn register_tcp(
factories: &mut HashMap<String, FilterFactory>,
name: &str,
factory_fn: fn(&serde_yaml::Value) -> Result<Box<dyn crate::tcp_filter::TcpFilter>, FilterError>,
) {
let prev = factories.insert(name.to_owned(), tcp_builtin(factory_fn));
debug_assert!(prev.is_none(), "duplicate built-in TCP filter name: '{name}'");
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::cognitive_complexity,
clippy::too_many_lines,
clippy::stable_sort_primitive,
reason = "tests"
)]
mod tests {
use super::*;
#[test]
fn builtins_registered() {
let registry = FilterRegistry::with_builtins();
let mut names = registry.available_filters();
names.sort();
assert!(names.contains(&"access_log"), "access_log should be registered");
assert!(
names.contains(&"circuit_breaker"),
"circuit_breaker should be registered"
);
assert!(names.contains(&"compression"), "compression should be registered");
assert!(names.contains(&"cors"), "cors should be registered");
assert!(names.contains(&"csrf"), "csrf should be registered");
assert!(
names.contains(&"credential_injection"),
"credential_injection should be registered"
);
assert!(
names.contains(&"forwarded_headers"),
"forwarded_headers should be registered"
);
assert!(names.contains(&"grpc_detection"), "grpc_detection should be registered");
assert!(names.contains(&"guardrails"), "guardrails should be registered");
assert!(names.contains(&"headers"), "headers should be registered");
assert!(names.contains(&"ip_acl"), "ip_acl should be registered");
assert!(names.contains(&"load_balancer"), "load_balancer should be registered");
assert!(names.contains(&"path_rewrite"), "path_rewrite should be registered");
assert!(names.contains(&"rate_limit"), "rate_limit should be registered");
assert!(names.contains(&"redirect"), "redirect should be registered");
assert!(names.contains(&"request_id"), "request_id should be registered");
assert!(names.contains(&"router"), "router should be registered");
assert!(names.contains(&"sni_router"), "sni_router should be registered");
assert!(
names.contains(&"static_response"),
"static_response should be registered"
);
assert!(names.contains(&"tcp_access_log"), "tcp_access_log should be registered");
assert!(
names.contains(&"tcp_load_balancer"),
"tcp_load_balancer should be registered"
);
assert!(names.contains(&"timeout"), "timeout should be registered");
assert!(names.contains(&"url_rewrite"), "url_rewrite should be registered");
assert!(
names.contains(&"json_body_field"),
"json_body_field should be registered"
);
assert!(names.contains(&"json_rpc"), "json_rpc should be registered");
#[cfg(feature = "cpex-policy-engine")]
assert!(names.contains(&"policy"), "policy should be registered");
}
#[test]
fn unknown_filter_errors() {
let registry = FilterRegistry::with_builtins();
match registry.create("nonexistent", &serde_yaml::Value::Null) {
Err(e) => assert!(
e.to_string().contains("unknown filter type"),
"error should mention unknown filter type"
),
Ok(_) => panic!("expected error for unknown filter type"),
}
}
#[test]
fn register_custom_filter_succeeds() {
let mut registry = FilterRegistry::with_builtins();
let factory = FilterFactory::Http(std::sync::Arc::new(|_| Err("unused".into())));
assert!(
registry.register("my_custom", factory).is_ok(),
"registering a unique name should succeed"
);
assert!(
registry.available_filters().contains(&"my_custom"),
"custom filter should appear in available filters"
);
}
#[test]
fn register_duplicate_builtin_errors() {
let mut registry = FilterRegistry::with_builtins();
let factory = FilterFactory::Http(std::sync::Arc::new(|_| Err("unused".into())));
let err = registry.register("router", factory).unwrap_err();
assert!(
err.to_string().contains("duplicate filter name: 'router'"),
"error should name the duplicate: {err}"
);
}
#[test]
fn register_duplicate_custom_errors() {
let mut registry = FilterRegistry::with_builtins();
let factory_a = FilterFactory::Http(std::sync::Arc::new(|_| Err("a".into())));
let factory_b = FilterFactory::Http(std::sync::Arc::new(|_| Err("b".into())));
registry.register("my_filter", factory_a).unwrap();
let err = registry.register("my_filter", factory_b).unwrap_err();
assert!(
err.to_string().contains("duplicate filter name: 'my_filter'"),
"error should name the duplicate: {err}"
);
}
}