Skip to main content

praxis_filter/
registry.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Filter registry: maps filter type names to their factory functions.
5
6use std::collections::HashMap;
7
8use crate::{
9    any_filter::AnyFilter,
10    factory::{FilterFactory, http_builtin, tcp_builtin},
11    filter::FilterError,
12};
13
14// -----------------------------------------------------------------------------
15// FilterRegistry
16// -----------------------------------------------------------------------------
17
18/// Registry of available filter types.
19///
20/// ```
21/// use praxis_filter::FilterRegistry;
22///
23/// let registry = FilterRegistry::with_builtins();
24/// let mut names = registry.available_filters();
25/// names.sort();
26/// assert!(names.contains(&"load_balancer"));
27/// assert!(names.contains(&"request_id"));
28/// assert!(names.contains(&"router"));
29/// ```
30pub struct FilterRegistry {
31    /// Maps filter names to their factory functions.
32    factories: HashMap<String, FilterFactory>,
33}
34
35impl FilterRegistry {
36    /// Create a registry with only the built-in filters.
37    #[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    /// Register a custom filter factory.
46    ///
47    /// Returns an error if a filter with the same name is already registered.
48    ///
49    /// ```
50    /// use praxis_filter::{FilterFactory, FilterRegistry, http_builtin};
51    ///
52    /// let mut registry = FilterRegistry::with_builtins();
53    /// let err = registry
54    ///     .register(
55    ///         "router",
56    ///         FilterFactory::Http(std::sync::Arc::new(|_| Err("unused".into()))),
57    ///     )
58    ///     .unwrap_err();
59    /// assert!(err.to_string().contains("duplicate filter name"));
60    /// ```
61    /// # Errors
62    ///
63    /// Returns [`FilterError`] if the name is already registered.
64    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    /// Instantiate a filter by type name and config.
73    ///
74    /// ```
75    /// use praxis_filter::FilterRegistry;
76    ///
77    /// let registry = FilterRegistry::with_builtins();
78    /// let filter = registry.create("router", &serde_yaml::from_str("routes: []").unwrap());
79    /// assert!(filter.is_ok());
80    ///
81    /// let err = registry
82    ///     .create("nonexistent", &serde_yaml::Value::Null)
83    ///     .err()
84    ///     .expect("should fail for unknown type");
85    /// assert!(err.to_string().contains("unknown filter type"));
86    /// ```
87    /// # Errors
88    ///
89    /// Returns [`FilterError`] if the filter type is unknown or instantiation fails.
90    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    /// Returns the names of all registered filter types.
99    pub fn available_filters(&self) -> Vec<&str> {
100        self.factories.keys().map(String::as_str).collect()
101    }
102}
103
104// -----------------------------------------------------------------------------
105// Filter Factory - Registration
106// -----------------------------------------------------------------------------
107
108/// Register all built-in HTTP filter factories.
109#[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/// Register a single HTTP filter factory by name.
149#[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
159/// Register all built-in TCP filter factories.
160fn 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/// Register a single TCP filter factory by name.
175#[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// -----------------------------------------------------------------------------
186// Tests
187// -----------------------------------------------------------------------------
188
189#[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}