Skip to main content

camel_processor/
filter.rs

1use std::future::Future;
2use std::pin::Pin;
3
4// ── FilterSegment (ADR-0025 OutcomePipeline) ─────────────────────────────
5
6/// Outcome-aware structural EIP segment for the Filter pattern.
7///
8/// When the predicate passes, delegates to `body` (which can return
9/// `Completed`, `Stopped`, or `Failed`). When the predicate evaluates to
10/// `false`, returns `Completed(original_exchange)` — the exchange is returned
11/// as-is and the body is skipped entirely. A failed predicate evaluation
12/// yields `Failed(err)`; the exchange is never silently dropped.
13///
14/// The segment operates at the `PipelineOutcome` layer and preserves the
15/// exchange at the Stop point including all mutations.
16pub struct FilterSegment {
17    pub predicate: camel_api::PredicateSource,
18    pub body: camel_api::OutcomeSegment,
19}
20
21impl Clone for FilterSegment {
22    fn clone(&self) -> Self {
23        Self {
24            predicate: self.predicate.clone(),
25            body: self.body.clone(),
26        }
27    }
28}
29
30impl camel_api::OutcomePipeline for FilterSegment {
31    fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
32        Box::new(self.clone())
33    }
34
35    fn run<'a>(
36        &'a mut self,
37        exchange: camel_api::Exchange,
38    ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
39        Box::pin(async move {
40            match self.predicate.matches(&exchange).await {
41                Ok(true) => self.body.run(exchange).await,
42                Ok(false) => camel_api::PipelineOutcome::Completed(exchange),
43                Err(err) => camel_api::PipelineOutcome::Failed(err),
44            }
45        })
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::test_processor_segment::processor_segment;
53    use camel_api::{
54        Body, BoxProcessor, BoxProcessorExt, CamelError, Exchange, FilterPredicate, Message,
55        OutcomePipeline, PipelineOutcome, PredicateSource, Value,
56    };
57
58    fn passthrough() -> BoxProcessor {
59        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
60    }
61
62    fn uppercase_body() -> BoxProcessor {
63        BoxProcessor::from_fn(|mut ex: Exchange| {
64            Box::pin(async move {
65                if let Body::Text(s) = &ex.input.body {
66                    ex.input.body = Body::Text(s.to_uppercase());
67                }
68                Ok(ex)
69            })
70        })
71    }
72
73    fn failing() -> BoxProcessor {
74        BoxProcessor::from_fn(|_ex| {
75            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
76        })
77    }
78
79    fn predicate(f: impl Fn(&Exchange) -> bool + Send + Sync + 'static) -> PredicateSource {
80        PredicateSource::Sync(FilterPredicate::new(f))
81    }
82
83    fn segment(predicate: PredicateSource, body: BoxProcessor) -> FilterSegment {
84        FilterSegment {
85            predicate,
86            body: processor_segment(body),
87        }
88    }
89
90    fn completed_exchange(outcome: PipelineOutcome) -> Exchange {
91        match outcome {
92            PipelineOutcome::Completed(ex) => ex,
93            other => panic!("expected PipelineOutcome::Completed, got {other:?}"),
94        }
95    }
96
97    // 1. Matching exchange is forwarded to body.
98    #[tokio::test]
99    async fn filter_segment_passes_matching_exchange() {
100        let mut seg = segment(
101            predicate(|ex: &Exchange| ex.input.header("active").is_some()),
102            uppercase_body(),
103        );
104        let mut ex = Exchange::new(Message::new("hello"));
105        ex.input.set_header("active", Value::Bool(true));
106        let result = completed_exchange(seg.run(ex).await);
107        assert_eq!(result.input.body.as_text(), Some("HELLO"));
108    }
109
110    // 2. Non-matching exchange is returned as-is, body not called.
111    #[tokio::test]
112    async fn filter_segment_blocks_non_matching_exchange() {
113        let mut seg = segment(
114            predicate(|ex: &Exchange| ex.input.header("active").is_some()),
115            uppercase_body(),
116        );
117        let ex = Exchange::new(Message::new("hello"));
118        let result = completed_exchange(seg.run(ex).await);
119        // body unchanged — uppercase_body body was NOT called
120        assert_eq!(result.input.body.as_text(), Some("hello"));
121    }
122
123    // 3. Result is the body's output, not the original exchange.
124    #[tokio::test]
125    async fn filter_segment_sub_pipeline_transforms_body() {
126        let mut seg = segment(predicate(|_: &Exchange| true), uppercase_body());
127        let ex = Exchange::new(Message::new("world"));
128        let result = completed_exchange(seg.run(ex).await);
129        assert_eq!(result.input.body.as_text(), Some("WORLD"));
130    }
131
132    // 4. Body errors surface as Failed.
133    #[tokio::test]
134    async fn filter_segment_sub_pipeline_error_propagates() {
135        let mut seg = segment(predicate(|_: &Exchange| true), failing());
136        let ex = Exchange::new(Message::new("x"));
137        match seg.run(ex).await {
138            PipelineOutcome::Failed(err) => {
139                assert!(err.to_string().contains("boom"), "unexpected error: {err}");
140            }
141            other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
142        }
143    }
144
145    // 5. Predicate receives the original exchange before the body mutates it.
146    #[tokio::test]
147    async fn filter_segment_predicate_receives_original_exchange() {
148        let mut seg = segment(
149            predicate(|ex: &Exchange| ex.input.body.as_text() == Some("check")),
150            uppercase_body(),
151        );
152        let ex = Exchange::new(Message::new("check"));
153        let result = completed_exchange(seg.run(ex).await);
154        assert_eq!(result.input.body.as_text(), Some("CHECK"));
155    }
156
157    // 6. Cloned segment runs independently.
158    #[tokio::test]
159    async fn filter_segment_clone_is_independent() {
160        let seg = segment(predicate(|_: &Exchange| true), passthrough());
161        let mut clone = seg.clone();
162        let ex = Exchange::new(Message::new("hi"));
163        let result = completed_exchange(clone.run(ex).await);
164        assert_eq!(result.input.body.as_text(), Some("hi"));
165    }
166
167    // ── Fallible predicate path (language-value-boundary task 1.4) ──
168
169    use std::sync::Arc;
170    use std::sync::atomic::{AtomicU32, Ordering};
171
172    use camel_api::ExpressionErrorClass;
173
174    fn expression_failed() -> CamelError {
175        CamelError::ExpressionFailed {
176            language: "rhai".to_string(),
177            route_id: "r1".to_string(),
178            step_id: "step#0".to_string(),
179            verb: "filter".to_string(),
180            class: ExpressionErrorClass::Runtime,
181            position: None,
182            conversion: None,
183            cause: None,
184        }
185    }
186
187    fn async_err_predicate(err: CamelError) -> PredicateSource {
188        PredicateSource::Async(Arc::new(move |_: &Exchange| {
189            let err = err.clone();
190            Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
191        }))
192    }
193
194    struct RecordRun(Arc<AtomicU32>);
195
196    impl camel_api::OutcomePipeline for RecordRun {
197        fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
198            Box::new(RecordRun(Arc::clone(&self.0)))
199        }
200        fn run<'a>(
201            &'a mut self,
202            exchange: Exchange,
203        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
204            let c = Arc::clone(&self.0);
205            Box::pin(async move {
206                c.fetch_add(1, Ordering::SeqCst);
207                PipelineOutcome::Completed(exchange)
208            })
209        }
210    }
211
212    #[tokio::test]
213    async fn filter_segment_propagates_predicate_error() {
214        let body_calls = Arc::new(AtomicU32::new(0));
215        let mut seg = FilterSegment {
216            predicate: async_err_predicate(expression_failed()),
217            body: camel_api::OutcomeSegment::new(Box::new(RecordRun(Arc::clone(&body_calls)))),
218        };
219        let outcome = seg.run(Exchange::default()).await;
220        match outcome {
221            PipelineOutcome::Failed(err) => {
222                assert!(
223                    matches!(err, CamelError::ExpressionFailed { .. }),
224                    "expected ExpressionFailed, got {err:?}"
225                );
226            }
227            other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
228        }
229        assert_eq!(
230            body_calls.load(Ordering::SeqCst),
231            0,
232            "filter body must not run when the predicate errors"
233        );
234    }
235}