use std::{future::Future, pin::Pin, sync::Arc};
use tracing::{debug, trace, warn};
use super::{
branch::{BranchOutcome, RejoinTarget, ResolvedBranch},
check_failure_mode,
filter::PipelineFilter,
};
use crate::{
FilterError, actions::FilterAction, any_filter::AnyFilter, condition::should_execute, context::HttpFilterContext,
};
pub(crate) fn evaluate_branches<'a>(
branches: &'a [ResolvedBranch],
ctx: &'a mut HttpFilterContext<'_>,
) -> Pin<Box<dyn Future<Output = Result<BranchOutcome, FilterError>> + Send + 'a>> {
Box::pin(evaluate_branches_inner(branches, ctx))
}
async fn evaluate_branches_inner(
branches: &[ResolvedBranch],
ctx: &mut HttpFilterContext<'_>,
) -> Result<BranchOutcome, FilterError> {
for branch in branches {
if !should_branch_fire(branch, ctx) {
trace!(branch = %branch.name, "branch condition not met");
continue;
}
if !check_reentrance_limit(branch, ctx) {
continue;
}
debug!(
branch = %branch.name,
"executing branch chain"
);
let action = execute_branch_filters(&branch.filters, ctx).await?;
if let FilterAction::Reject(r) = action {
return Ok(BranchOutcome::Reject(r));
}
match &branch.rejoin {
RejoinTarget::Next => {},
RejoinTarget::Terminal => return Ok(BranchOutcome::Terminal),
RejoinTarget::SkipTo(target) => return Ok(BranchOutcome::SkipTo(*target)),
RejoinTarget::ReEnter(target) => {
ctx.filter_results.clear();
return Ok(BranchOutcome::ReEnter(*target));
},
}
}
ctx.filter_results.clear();
Ok(BranchOutcome::Continue)
}
fn should_branch_fire(branch: &ResolvedBranch, ctx: &HttpFilterContext<'_>) -> bool {
match &branch.condition {
None => true,
Some(cond) => ctx
.filter_results
.get(cond.filter_name.as_ref())
.is_some_and(|rs| rs.matches(cond.key.as_ref(), cond.value.as_ref())),
}
}
fn check_reentrance_limit(branch: &ResolvedBranch, ctx: &mut HttpFilterContext<'_>) -> bool {
if let RejoinTarget::ReEnter(_) = branch.rejoin {
let count = ctx.branch_iterations.entry(Arc::clone(&branch.name)).or_insert(0);
*count += 1;
if let Some(max) = branch.max_iterations
&& *count > max
{
debug!(
branch = %branch.name,
iterations = *count,
"max iterations exceeded, falling through"
);
return false;
}
}
true
}
async fn execute_branch_filters(
filters: &[PipelineFilter],
ctx: &mut HttpFilterContext<'_>,
) -> Result<FilterAction, FilterError> {
for pf in filters {
let http_filter = match &pf.filter {
AnyFilter::Http(f) => f.as_ref(),
AnyFilter::Tcp(_) => continue,
};
if !should_execute(&pf.conditions, ctx.request) {
continue;
}
ctx.current_filter_id = Some(pf.filter_id);
let result = http_filter.on_request(ctx).await;
ctx.current_filter_id = None;
match result {
Ok(FilterAction::Continue | FilterAction::Release | FilterAction::BodyDone) => {},
Ok(FilterAction::Reject(r)) => return Ok(FilterAction::Reject(r)),
Err(e) => {
check_failure_mode(http_filter.name(), e, "branch request", pf.failure_mode)?;
},
}
if let Some(action) = dispatch_nested_outcome(&pf.branches, ctx).await? {
return Ok(action);
}
}
Ok(FilterAction::Continue)
}
async fn dispatch_nested_outcome(
branches: &[ResolvedBranch],
ctx: &mut HttpFilterContext<'_>,
) -> Result<Option<FilterAction>, FilterError> {
let outcome = evaluate_branches(branches, ctx).await?;
match outcome {
BranchOutcome::Continue => Ok(None),
BranchOutcome::SkipTo(target) => {
warn!(
target,
"discarding SkipTo from nested branch; nested control flow does not propagate"
);
Ok(None)
},
BranchOutcome::ReEnter(target) => {
warn!(
target,
"discarding ReEnter from nested branch; nested control flow does not propagate"
);
Ok(None)
},
BranchOutcome::Terminal => Ok(Some(FilterAction::Continue)),
BranchOutcome::Reject(r) => Ok(Some(FilterAction::Reject(r))),
}
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
clippy::too_many_lines,
clippy::doc_markdown,
reason = "tests"
)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use async_trait::async_trait;
use http::Method;
use praxis_core::config::FailureMode;
use super::*;
use crate::{
FilterError, Rejection, filter::HttpFilter, pipeline::branch::ResolvedBranchCondition, results::FilterResultSet,
};
#[tokio::test]
async fn unconditional_branch_fires() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"uncond",
None,
RejoinTarget::Next,
None,
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"unconditional branch with Next rejoin should continue"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"unconditional branch filter should have executed"
);
}
#[tokio::test]
async fn conditional_branch_fires_on_match() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"cond_match",
Some(("cache", "status", "hit")),
RejoinTarget::Next,
None,
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut rs = FilterResultSet::new();
rs.set("status", "hit").unwrap();
ctx.filter_results.insert("cache", rs);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"matching conditional branch should continue"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"matching branch filter should have executed"
);
}
#[tokio::test]
async fn conditional_branch_skips_on_mismatch() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"cond_miss",
Some(("cache", "status", "hit")),
RejoinTarget::Next,
None,
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut rs = FilterResultSet::new();
rs.set("status", "miss").unwrap();
ctx.filter_results.insert("cache", rs);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"mismatched branch should continue"
);
assert_eq!(
counter.load(Ordering::SeqCst),
0,
"mismatched branch filter should not have executed"
);
}
#[tokio::test]
async fn terminal_rejoin_stops_parent() {
let branches = vec![make_branch("term", None, RejoinTarget::Terminal, None, vec![])];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Terminal),
"terminal branch should stop parent"
);
}
#[tokio::test]
async fn skip_to_advances_to_target() {
let branches = vec![make_branch("skip", None, RejoinTarget::SkipTo(5), None, vec![])];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::SkipTo(5)),
"SkipTo branch should advance to target index 5"
);
}
#[tokio::test]
async fn reenter_loops_back() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"reenter",
None,
RejoinTarget::ReEnter(1),
Some(3),
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::ReEnter(1)),
"ReEnter branch should loop back to target index"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"branch filter should execute once per evaluation"
);
}
#[tokio::test]
async fn reenter_max_iterations_exceeded_falls_through() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"limited",
None,
RejoinTarget::ReEnter(0),
Some(2),
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
evaluate_branches(&branches, &mut ctx).await.unwrap();
evaluate_branches(&branches, &mut ctx).await.unwrap();
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"exceeded max_iterations should fall through to Continue"
);
assert_eq!(
counter.load(Ordering::SeqCst),
2,
"branch should execute max_iterations times then stop"
);
}
#[tokio::test]
async fn branch_filter_reject_propagates() {
let branches = vec![make_branch(
"reject",
None,
RejoinTarget::Next,
None,
vec![reject_pf(403)],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Reject(r) if r.status == 403),
"branch filter rejection should propagate"
);
}
#[tokio::test]
async fn branch_filter_error_respects_failure_mode_open() {
let branches = vec![make_branch(
"open_error",
None,
RejoinTarget::Next,
None,
vec![error_pf(FailureMode::Open)],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"failure_mode=open should swallow branch filter errors"
);
assert_eq!(ctx.current_filter_id, None, "filter id should be cleared after error");
}
#[tokio::test]
async fn branch_filter_error_respects_failure_mode_closed() {
let branches = vec![make_branch(
"closed_error",
None,
RejoinTarget::Next,
None,
vec![error_pf(FailureMode::Closed)],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let Err(err) = evaluate_branches(&branches, &mut ctx).await else {
panic!("failure_mode=closed should propagate branch filter errors");
};
assert!(
err.to_string().contains("branch error"),
"unexpected branch filter error: {err}"
);
assert_eq!(ctx.current_filter_id, None, "filter id should be cleared after error");
}
#[tokio::test]
async fn results_cleared_after_evaluation() {
let branches: Vec<ResolvedBranch> = vec![];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut rs = FilterResultSet::new();
rs.set("status", "hit").unwrap();
ctx.filter_results.insert("cache", rs);
assert!(
!ctx.filter_results.is_empty(),
"results should be present before evaluation"
);
evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
ctx.filter_results.is_empty(),
"results should be cleared after evaluation"
);
}
#[tokio::test]
async fn multiple_branches_first_match_wins() {
let counter_a = Arc::new(AtomicUsize::new(0));
let counter_b = Arc::new(AtomicUsize::new(0));
let branches = vec![
make_branch(
"first",
None,
RejoinTarget::Terminal,
None,
vec![counting_pf(Arc::clone(&counter_a))],
),
make_branch(
"second",
None,
RejoinTarget::Terminal,
None,
vec![counting_pf(Arc::clone(&counter_b))],
),
];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Terminal),
"first matching branch should win"
);
assert_eq!(counter_a.load(Ordering::SeqCst), 1, "first branch should execute");
assert_eq!(counter_b.load(Ordering::SeqCst), 0, "second branch should not execute");
}
#[tokio::test]
async fn empty_branches_is_noop() {
let branches: Vec<ResolvedBranch> = vec![];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"empty branches should continue"
);
}
#[tokio::test]
async fn nested_branch_terminal_propagates() {
let inner_branch = make_branch("inner", None, RejoinTarget::Terminal, None, vec![]);
let outer_filter = PipelineFilter {
filter_id: 100,
branches: vec![inner_branch],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(NoopFilter)),
name: None,
response_conditions: vec![],
};
let outer_branches = vec![make_branch("outer", None, RejoinTarget::Next, None, vec![outer_filter])];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&outer_branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"nested terminal should stop the branch but outer continues with Next rejoin"
);
}
#[tokio::test]
async fn conditional_branch_skips_when_filter_absent_from_results() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"absent_filter",
Some(("cache", "status", "hit")),
RejoinTarget::Next,
None,
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"branch should not fire when referenced filter has no results"
);
assert_eq!(
counter.load(Ordering::SeqCst),
0,
"branch filter should not execute when referenced filter is absent from results"
);
}
#[tokio::test]
async fn reenter_does_not_carry_stale_results_into_conditional_branch() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"cond_reenter",
Some(("tracker", "status", "stale")),
RejoinTarget::ReEnter(0),
Some(3),
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let mut rs = FilterResultSet::new();
rs.set("status", "stale").unwrap();
ctx.filter_results.insert("tracker", rs);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::ReEnter(0)),
"first call should fire and produce ReEnter"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"branch should execute once on first evaluation"
);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"second call should not fire because stale results were cleared"
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"branch should not execute again when stale result is gone"
);
}
#[tokio::test]
async fn reenter_max_iterations_at_ceiling_fires_exactly_100_times() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"ceiling",
None,
RejoinTarget::ReEnter(0),
Some(100),
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
for i in 1..=100 {
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::ReEnter(0)),
"iteration {i} should re-enter"
);
}
assert_eq!(
counter.load(Ordering::SeqCst),
100,
"branch should fire exactly 100 times"
);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"iteration 101 should fall through"
);
assert_eq!(
counter.load(Ordering::SeqCst),
100,
"branch should not fire after max_iterations exceeded"
);
}
#[tokio::test]
async fn branch_filter_can_insert_and_read_own_state() {
let log: ObsLog = Arc::new(std::sync::Mutex::new(Vec::new()));
let branch_pf_id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
let branch_pf = PipelineFilter {
filter_id: branch_pf_id,
branches: vec![],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(BranchStatefulFilter {
id: 42,
log: Arc::clone(&log),
})),
name: None,
response_conditions: vec![],
};
let branches = vec![make_branch(
"state_branch",
None,
RejoinTarget::Next,
None,
vec![branch_pf],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
evaluate_branches(&branches, &mut ctx).await.unwrap();
let recorded = log.lock().unwrap().clone();
assert_eq!(recorded, vec![(42, "insert")], "branch filter should insert state");
assert!(
ctx.filter_state.contains_key(&branch_pf_id),
"state should exist at the branch filter's unique id"
);
}
#[tokio::test]
async fn branch_filter_state_does_not_collide_with_parent() {
let log: ObsLog = Arc::new(std::sync::Mutex::new(Vec::new()));
let parent_id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
let branch_id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
let branch_pf = PipelineFilter {
filter_id: branch_id,
branches: vec![],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(BranchStatefulFilter {
id: 99,
log: Arc::clone(&log),
})),
name: None,
response_conditions: vec![],
};
let branches = vec![make_branch(
"state_branch",
None,
RejoinTarget::Next,
None,
vec![branch_pf],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
ctx.filter_state.insert(parent_id, Box::new(77_u64));
evaluate_branches(&branches, &mut ctx).await.unwrap();
let parent_state = ctx.filter_state.get(&parent_id).unwrap().downcast_ref::<u64>().unwrap();
let branch_state = ctx.filter_state.get(&branch_id).unwrap().downcast_ref::<u64>().unwrap();
assert_eq!(*parent_state, 77, "parent state should be unchanged");
assert_eq!(*branch_state, 99, "branch filter should have its own state");
}
#[tokio::test]
async fn two_branch_filters_of_same_type_get_independent_state() {
let log: ObsLog = Arc::new(std::sync::Mutex::new(Vec::new()));
let pf_a = stateful_pf(100, &log);
let pf_b = stateful_pf(200, &log);
let id_a = pf_a.filter_id;
let id_b = pf_b.filter_id;
let branches = vec![make_branch("dual", None, RejoinTarget::Next, None, vec![pf_a, pf_b])];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
evaluate_branches(&branches, &mut ctx).await.unwrap();
let recorded = log.lock().unwrap().clone();
assert_eq!(
recorded,
vec![(100, "insert"), (200, "insert")],
"both branch filters should insert"
);
let state_a = ctx.filter_state.get(&id_a).unwrap().downcast_ref::<u64>().unwrap();
let state_b = ctx.filter_state.get(&id_b).unwrap().downcast_ref::<u64>().unwrap();
assert_eq!(*state_a, 100, "first branch filter state");
assert_eq!(*state_b, 200, "second branch filter state");
}
#[tokio::test]
async fn identity_is_none_after_branch_evaluation() {
let branches = vec![make_branch(
"check",
None,
RejoinTarget::Next,
None,
vec![counting_pf(Arc::new(AtomicUsize::new(0)))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
ctx.current_filter_id.is_none(),
"current_filter_id should be None after branch evaluation"
);
}
#[tokio::test]
async fn identity_is_none_after_branch_rejection() {
let branches = vec![make_branch(
"reject_branch",
None,
RejoinTarget::Next,
None,
vec![reject_pf(403)],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(matches!(outcome, BranchOutcome::Reject(_)), "should reject");
assert!(
ctx.current_filter_id.is_none(),
"current_filter_id should be None after branch rejection"
);
}
#[tokio::test]
async fn reenter_without_max_iterations_always_fires() {
let counter = Arc::new(AtomicUsize::new(0));
let branches = vec![make_branch(
"unlimited",
None,
RejoinTarget::ReEnter(0),
None,
vec![counting_pf(Arc::clone(&counter))],
)];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
for i in 1..=10 {
let outcome = evaluate_branches(&branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::ReEnter(0)),
"iteration {i} should re-enter when max_iterations is None"
);
}
assert_eq!(
counter.load(Ordering::SeqCst),
10,
"branch should fire every time when max_iterations is None"
);
}
#[tokio::test]
async fn nested_branch_skip_to_discarded() {
let inner_branch = make_branch("inner_skip", None, RejoinTarget::SkipTo(42), None, vec![]);
let outer_filter = PipelineFilter {
filter_id: NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst),
branches: vec![inner_branch],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(NoopFilter)),
name: None,
response_conditions: vec![],
};
let outer_branches = vec![make_branch("outer", None, RejoinTarget::Next, None, vec![outer_filter])];
let req = crate::test_utils::make_request(Method::GET, "/");
let mut ctx = crate::test_utils::make_filter_context(&req);
let outcome = evaluate_branches(&outer_branches, &mut ctx).await.unwrap();
assert!(
matches!(outcome, BranchOutcome::Continue),
"nested SkipTo should be discarded and outer should continue"
);
}
struct NoopFilter;
#[async_trait]
impl HttpFilter for NoopFilter {
fn name(&self) -> &'static str {
"noop"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Continue)
}
}
struct CountFilter {
counter: Arc<AtomicUsize>,
}
#[async_trait]
impl HttpFilter for CountFilter {
fn name(&self) -> &'static str {
"count"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
self.counter.fetch_add(1, Ordering::SeqCst);
Ok(FilterAction::Continue)
}
}
struct RejectFilter {
status: u16,
}
#[async_trait]
impl HttpFilter for RejectFilter {
fn name(&self) -> &'static str {
"reject"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Ok(FilterAction::Reject(Rejection::status(self.status)))
}
}
struct ErrorFilter;
#[async_trait]
impl HttpFilter for ErrorFilter {
fn name(&self) -> &'static str {
"error"
}
async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
Err("branch error".into())
}
}
type ObsLog = Arc<std::sync::Mutex<Vec<(u64, &'static str)>>>;
struct BranchStatefulFilter {
id: u64,
log: ObsLog,
}
#[async_trait]
impl HttpFilter for BranchStatefulFilter {
fn name(&self) -> &'static str {
"branch_stateful"
}
async fn on_request(&self, ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
ctx.insert_filter_state(self.id);
self.log.lock().unwrap().push((self.id, "insert"));
Ok(FilterAction::Continue)
}
}
static NEXT_TEST_ID: AtomicUsize = AtomicUsize::new(1000);
fn counting_pf(counter: Arc<AtomicUsize>) -> PipelineFilter {
PipelineFilter {
filter_id: NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst),
branches: vec![],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(CountFilter { counter })),
name: None,
response_conditions: vec![],
}
}
fn reject_pf(status: u16) -> PipelineFilter {
PipelineFilter {
filter_id: NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst),
branches: vec![],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(RejectFilter { status })),
name: None,
response_conditions: vec![],
}
}
fn error_pf(failure_mode: FailureMode) -> PipelineFilter {
PipelineFilter {
filter_id: NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst),
branches: vec![],
conditions: vec![],
failure_mode,
filter: AnyFilter::Http(Box::new(ErrorFilter)),
name: None,
response_conditions: vec![],
}
}
fn stateful_pf(id: u64, log: &ObsLog) -> PipelineFilter {
PipelineFilter {
filter_id: NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst),
branches: vec![],
conditions: vec![],
failure_mode: FailureMode::default(),
filter: AnyFilter::Http(Box::new(BranchStatefulFilter {
id,
log: Arc::clone(log),
})),
name: None,
response_conditions: vec![],
}
}
fn make_branch(
name: &str,
condition: Option<(&str, &str, &str)>,
rejoin: RejoinTarget,
max_iterations: Option<u32>,
filters: Vec<PipelineFilter>,
) -> ResolvedBranch {
ResolvedBranch {
condition: condition.map(|(filter, key, value)| ResolvedBranchCondition {
filter_name: Arc::from(filter),
key: Arc::from(key),
value: Arc::from(value),
}),
filters,
max_iterations,
name: Arc::from(name),
rejoin,
}
}
}