praxis-proxy-filter 0.7.2

Filter pipeline engine and built-in filters for Praxis
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Praxis Contributors

//! Test-only filters for pipeline capability validation.

use async_trait::async_trait;
use praxis_core::config::{Condition, FailureMode};

use super::filter::PipelineFilter;
#[cfg(feature = "upstream-binding")]
use crate::pipeline::catalog::ClusterMetadataDeclaration;
use crate::{
    FilterAction, FilterError,
    any_filter::AnyFilter,
    body::{BodyAccess, BodyMode},
    filter::{HttpFilter, HttpFilterContext},
};

pub(in crate::pipeline) fn selector_filter(name: &'static str, clusters: &[&str]) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name,
        selected_clusters: owned(clusters),
        selects_cluster: true,
        ..CapabilityFilter::default()
    })
}

pub(in crate::pipeline) fn lb_filter(clusters: &[&str]) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        load_balancer_clusters: owned(clusters),
        name: "load_balancer",
        ..CapabilityFilter::default()
    })
}

pub(in crate::pipeline) fn noop_filter(name: &'static str) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name,
        ..CapabilityFilter::default()
    })
}

#[cfg(feature = "upstream-binding")]
pub(in crate::pipeline) fn terminal_filter(name: &'static str) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name,
        produces_terminal_response: true,
        ..CapabilityFilter::default()
    })
}

/// A binding router stand-in: selects and *binds* one of `clusters` as the
/// logical upstream.
#[cfg(feature = "upstream-binding")]
pub(in crate::pipeline) fn binding_router(clusters: &[&str]) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name: "router",
        selected_clusters: owned(clusters),
        selects_cluster: true,
        binds_upstream: true,
        ..CapabilityFilter::default()
    })
}

/// A bound-consuming load balancer stand-in: resolves `clusters` from the
/// frozen logical binding.
#[cfg(feature = "upstream-binding")]
pub(in crate::pipeline) fn bound_lb(clusters: &[&str]) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name: "load_balancer",
        load_balancer_clusters: owned(clusters),
        consumes_bound_upstream: true,
        bound_upstream_clusters: owned(clusters),
        ..CapabilityFilter::default()
    })
}

/// A filter participating in the bound-upstream request-body phase with the
/// given access and delivery mode.
#[cfg(feature = "bound-upstream-request-body")]
pub(in crate::pipeline) fn bound_body_filter(name: &'static str, access: BodyAccess, mode: BodyMode) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name,
        bound_upstream_request_body_access: access,
        request_body_mode: Some(mode),
        ..CapabilityFilter::default()
    })
}

/// A filter declaring BOTH a pre-read and a bound-upstream request-body hook,
/// with the given accesses and a shared delivery mode.
///
/// Used to exercise dual-access phase inference: the effective phase depends on
/// whether the filter carries a `bound_upstream` request condition.
#[cfg(feature = "bound-upstream-request-body")]
pub(in crate::pipeline) fn dual_phase_body_filter(
    name: &'static str,
    pre_read: BodyAccess,
    bound: BodyAccess,
    mode: BodyMode,
) -> PipelineFilter {
    capability_filter(CapabilityFilter {
        name,
        request_body_access: pre_read,
        bound_upstream_request_body_access: bound,
        request_body_mode: Some(mode),
        ..CapabilityFilter::default()
    })
}

/// A filter declaring application metadata for one cluster, standing in for a
/// load balancer in catalog tests.
#[cfg(feature = "upstream-binding")]
pub(in crate::pipeline) fn metadata_filter(
    name: &'static str,
    cluster: &str,
    protocol: Option<&str>,
    provider: Option<&str>,
) -> PipelineFilter {
    use std::sync::Arc;

    use crate::pipeline::catalog::ClusterApplicationMetadata;

    capability_filter(CapabilityFilter {
        name,
        declared_metadata: vec![ClusterMetadataDeclaration {
            name: Arc::from(cluster),
            metadata: ClusterApplicationMetadata::new(protocol.map(Arc::from), provider.map(Arc::from)),
        }],
        ..CapabilityFilter::default()
    })
}

