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;
6use tower::ServiceExt;
7
8use camel_api::{BoxProcessor, CamelError, Exchange, FilterPredicate, PredicateSource};
9
10/// Tower Service implementing the Filter EIP.
11///
12/// If the predicate evaluates to `true`, the exchange is forwarded through the
13/// sub-pipeline. If `false`, the exchange is returned as-is and the
14/// sub-pipeline is skipped entirely. A failed (async) predicate evaluation
15/// propagates as `Err` — the exchange is never silently dropped.
16#[derive(Clone)]
17pub struct FilterService {
18    predicate: PredicateSource,
19    sub_pipeline: BoxProcessor,
20}
21
22impl FilterService {
23    /// Create from a closure predicate and a resolved sub-pipeline.
24    pub fn new(
25        predicate: impl Fn(&Exchange) -> bool + Send + Sync + 'static,
26        sub_pipeline: BoxProcessor,
27    ) -> Self {
28        Self {
29            predicate: PredicateSource::Sync(FilterPredicate::new(predicate)),
30            sub_pipeline,
31        }
32    }
33
34    /// Create from a fallible `PredicateSource` (used by `resolve_steps`).
35    pub fn from_predicate(predicate: PredicateSource, sub_pipeline: BoxProcessor) -> Self {
36        Self {
37            predicate,
38            sub_pipeline,
39        }
40    }
41}
42
43impl Service<Exchange> for FilterService {
44    type Response = Exchange;
45    type Error = CamelError;
46    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
47
48    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
49        self.sub_pipeline.poll_ready(cx)
50    }
51
52    fn call(&mut self, exchange: Exchange) -> Self::Future {
53        // Clone-and-replace: the future owns its state so the predicate can be
54        // awaited before deciding whether to invoke the sub-pipeline.
55        let predicate = self.predicate.clone();
56        let mut sub_pipeline = self.sub_pipeline.clone();
57        Box::pin(async move {
58            match predicate.matches(&exchange).await {
59                Ok(true) => sub_pipeline.ready().await?.call(exchange).await,
60                Ok(false) => Ok(exchange),
61                Err(err) => Err(err),
62            }
63        })
64    }
65}
66
67// ── FilterSegment (ADR-0025 OutcomePipeline) ─────────────────────────────
68
69/// Outcome-aware structural EIP segment for the Filter pattern.
70///
71/// When the predicate passes, delegates to `body` (which can return
72/// `Completed`, `Stopped`, or `Failed`). When the predicate evaluates to
73/// `false`, returns `Completed(original_exchange)` — the exchange is returned
74/// as-is and the body is skipped entirely. A failed predicate evaluation
75/// yields `Failed(err)`; the exchange is never silently dropped.
76///
77/// Unlike `FilterService` (which operates at the Tower layer and cannot
78/// preserve `Stopped(ex)` with mutations), `FilterSegment` operates at
79/// the `PipelineOutcome` layer and preserves the exchange at the Stop
80/// point including all mutations.
81pub struct FilterSegment {
82    pub predicate: camel_api::PredicateSource,
83    pub body: camel_api::OutcomeSegment,
84}
85
86impl Clone for FilterSegment {
87    fn clone(&self) -> Self {
88        Self {
89            predicate: self.predicate.clone(),
90            body: self.body.clone(),
91        }
92    }
93}
94
95impl camel_api::OutcomePipeline for FilterSegment {
96    fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
97        Box::new(self.clone())
98    }
99
100    fn run<'a>(
101        &'a mut self,
102        exchange: camel_api::Exchange,
103    ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
104        Box::pin(async move {
105            match self.predicate.matches(&exchange).await {
106                Ok(true) => self.body.run(exchange).await,
107                Ok(false) => camel_api::PipelineOutcome::Completed(exchange),
108                Err(err) => camel_api::PipelineOutcome::Failed(err),
109            }
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use camel_api::{Body, BoxProcessorExt, Message, Value};
118    use tower::ServiceExt;
119
120    fn passthrough() -> BoxProcessor {
121        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
122    }
123
124    fn uppercase_body() -> BoxProcessor {
125        BoxProcessor::from_fn(|mut ex: Exchange| {
126            Box::pin(async move {
127                if let Body::Text(s) = &ex.input.body {
128                    ex.input.body = Body::Text(s.to_uppercase());
129                }
130                Ok(ex)
131            })
132        })
133    }
134
135    fn failing() -> BoxProcessor {
136        BoxProcessor::from_fn(|_ex| {
137            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
138        })
139    }
140
141    // 1. Matching exchange is forwarded to sub_pipeline.
142    #[tokio::test]
143    async fn test_filter_passes_matching_exchange() {
144        let mut svc = FilterService::new(
145            |ex: &Exchange| ex.input.header("active").is_some(),
146            uppercase_body(),
147        );
148        let mut ex = Exchange::new(Message::new("hello"));
149        ex.input.set_header("active", Value::Bool(true));
150        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
151        assert_eq!(result.input.body.as_text(), Some("HELLO"));
152    }
153
154    // 2. Non-matching exchange is returned as-is, sub_pipeline not called.
155    #[tokio::test]
156    async fn test_filter_blocks_non_matching_exchange() {
157        let mut svc = FilterService::new(
158            |ex: &Exchange| ex.input.header("active").is_some(),
159            uppercase_body(),
160        );
161        let ex = Exchange::new(Message::new("hello"));
162        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
163        // body unchanged — uppercase_body was NOT called
164        assert_eq!(result.input.body.as_text(), Some("hello"));
165    }
166
167    // 3. Result is the sub_pipeline's output, not the original exchange.
168    #[tokio::test]
169    async fn test_filter_sub_pipeline_transforms_body() {
170        let mut svc = FilterService::new(|_: &Exchange| true, uppercase_body());
171        let ex = Exchange::new(Message::new("world"));
172        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
173        assert_eq!(result.input.body.as_text(), Some("WORLD"));
174    }
175
176    // 4. Sub-pipeline errors propagate.
177    #[tokio::test]
178    async fn test_filter_sub_pipeline_error_propagates() {
179        let mut svc = FilterService::new(|_: &Exchange| true, failing());
180        let ex = Exchange::new(Message::new("x"));
181        let result = svc.ready().await.unwrap().call(ex).await;
182        assert!(result.is_err());
183        assert!(result.unwrap_err().to_string().contains("boom"));
184    }
185
186    // 5. Predicate receives the original exchange before sub_pipeline mutates it.
187    #[tokio::test]
188    async fn test_filter_predicate_receives_original_exchange() {
189        let mut svc = FilterService::new(
190            |ex: &Exchange| ex.input.body.as_text() == Some("check"),
191            uppercase_body(),
192        );
193        let ex = Exchange::new(Message::new("check"));
194        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
195        assert_eq!(result.input.body.as_text(), Some("CHECK"));
196    }
197
198    // 6. Cloned FilterService shares no mutable state (BoxProcessor clone is independent).
199    #[tokio::test]
200    async fn test_filter_clone_is_independent() {
201        let svc = FilterService::new(|_: &Exchange| true, passthrough());
202        let mut clone = svc.clone();
203        let ex = Exchange::new(Message::new("hi"));
204        let result = clone.ready().await.unwrap().call(ex).await.unwrap();
205        assert_eq!(result.input.body.as_text(), Some("hi"));
206    }
207
208    // ── Fallible predicate path (language-value-boundary task 1.4) ──
209
210    use std::sync::Arc;
211    use std::sync::atomic::{AtomicU32, Ordering};
212
213    use camel_api::outcome_pipeline::OutcomePipeline as _;
214    use camel_api::{ExpressionErrorClass, PipelineOutcome, PredicateSource};
215
216    fn expression_failed() -> CamelError {
217        CamelError::ExpressionFailed {
218            language: "rhai".to_string(),
219            route_id: "r1".to_string(),
220            step_id: "step#0".to_string(),
221            verb: "filter".to_string(),
222            class: ExpressionErrorClass::Runtime,
223            position: None,
224            conversion: None,
225            cause: None,
226        }
227    }
228
229    fn async_err_predicate(err: CamelError) -> PredicateSource {
230        PredicateSource::Async(Arc::new(move |_: &Exchange| {
231            let err = err.clone();
232            Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
233        }))
234    }
235
236    struct RecordRun(Arc<AtomicU32>);
237
238    impl camel_api::OutcomePipeline for RecordRun {
239        fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
240            Box::new(RecordRun(Arc::clone(&self.0)))
241        }
242        fn run<'a>(
243            &'a mut self,
244            exchange: Exchange,
245        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
246            let c = Arc::clone(&self.0);
247            Box::pin(async move {
248                c.fetch_add(1, Ordering::SeqCst);
249                PipelineOutcome::Completed(exchange)
250            })
251        }
252    }
253
254    #[tokio::test]
255    async fn filter_segment_propagates_predicate_error() {
256        let body_calls = Arc::new(AtomicU32::new(0));
257        let mut seg = FilterSegment {
258            predicate: async_err_predicate(expression_failed()),
259            body: camel_api::OutcomeSegment::new(Box::new(RecordRun(Arc::clone(&body_calls)))),
260        };
261        let outcome = seg.run(Exchange::default()).await;
262        match outcome {
263            PipelineOutcome::Failed(err) => {
264                assert!(
265                    matches!(err, CamelError::ExpressionFailed { .. }),
266                    "expected ExpressionFailed, got {err:?}"
267                );
268            }
269            other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
270        }
271        assert_eq!(
272            body_calls.load(Ordering::SeqCst),
273            0,
274            "filter body must not run when the predicate errors"
275        );
276    }
277}