#![deny(unreachable_pub)]
mod actions;
mod any_filter;
pub mod body;
pub mod builtins;
mod condition;
mod context;
mod extensions;
mod factory;
mod filter;
pub(crate) mod load_balancing;
pub(crate) mod path_match;
mod pipeline;
mod registry;
mod results;
mod tcp_filter;
pub use actions::{FilterAction, Rejection};
pub use any_filter::AnyFilter;
pub use body::{BodyAccess, BodyBuffer, BodyBufferOverflow, BodyCapabilities, BodyMode};
#[cfg(feature = "cpex-policy-engine")]
pub use builtins::PolicyFilter;
pub use builtins::{
CircuitBreakerFilter, ContainsValue, CredentialInjectionFilter, DisallowedOriginMode, GuardrailsAction,
GuardrailsFilter, LoadBalancerFilter, PiiKind, RateLimitMode, RedirectStatus, RouterFilter, RuleTargetKind,
has_dot_dot_traversal, http::payload_processing::compression_config::CompressionConfig, normalize_rewritten_path,
};
pub use condition::{should_execute, should_execute_response, should_execute_response_ref};
pub use context::{HttpFilterContext, Request, Response};
pub use extensions::RequestExtensions;
pub use factory::{FilterFactory, HttpFilterFactory, TcpFilterFactory, http_builtin, parse_filter_config, tcp_builtin};
pub use filter::{Filter, FilterContext, FilterError, HttpFilter};
pub use pipeline::{FilterPipeline, PipelineExtension};
pub use praxis_core::config::{FailureMode, FilterEntry};
pub use registry::FilterRegistry;
pub use results::FilterResultSet;
pub use tcp_filter::{TcpFilter, TcpFilterContext};
#[macro_export]
macro_rules! register_filters {
( @register $registry:ident, http $name:expr => $factory:expr ) => {
$registry.register(
$name,
$crate::FilterFactory::Http(
::std::sync::Arc::new(move |config: &serde_yaml::Value| {
($factory)(config)
}),
),
).unwrap_or_else(|_| panic!("duplicate filter name: '{}'", $name));
};
( @register $registry:ident, tcp $name:expr => $factory:expr ) => {
$registry.register(
$name,
$crate::FilterFactory::Tcp(
::std::sync::Arc::new(move |config: &serde_yaml::Value| {
($factory)(config)
}),
),
).unwrap_or_else(|_| panic!("duplicate filter name: '{}'", $name));
};
( $( $kind:ident $name:expr => $factory:expr ),* $(,)? ) => {
pub fn custom_registry() -> $crate::FilterRegistry {
let mut registry = $crate::FilterRegistry::with_builtins();
$(
$crate::register_filters!(@register registry, $kind $name => $factory);
)*
registry
}
};
}
#[macro_export]
macro_rules! export_filters {
( $( $kind:ident $name:expr => $factory:expr ),* $(,)? ) => {
pub fn register_filters(registry: &mut $crate::FilterRegistry) {
$(
$crate::register_filters!(@register registry, $kind $name => $factory);
)*
}
};
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
unreachable_pub,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unnecessary_wraps,
reason = "internal pub items re-exported selectively; test module"
)]
mod macro_tests {
use async_trait::async_trait;
use crate::{FilterAction, FilterError, HttpFilter, HttpFilterContext, TcpFilter};
register_filters! {
http "dummy_http" => DummyHttpFilter::from_config,
tcp "dummy_tcp" => DummyTcpFilter::from_config,
}
#[test]
fn macro_registers_http_filter() {
let registry = custom_registry();
assert!(
registry.available_filters().contains(&"dummy_http"),
"registry should contain custom HTTP filter"
);
}
#[test]
fn macro_registers_tcp_filter() {
let registry = custom_registry();
assert!(
registry.available_filters().contains(&"dummy_tcp"),
"registry should contain custom TCP filter"
);
}
#[test]
fn macro_registers_http_filter_with_name_expression() {
let mut registry = crate::FilterRegistry::with_builtins();
let name = String::from("dummy_http_expr");
register_filters!(@register registry, http name.as_str() => DummyHttpFilter::from_config);
assert!(
registry.available_filters().contains(&"dummy_http_expr"),
"registry should contain custom HTTP filter registered with a name expression"
);
}
#[test]
fn macro_registers_tcp_filter_with_name_expression() {
let mut registry = crate::FilterRegistry::with_builtins();
let name = String::from("dummy_tcp_expr");
register_filters!(@register registry, tcp name.as_str() => DummyTcpFilter::from_config);
assert!(
registry.available_filters().contains(&"dummy_tcp_expr"),
"registry should contain custom TCP filter registered with a name expression"
);
}
#[test]
fn macro_preserves_builtins() {
let registry = custom_registry();
assert!(
registry.available_filters().contains(&"router"),
"registry should still contain built-in router"
);
assert!(
registry.available_filters().contains(&"load_balancer"),
"registry should still contain built-in load_balancer"
);
}
#[test]
fn macro_registered_http_filter_creates_successfully() {
let registry = custom_registry();
let result = registry.create("dummy_http", &serde_yaml::Value::Null);
assert!(result.is_ok(), "custom HTTP filter should instantiate without error");
}
#[test]
fn macro_registered_tcp_filter_creates_successfully() {
let registry = custom_registry();
let result = registry.create("dummy_tcp", &serde_yaml::Value::Null);
assert!(result.is_ok(), "custom TCP filter should instantiate without error");
}
#[test]
#[should_panic(expected = "duplicate filter name: 'router'")]
fn macro_panics_on_builtin_collision() {
let mut registry = crate::FilterRegistry::with_builtins();
register_filters!(@register registry, http "router" => DummyHttpFilter::from_config);
}
mod export_test {
use super::*;
export_filters! {
http "exported_http" => DummyHttpFilter::from_config,
tcp "exported_tcp" => DummyTcpFilter::from_config,
}
}
#[test]
fn export_filters_registers_http() {
let mut registry = crate::FilterRegistry::with_builtins();
export_test::register_filters(&mut registry);
assert!(
registry.available_filters().contains(&"exported_http"),
"exported HTTP filter should be registered"
);
}
#[test]
fn export_filters_registers_tcp() {
let mut registry = crate::FilterRegistry::with_builtins();
export_test::register_filters(&mut registry);
assert!(
registry.available_filters().contains(&"exported_tcp"),
"exported TCP filter should be registered"
);
}
#[test]
fn export_filters_preserves_builtins() {
let mut registry = crate::FilterRegistry::with_builtins();
export_test::register_filters(&mut registry);
assert!(
registry.available_filters().contains(&"router"),
"built-in router should still be registered"
);
}
#[test]
fn export_filters_creates_http_filter_successfully() {
let mut registry = crate::FilterRegistry::with_builtins();
export_test::register_filters(&mut registry);
let result = registry.create("exported_http", &serde_yaml::Value::Null);
assert!(result.is_ok(), "exported HTTP filter should instantiate without error");
}
#[test]
fn export_filters_creates_tcp_filter_successfully() {
let mut registry = crate::FilterRegistry::with_builtins();
export_test::register_filters(&mut registry);
let result = registry.create("exported_tcp", &serde_yaml::Value::Null);
assert!(result.is_ok(), "exported TCP filter should instantiate without error");
}
#[test]
#[should_panic(expected = "duplicate filter name: 'router'")]
fn export_filters_panics_on_builtin_collision() {
mod collision {
use super::*;
export_filters! {
http "router" => DummyHttpFilter::from_config,
}
}
let mut registry = crate::FilterRegistry::with_builtins();
collision::register_filters(&mut registry);
}
struct DummyHttpFilter;
#[async_trait]
impl HttpFilter for DummyHttpFilter {
fn name(&self) -> &'static str {
"dummy_http"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Continue)
}
}
impl DummyHttpFilter {
fn from_config(_: &serde_yaml::Value) -> Result<Box<dyn HttpFilter>, FilterError> {
Ok(Box::new(Self))
}
}
struct DummyTcpFilter;
#[async_trait]
impl TcpFilter for DummyTcpFilter {
fn name(&self) -> &'static str {
"dummy_tcp"
}
}
impl DummyTcpFilter {
fn from_config(_: &serde_yaml::Value) -> Result<Box<dyn TcpFilter>, FilterError> {
Ok(Box::new(Self))
}
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::expect_used, reason = "test utilities")]
pub(crate) mod test_utils {
use std::sync::LazyLock;
use http::{HeaderMap, Method, Uri};
use praxis_core::id::IdGenerator;
use crate::{HttpFilterContext, Request};
static TEST_ID_GENERATOR: LazyLock<IdGenerator> = LazyLock::new(|| IdGenerator::with_seed(0));
pub(crate) fn make_request(method: Method, path: &str) -> Request {
Request {
method,
uri: path.parse::<Uri>().expect("invalid URI in test"),
headers: HeaderMap::new(),
}
}
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::too_many_lines,
reason = "test context constructor mirrors all context fields"
)]
pub(crate) fn make_filter_context(req: &Request) -> HttpFilterContext<'_> {
HttpFilterContext {
body_done_indices: Vec::new(),
branch_iterations: std::collections::HashMap::new(),
client_addr: None,
cluster: None,
current_filter_id: None,
downstream_tls: false,
extensions: crate::extensions::RequestExtensions::default(),
executed_filter_indices: Vec::new(),
extra_request_headers: Vec::new(),
request_headers_to_remove: Vec::new(),
request_headers_to_set: Vec::new(),
filter_metadata: std::collections::HashMap::new(),
filter_results: std::collections::HashMap::new(),
filter_state: std::collections::HashMap::new(),
health_registry: None,
id_generator: &TEST_ID_GENERATOR,
kv_stores: None,
request: req,
request_body_bytes: 0,
request_body_mode: crate::body::BodyMode::Stream,
request_start: std::time::Instant::now(),
response_body_bytes: 0,
response_body_mode: crate::body::BodyMode::Stream,
response_header: None,
response_headers_modified: false,
rewritten_path: None,
selected_endpoint_index: None,
time_source: &praxis_core::time::SystemTimeSource,
upstream: None,
}
}
pub(crate) fn make_response() -> crate::context::Response {
crate::context::Response {
headers: HeaderMap::new(),
status: http::StatusCode::OK,
}
}
}