fn owned(clusters: &[&str]) -> Vec<String> {
    clusters.iter().map(|cluster| (*cluster).to_owned()).collect()
}

pub(in crate::pipeline) fn noop_filter_with_conditions(
    name: &'static str,
    conditions: Vec<Condition>,
) -> PipelineFilter {
    let mut filter = noop_filter(name);
    filter.conditions = conditions;
    filter
}

fn capability_filter(filter: CapabilityFilter) -> PipelineFilter {
    PipelineFilter {
        filter_id: 0,
        is_security: false,
        branches: vec![],
        conditions: vec![],
        failure_mode: FailureMode::default(),
        filter: AnyFilter::Http(Box::new(filter)),
        name: None,
        response_conditions: vec![],
    }
}

#[derive(Default)]
#[cfg_attr(
    feature = "upstream-binding",
    expect(
        clippy::struct_excessive_bools,
        reason = "test-only capability record; each flag toggles one independent HttpFilter capability"
    )
)]
struct CapabilityFilter {
    name: &'static str,
    selects_cluster: bool,
    selected_clusters: Vec<String>,
    load_balancer_clusters: Vec<String>,
    #[cfg(feature = "upstream-binding")]
    binds_upstream: bool,
    #[cfg(feature = "upstream-binding")]
    consumes_bound_upstream: bool,
    #[cfg(feature = "upstream-binding")]
    bound_upstream_clusters: Vec<String>,
    #[cfg(feature = "bound-upstream-request-body")]
    bound_upstream_request_body_access: BodyAccess,
    request_body_access: BodyAccess,
    request_body_mode: Option<BodyMode>,
    #[cfg(feature = "upstream-binding")]
    declared_metadata: Vec<ClusterMetadataDeclaration>,
    produces_terminal_response: bool,
}

#[async_trait]
impl HttpFilter for CapabilityFilter {
    fn name(&self) -> &'static str {
        self.name
    }

    fn selects_cluster(&self) -> bool {
        self.selects_cluster
    }

    fn selected_clusters(&self) -> Vec<String> {
        self.selected_clusters.clone()
    }

    fn load_balancer_clusters(&self) -> Vec<String> {
        self.load_balancer_clusters.clone()
    }

    #[cfg(feature = "upstream-binding")]
    fn binds_upstream(&self) -> bool {
        self.binds_upstream
    }

    #[cfg(feature = "upstream-binding")]
    fn consumes_bound_upstream(&self) -> bool {
        self.consumes_bound_upstream
    }

    #[cfg(feature = "upstream-binding")]
    fn bound_upstream_clusters(&self) -> Vec<String> {
        self.bound_upstream_clusters.clone()
    }

    #[cfg(feature = "bound-upstream-request-body")]
    fn bound_upstream_request_body_access(&self) -> BodyAccess {
        self.bound_upstream_request_body_access
    }

    fn request_body_access(&self) -> BodyAccess {
        self.request_body_access
    }

    fn request_body_mode(&self) -> BodyMode {
        self.request_body_mode.unwrap_or_default()
    }

    fn produces_terminal_response(&self) -> bool {
        self.produces_terminal_response
    }

    #[cfg(feature = "upstream-binding")]
    fn declared_cluster_metadata(&self) -> Vec<ClusterMetadataDeclaration> {
        self.declared_metadata.clone()
    }

    async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
        Ok(FilterAction::Continue)
    }
}

pub(in crate::pipeline) fn streaming_capable_filter() -> PipelineFilter {
    PipelineFilter {
        filter_id: 0,
        is_security: false,
        branches: vec![],
        conditions: vec![],
        failure_mode: FailureMode::default(),
        filter: AnyFilter::Http(Box::new(StreamingCapableFilter)),
        name: None,
        response_conditions: vec![],
    }
}

struct StreamingCapableFilter;

#[async_trait]
impl HttpFilter for StreamingCapableFilter {
    fn name(&self) -> &'static str {
        "streaming_capable"
    }

    fn may_select_streaming_subrequest_response(&self) -> bool {
        true
    }

    async fn on_request(&self, _ctx: &mut HttpFilterContext<'_>) -> Result<FilterAction, FilterError> {
        Ok(FilterAction::Continue)
    }
}