1#![deny(unreachable_pub)]
5
6mod actions;
9mod any_filter;
10pub mod body;
11pub mod builtins;
12mod condition;
13mod context;
14mod extensions;
15mod factory;
16mod filter;
17pub(crate) mod load_balancing;
18pub(crate) mod path_match;
19mod pipeline;
20mod registry;
21mod results;
22mod tcp_filter;
23
24pub use actions::{FilterAction, Rejection};
25pub use any_filter::AnyFilter;
26pub use body::{BodyAccess, BodyBuffer, BodyBufferOverflow, BodyCapabilities, BodyMode};
27#[cfg(feature = "cpex-policy-engine")]
28pub use builtins::PolicyFilter;
29pub use builtins::{
30 CircuitBreakerFilter, ContainsValue, CredentialInjectionFilter, DisallowedOriginMode, GuardrailsAction,
31 GuardrailsFilter, LoadBalancerFilter, PiiKind, RateLimitMode, RedirectStatus, RouterFilter, RuleTargetKind,
32 has_dot_dot_traversal, http::payload_processing::compression_config::CompressionConfig, normalize_rewritten_path,
33};
34pub use condition::{should_execute, should_execute_response, should_execute_response_ref};
35pub use context::{HttpFilterContext, Request, Response};
36pub use extensions::RequestExtensions;
37pub use factory::{FilterFactory, HttpFilterFactory, TcpFilterFactory, http_builtin, parse_filter_config, tcp_builtin};
38pub use filter::{Filter, FilterContext, FilterError, HttpFilter};
39pub use pipeline::{FilterPipeline, PipelineExtension};
40pub use praxis_core::config::{FailureMode, FilterEntry};
41pub use registry::FilterRegistry;
42pub use results::FilterResultSet;
43pub use tcp_filter::{TcpFilter, TcpFilterContext};
44
45#[macro_export]
63macro_rules! register_filters {
64 ( @register $registry:ident, http $name:expr => $factory:expr ) => {
65 $registry.register(
66 $name,
67 $crate::FilterFactory::Http(
68 ::std::sync::Arc::new(move |config: &serde_yaml::Value| {
69 ($factory)(config)
70 }),
71 ),
72 ).unwrap_or_else(|_| panic!("duplicate filter name: '{}'", $name));
73 };
74 ( @register $registry:ident, tcp $name:expr => $factory:expr ) => {
75 $registry.register(
76 $name,
77 $crate::FilterFactory::Tcp(
78 ::std::sync::Arc::new(move |config: &serde_yaml::Value| {
79 ($factory)(config)
80 }),
81 ),
82 ).unwrap_or_else(|_| panic!("duplicate filter name: '{}'", $name));
83 };
84 ( $( $kind:ident $name:expr => $factory:expr ),* $(,)? ) => {
85 pub fn custom_registry() -> $crate::FilterRegistry {
87 let mut registry = $crate::FilterRegistry::with_builtins();
88 $(
89 $crate::register_filters!(@register registry, $kind $name => $factory);
90 )*
91 registry
92 }
93 };
94}
95
96#[macro_export]
128macro_rules! export_filters {
129 ( $( $kind:ident $name:expr => $factory:expr ),* $(,)? ) => {
130 pub fn register_filters(registry: &mut $crate::FilterRegistry) {
143 $(
144 $crate::register_filters!(@register registry, $kind $name => $factory);
145 )*
146 }
147 };
148}
149
150#[cfg(test)]
155#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
156#[allow(
157 unreachable_pub,
158 clippy::unwrap_used,
159 clippy::expect_used,
160 clippy::panic,
161 clippy::unnecessary_wraps,
162 reason = "internal pub items re-exported selectively; test module"
163)]
164mod macro_tests {
165 use async_trait::async_trait;
166
167 use crate::{FilterAction, FilterError, HttpFilter, HttpFilterContext, TcpFilter};
168
169 register_filters! {
170 http "dummy_http" => DummyHttpFilter::from_config,
171 tcp "dummy_tcp" => DummyTcpFilter::from_config,
172 }
173
174 #[test]
175 fn macro_registers_http_filter() {
176 let registry = custom_registry();
177 assert!(
178 registry.available_filters().contains(&"dummy_http"),
179 "registry should contain custom HTTP filter"
180 );
181 }
182
183 #[test]
184 fn macro_registers_tcp_filter() {
185 let registry = custom_registry();
186 assert!(
187 registry.available_filters().contains(&"dummy_tcp"),
188 "registry should contain custom TCP filter"
189 );
190 }
191
192 #[test]
193 fn macro_registers_http_filter_with_name_expression() {
194 let mut registry = crate::FilterRegistry::with_builtins();
195 let name = String::from("dummy_http_expr");
196 register_filters!(@register registry, http name.as_str() => DummyHttpFilter::from_config);
197 assert!(
198 registry.available_filters().contains(&"dummy_http_expr"),
199 "registry should contain custom HTTP filter registered with a name expression"
200 );
201 }
202
203 #[test]
204 fn macro_registers_tcp_filter_with_name_expression() {
205 let mut registry = crate::FilterRegistry::with_builtins();
206 let name = String::from("dummy_tcp_expr");
207 register_filters!(@register registry, tcp name.as_str() => DummyTcpFilter::from_config);
208 assert!(
209 registry.available_filters().contains(&"dummy_tcp_expr"),
210 "registry should contain custom TCP filter registered with a name expression"
211 );
212 }
213
214 #[test]
215 fn macro_preserves_builtins() {
216 let registry = custom_registry();
217 assert!(
218 registry.available_filters().contains(&"router"),
219 "registry should still contain built-in router"
220 );
221 assert!(
222 registry.available_filters().contains(&"load_balancer"),
223 "registry should still contain built-in load_balancer"
224 );
225 }
226
227 #[test]
228 fn macro_registered_http_filter_creates_successfully() {
229 let registry = custom_registry();
230 let result = registry.create("dummy_http", &serde_yaml::Value::Null);
231 assert!(result.is_ok(), "custom HTTP filter should instantiate without error");
232 }
233
234 #[test]
235 fn macro_registered_tcp_filter_creates_successfully() {
236 let registry = custom_registry();
237 let result = registry.create("dummy_tcp", &serde_yaml::Value::Null);
238 assert!(result.is_ok(), "custom TCP filter should instantiate without error");
239 }
240
241 #[test]
242 #[should_panic(expected = "duplicate filter name: 'router'")]
243 fn macro_panics_on_builtin_collision() {
244 let mut registry = crate::FilterRegistry::with_builtins();
245 register_filters!(@register registry, http "router" => DummyHttpFilter::from_config);
246 }
247
248 mod export_test {
253 use super::*;
254
255 export_filters! {
256 http "exported_http" => DummyHttpFilter::from_config,
257 tcp "exported_tcp" => DummyTcpFilter::from_config,
258 }
259 }
260
261 #[test]
262 fn export_filters_registers_http() {
263 let mut registry = crate::FilterRegistry::with_builtins();
264 export_test::register_filters(&mut registry);
265 assert!(
266 registry.available_filters().contains(&"exported_http"),
267 "exported HTTP filter should be registered"
268 );
269 }
270
271 #[test]
272 fn export_filters_registers_tcp() {
273 let mut registry = crate::FilterRegistry::with_builtins();
274 export_test::register_filters(&mut registry);
275 assert!(
276 registry.available_filters().contains(&"exported_tcp"),
277 "exported TCP filter should be registered"
278 );
279 }
280
281 #[test]
282 fn export_filters_preserves_builtins() {
283 let mut registry = crate::FilterRegistry::with_builtins();
284 export_test::register_filters(&mut registry);
285 assert!(
286 registry.available_filters().contains(&"router"),
287 "built-in router should still be registered"
288 );
289 }
290
291 #[test]
292 fn export_filters_creates_http_filter_successfully() {
293 let mut registry = crate::FilterRegistry::with_builtins();
294 export_test::register_filters(&mut registry);
295 let result = registry.create("exported_http", &serde_yaml::Value::Null);
296 assert!(result.is_ok(), "exported HTTP filter should instantiate without error");
297 }
298
299 #[test]
300 fn export_filters_creates_tcp_filter_successfully() {
301 let mut registry = crate::FilterRegistry::with_builtins();
302 export_test::register_filters(&mut registry);
303 let result = registry.create("exported_tcp", &serde_yaml::Value::Null);
304 assert!(result.is_ok(), "exported TCP filter should instantiate without error");
305 }
306
307 #[test]
308 #[should_panic(expected = "duplicate filter name: 'router'")]
309 fn export_filters_panics_on_builtin_collision() {
310 mod collision {
311 use super::*;
312
313 export_filters! {
314 http "router" => DummyHttpFilter::from_config,
315 }
316 }
317 let mut registry = crate::FilterRegistry::with_builtins();
318 collision::register_filters(&mut registry);
319 }
320
321 struct DummyHttpFilter;
327
328 #[async_trait]
329 impl HttpFilter for DummyHttpFilter {
330 fn name(&self) -> &'static str {
331 "dummy_http"
332 }
333
334 async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
335 Ok(FilterAction::Continue)
336 }
337 }
338
339 impl DummyHttpFilter {
340 fn from_config(_: &serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError> {
341 Ok(Box::new(Self))
342 }
343 }
344
345 struct DummyTcpFilter;
347
348 #[async_trait]
349 impl TcpFilter for DummyTcpFilter {
350 fn name(&self) -> &'static str {
351 "dummy_tcp"
352 }
353 }
354
355 impl DummyTcpFilter {
356 fn from_config(_: &serde_yaml::Value) -> Result<Box<dyn TcpFilter>, FilterError> {
357 Ok(Box::new(Self))
358 }
359 }
360}
361
362#[cfg(test)]
367#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
368#[allow(clippy::expect_used, reason = "test utilities")]
369pub(crate) mod test_utils {
370 use std::sync::LazyLock;
371
372 use http::{HeaderMap, Method, Uri};
373 use praxis_core::id::IdGenerator;
374
375 use crate::{HttpFilterContext, Request};
376
377 static TEST_ID_GENERATOR: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::with_seed(0));
379
380 pub(crate) fn make_request(method: Method, path: &str) -> Request {
381 Request {
382 method,
383 uri: path.parse::<Uri>().expect("invalid URI in test"),
384 headers: HeaderMap::new(),
385 }
386 }
387
388 #[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
389 #[allow(
390 clippy::too_many_lines,
391 reason = "test context constructor mirrors all context fields"
392 )]
393 pub(crate) fn make_filter_context(req: &Request) -> HttpFilterContext<'_> {
394 HttpFilterContext {
395 body_done_indices: Vec::new(),
396 branch_iterations: std::collections::HashMap::new(),
397 client_addr: None,
398 cluster: None,
399 current_filter_id: None,
400 downstream_tls: false,
401 extensions: crate::extensions::RequestExtensions::default(),
402 executed_filter_indices: Vec::new(),
403 extra_request_headers: Vec::new(),
404 request_headers_to_remove: Vec::new(),
405 request_headers_to_set: Vec::new(),
406 filter_metadata: std::collections::HashMap::new(),
407 filter_results: std::collections::HashMap::new(),
408 filter_state: std::collections::HashMap::new(),
409 health_registry: None,
410 id_generator: &TEST_ID_GENERATOR,
411 kv_stores: None,
412 request: req,
413 request_body_bytes: 0,
414 request_body_mode: crate::body::BodyMode::Stream,
415 request_start: std::time::Instant::now(),
416 response_body_bytes: 0,
417 response_body_mode: crate::body::BodyMode::Stream,
418 response_header: None,
419 response_headers_modified: false,
420 rewritten_path: None,
421 selected_endpoint_index: None,
422 time_source: &praxis_core::time::SystemTimeSource,
423 upstream: None,
424 }
425 }
426
427 pub(crate) fn make_response() -> crate::context::Response {
429 crate::context::Response {
430 headers: HeaderMap::new(),
431 status: http::StatusCode::OK,
432 }
433 }
434}