Skip to main content

camel_processor/
filter.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::Service;
6
7use camel_api::{BoxProcessor, CamelError, Exchange, FilterPredicate};
8
9/// Tower Service implementing the Filter EIP.
10///
11/// If the predicate returns `true`, the exchange is forwarded through the sub-pipeline.
12/// If `false`, the exchange is returned as-is and the sub-pipeline is skipped entirely.
13#[derive(Clone)]
14pub struct FilterService {
15    predicate: FilterPredicate,
16    sub_pipeline: BoxProcessor,
17}
18
19impl FilterService {
20    /// Create from a closure predicate and a resolved sub-pipeline.
21    pub fn new(
22        predicate: impl Fn(&Exchange) -> bool + Send + Sync + 'static,
23        sub_pipeline: BoxProcessor,
24    ) -> Self {
25        Self {
26            predicate: FilterPredicate::new(predicate),
27            sub_pipeline,
28        }
29    }
30
31    /// Create from a pre-boxed `FilterPredicate` (used by `resolve_steps`).
32    pub fn from_predicate(predicate: FilterPredicate, sub_pipeline: BoxProcessor) -> Self {
33        Self {
34            predicate,
35            sub_pipeline,
36        }
37    }
38}
39
40impl Service<Exchange> for FilterService {
41    type Response = Exchange;
42    type Error = CamelError;
43    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
44
45    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
46        self.sub_pipeline.poll_ready(cx)
47    }
48
49    fn call(&mut self, exchange: Exchange) -> Self::Future {
50        if (self.predicate)(&exchange) {
51            let fut = self.sub_pipeline.call(exchange);
52            Box::pin(fut)
53        } else {
54            Box::pin(async move { Ok(exchange) })
55        }
56    }
57}
58
59// ── FilterSegment (ADR-0025 OutcomePipeline) ─────────────────────────────
60
61/// Outcome-aware structural EIP segment for the Filter pattern.
62///
63/// When the predicate passes, delegates to `body` (which can return
64/// `Completed`, `Stopped`, or `Failed`). When the predicate fails,
65/// returns `Completed(original_exchange)` — the exchange is returned
66/// as-is and the body is skipped entirely.
67///
68/// Unlike `FilterService` (which operates at the Tower layer and cannot
69/// preserve `Stopped(ex)` with mutations), `FilterSegment` operates at
70/// the `PipelineOutcome` layer and preserves the exchange at the Stop
71/// point including all mutations.
72pub struct FilterSegment {
73    pub predicate: camel_api::FilterPredicate,
74    pub body: camel_api::OutcomeSegment,
75}
76
77impl Clone for FilterSegment {
78    fn clone(&self) -> Self {
79        Self {
80            predicate: self.predicate.clone(),
81            body: self.body.clone(),
82        }
83    }
84}
85
86impl camel_api::OutcomePipeline for FilterSegment {
87    fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
88        Box::new(self.clone())
89    }
90
91    fn run<'a>(
92        &'a mut self,
93        exchange: camel_api::Exchange,
94    ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
95        Box::pin(async move {
96            if (self.predicate)(&exchange) {
97                self.body.run(exchange).await
98            } else {
99                camel_api::PipelineOutcome::Completed(exchange)
100            }
101        })
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use camel_api::{Body, BoxProcessorExt, Message, Value};
109    use tower::ServiceExt;
110
111    fn passthrough() -> BoxProcessor {
112        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
113    }
114
115    fn uppercase_body() -> BoxProcessor {
116        BoxProcessor::from_fn(|mut ex: Exchange| {
117            Box::pin(async move {
118                if let Body::Text(s) = &ex.input.body {
119                    ex.input.body = Body::Text(s.to_uppercase());
120                }
121                Ok(ex)
122            })
123        })
124    }
125
126    fn failing() -> BoxProcessor {
127        BoxProcessor::from_fn(|_ex| {
128            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
129        })
130    }
131
132    // 1. Matching exchange is forwarded to sub_pipeline.
133    #[tokio::test]
134    async fn test_filter_passes_matching_exchange() {
135        let mut svc = FilterService::new(
136            |ex: &Exchange| ex.input.header("active").is_some(),
137            uppercase_body(),
138        );
139        let mut ex = Exchange::new(Message::new("hello"));
140        ex.input.set_header("active", Value::Bool(true));
141        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
142        assert_eq!(result.input.body.as_text(), Some("HELLO"));
143    }
144
145    // 2. Non-matching exchange is returned as-is, sub_pipeline not called.
146    #[tokio::test]
147    async fn test_filter_blocks_non_matching_exchange() {
148        let mut svc = FilterService::new(
149            |ex: &Exchange| ex.input.header("active").is_some(),
150            uppercase_body(),
151        );
152        let ex = Exchange::new(Message::new("hello"));
153        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
154        // body unchanged — uppercase_body was NOT called
155        assert_eq!(result.input.body.as_text(), Some("hello"));
156    }
157
158    // 3. Result is the sub_pipeline's output, not the original exchange.
159    #[tokio::test]
160    async fn test_filter_sub_pipeline_transforms_body() {
161        let mut svc = FilterService::new(|_: &Exchange| true, uppercase_body());
162        let ex = Exchange::new(Message::new("world"));
163        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
164        assert_eq!(result.input.body.as_text(), Some("WORLD"));
165    }
166
167    // 4. Sub-pipeline errors propagate.
168    #[tokio::test]
169    async fn test_filter_sub_pipeline_error_propagates() {
170        let mut svc = FilterService::new(|_: &Exchange| true, failing());
171        let ex = Exchange::new(Message::new("x"));
172        let result = svc.ready().await.unwrap().call(ex).await;
173        assert!(result.is_err());
174        assert!(result.unwrap_err().to_string().contains("boom"));
175    }
176
177    // 5. Predicate receives the original exchange before sub_pipeline mutates it.
178    #[tokio::test]
179    async fn test_filter_predicate_receives_original_exchange() {
180        let mut svc = FilterService::new(
181            |ex: &Exchange| ex.input.body.as_text() == Some("check"),
182            uppercase_body(),
183        );
184        let ex = Exchange::new(Message::new("check"));
185        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
186        assert_eq!(result.input.body.as_text(), Some("CHECK"));
187    }
188
189    // 6. Cloned FilterService shares no mutable state (BoxProcessor clone is independent).
190    #[tokio::test]
191    async fn test_filter_clone_is_independent() {
192        let svc = FilterService::new(|_: &Exchange| true, passthrough());
193        let mut clone = svc.clone();
194        let ex = Exchange::new(Message::new("hi"));
195        let result = clone.ready().await.unwrap().call(ex).await.unwrap();
196        assert_eq!(result.input.body.as_text(), Some("hi"));
197    }
198}