1use std::collections::HashMap;
7
8use crate::{
9 any_filter::AnyFilter,
10 factory::{FilterFactory, http_builtin, tcp_builtin},
11 filter::FilterError,
12};
13
14pub struct FilterRegistry {
31 factories: HashMap<String, FilterFactory>,
33}
34
35impl FilterRegistry {
36 #[must_use]
38 pub fn with_builtins() -> Self {
39 let mut factories = HashMap::new();
40 register_http_builtins(&mut factories);
41 register_tcp_builtins(&mut factories);
42 Self { factories }
43 }
44
45 pub fn register(&mut self, name: &str, factory: FilterFactory) -> Result<(), FilterError> {
65 if self.factories.contains_key(name) {
66 return Err(format!("duplicate filter name: '{name}'").into());
67 }
68 self.factories.insert(name.to_owned(), factory);
69 Ok(())
70 }
71
72 pub fn create(&self, name: &str, config: &serde_yaml::Value) -> Result<AnyFilter, FilterError> {
91 let factory = self
92 .factories
93 .get(name)
94 .ok_or_else(|| -> FilterError { format!("unknown filter type: '{name}'").into() })?;
95 factory.create(config)
96 }
97
98 pub fn available_filters(&self) -> Vec<&str> {
100 self.factories.keys().map(String::as_str).collect()
101 }
102}
103
104#[expect(clippy::too_many_lines, reason = "one line per filter, will grow")]
110fn register_http_builtins(factories: &mut HashMap<String, FilterFactory>) {
111 use crate::builtins::{
112 AccessLogFilter, CircuitBreakerFilter, CompressionFilter, CorsFilter, CredentialInjectionFilter, CsrfFilter,
113 ForwardedHeadersFilter, GrpcDetectionFilter, HeaderFilter, IpAclFilter, JsonBodyFieldFilter, JsonRpcFilter,
114 PathRewriteFilter, RateLimitFilter, RedirectFilter, RequestIdFilter, StaticResponseFilter, TimeoutFilter,
115 UrlRewriteFilter,
116 };
117
118 register_http(factories, "access_log", AccessLogFilter::from_config);
119 register_http(factories, "circuit_breaker", CircuitBreakerFilter::from_config);
120 register_http(factories, "compression", CompressionFilter::from_config);
121 register_http(factories, "cors", CorsFilter::from_config);
122 #[cfg(feature = "cpex-policy-engine")]
123 register_http(factories, "policy", crate::PolicyFilter::from_config);
124 register_http(factories, "csrf", CsrfFilter::from_config);
125 register_http(
126 factories,
127 "credential_injection",
128 CredentialInjectionFilter::from_config,
129 );
130 register_http(factories, "headers", HeaderFilter::from_config);
131 register_http(factories, "forwarded_headers", ForwardedHeadersFilter::from_config);
132 register_http(factories, "grpc_detection", GrpcDetectionFilter::from_config);
133 register_http(factories, "guardrails", crate::GuardrailsFilter::from_config);
134 register_http(factories, "ip_acl", IpAclFilter::from_config);
135 register_http(factories, "load_balancer", crate::LoadBalancerFilter::from_config);
136 register_http(factories, "path_rewrite", PathRewriteFilter::from_config);
137 register_http(factories, "rate_limit", RateLimitFilter::from_config);
138 register_http(factories, "redirect", RedirectFilter::from_config);
139 register_http(factories, "request_id", RequestIdFilter::from_config);
140 register_http(factories, "router", crate::RouterFilter::from_config);
141 register_http(factories, "static_response", StaticResponseFilter::from_config);
142 register_http(factories, "timeout", TimeoutFilter::from_config);
143 register_http(factories, "url_rewrite", UrlRewriteFilter::from_config);
144 register_http(factories, "json_body_field", JsonBodyFieldFilter::from_config);
145 register_http(factories, "json_rpc", JsonRpcFilter::from_config);
146}
147
148#[expect(clippy::type_complexity, reason = "complex function pointer")]
150fn register_http(
151 factories: &mut HashMap<String, FilterFactory>,
152 name: &str,
153 factory_fn: fn(&serde_yaml::Value) -> Result<Box<dyn crate::filter::HttpFilter>, FilterError>,
154) {
155 let prev = factories.insert(name.to_owned(), http_builtin(factory_fn));
156 debug_assert!(prev.is_none(), "duplicate built-in HTTP filter name: '{name}'");
157}
158
159fn register_tcp_builtins(factories: &mut HashMap<String, FilterFactory>) {
161 register_tcp(factories, "sni_router", crate::builtins::SniRouterFilter::from_config);
162 register_tcp(
163 factories,
164 "tcp_access_log",
165 crate::builtins::TcpAccessLogFilter::from_config,
166 );
167 register_tcp(
168 factories,
169 "tcp_load_balancer",
170 crate::builtins::TcpLoadBalancerFilter::from_config,
171 );
172}
173
174#[expect(clippy::type_complexity, reason = "complex function pointer")]
176fn register_tcp(
177 factories: &mut HashMap<String, FilterFactory>,
178 name: &str,
179 factory_fn: fn(&serde_yaml::Value) -> Result<Box<dyn crate::tcp_filter::TcpFilter>, FilterError>,
180) {
181 let prev = factories.insert(name.to_owned(), tcp_builtin(factory_fn));
182 debug_assert!(prev.is_none(), "duplicate built-in TCP filter name: '{name}'");
183}
184
185#[cfg(test)]
190#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
191#[allow(
192 clippy::unwrap_used,
193 clippy::expect_used,
194 clippy::indexing_slicing,
195 clippy::panic,
196 clippy::cognitive_complexity,
197 clippy::too_many_lines,
198 clippy::stable_sort_primitive,
199 reason = "tests"
200)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn builtins_registered() {
206 let registry = FilterRegistry::with_builtins();
207 let mut names = registry.available_filters();
208 names.sort();
209
210 assert!(names.contains(&"access_log"), "access_log should be registered");
211 assert!(
212 names.contains(&"circuit_breaker"),
213 "circuit_breaker should be registered"
214 );
215 assert!(names.contains(&"compression"), "compression should be registered");
216 assert!(names.contains(&"cors"), "cors should be registered");
217 assert!(names.contains(&"csrf"), "csrf should be registered");
218 assert!(
219 names.contains(&"credential_injection"),
220 "credential_injection should be registered"
221 );
222 assert!(
223 names.contains(&"forwarded_headers"),
224 "forwarded_headers should be registered"
225 );
226 assert!(names.contains(&"grpc_detection"), "grpc_detection should be registered");
227 assert!(names.contains(&"guardrails"), "guardrails should be registered");
228 assert!(names.contains(&"headers"), "headers should be registered");
229 assert!(names.contains(&"ip_acl"), "ip_acl should be registered");
230 assert!(names.contains(&"load_balancer"), "load_balancer should be registered");
231 assert!(names.contains(&"path_rewrite"), "path_rewrite should be registered");
232 assert!(names.contains(&"rate_limit"), "rate_limit should be registered");
233 assert!(names.contains(&"redirect"), "redirect should be registered");
234 assert!(names.contains(&"request_id"), "request_id should be registered");
235 assert!(names.contains(&"router"), "router should be registered");
236 assert!(names.contains(&"sni_router"), "sni_router should be registered");
237 assert!(
238 names.contains(&"static_response"),
239 "static_response should be registered"
240 );
241 assert!(names.contains(&"tcp_access_log"), "tcp_access_log should be registered");
242 assert!(
243 names.contains(&"tcp_load_balancer"),
244 "tcp_load_balancer should be registered"
245 );
246 assert!(names.contains(&"timeout"), "timeout should be registered");
247 assert!(names.contains(&"url_rewrite"), "url_rewrite should be registered");
248 assert!(
249 names.contains(&"json_body_field"),
250 "json_body_field should be registered"
251 );
252 assert!(names.contains(&"json_rpc"), "json_rpc should be registered");
253 #[cfg(feature = "cpex-policy-engine")]
254 assert!(names.contains(&"policy"), "policy should be registered");
255 }
256
257 #[test]
258 fn unknown_filter_errors() {
259 let registry = FilterRegistry::with_builtins();
260 match registry.create("nonexistent", &serde_yaml::Value::Null) {
261 Err(e) => assert!(
262 e.to_string().contains("unknown filter type"),
263 "error should mention unknown filter type"
264 ),
265 Ok(_) => panic!("expected error for unknown filter type"),
266 }
267 }
268
269 #[test]
270 fn register_custom_filter_succeeds() {
271 let mut registry = FilterRegistry::with_builtins();
272 let factory = FilterFactory::Http(std::sync::Arc::new(|_| Err("unused".into())));
273 assert!(
274 registry.register("my_custom", factory).is_ok(),
275 "registering a unique name should succeed"
276 );
277 assert!(
278 registry.available_filters().contains(&"my_custom"),
279 "custom filter should appear in available filters"
280 );
281 }
282
283 #[test]
284 fn register_duplicate_builtin_errors() {
285 let mut registry = FilterRegistry::with_builtins();
286 let factory = FilterFactory::Http(std::sync::Arc::new(|_| Err("unused".into())));
287 let err = registry.register("router", factory).unwrap_err();
288 assert!(
289 err.to_string().contains("duplicate filter name: 'router'"),
290 "error should name the duplicate: {err}"
291 );
292 }
293
294 #[test]
295 fn register_duplicate_custom_errors() {
296 let mut registry = FilterRegistry::with_builtins();
297 let factory_a = FilterFactory::Http(std::sync::Arc::new(|_| Err("a".into())));
298 let factory_b = FilterFactory::Http(std::sync::Arc::new(|_| Err("b".into())));
299 registry.register("my_filter", factory_a).unwrap();
300 let err = registry.register("my_filter", factory_b).unwrap_err();
301 assert!(
302 err.to_string().contains("duplicate filter name: 'my_filter'"),
303 "error should name the duplicate: {err}"
304 );
305 }
306}