Skip to main content

camel_processor/
choice.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, PredicateSource};
9
10/// A single when-clause: a fallible predicate + the sub-pipeline to execute
11/// when it matches.
12pub struct WhenClause {
13    pub predicate: PredicateSource,
14    pub pipeline: BoxProcessor,
15}
16
17impl Clone for WhenClause {
18    fn clone(&self) -> Self {
19        Self {
20            predicate: self.predicate.clone(),
21            pipeline: self.pipeline.clone(),
22        }
23    }
24}
25
26/// Tower Service implementing the Choice EIP (Content-Based Router).
27///
28/// Evaluates `when` clauses in order. The first matching predicate routes the
29/// exchange through its sub-pipeline. If no predicate matches, the `otherwise`
30/// pipeline is used (if present); otherwise the exchange passes through
31/// unchanged. A failed predicate evaluation propagates as `Err` instead of
32/// falling through to `otherwise`.
33#[derive(Clone)]
34pub struct ChoiceService {
35    whens: Vec<WhenClause>,
36    otherwise: Option<BoxProcessor>,
37}
38
39impl ChoiceService {
40    /// Create a new `ChoiceService`.
41    ///
42    /// `whens` — ordered list of `(predicate, sub_pipeline)` pairs.
43    /// `otherwise` — optional fallback pipeline (executed when no `when` matches).
44    pub fn new(whens: Vec<WhenClause>, otherwise: Option<BoxProcessor>) -> Self {
45        Self { whens, otherwise }
46    }
47}
48
49impl Service<Exchange> for ChoiceService {
50    type Response = Exchange;
51    type Error = CamelError;
52    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
53
54    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
55        Poll::Ready(Ok(()))
56    }
57
58    fn call(&mut self, exchange: Exchange) -> Self::Future {
59        // Clone-and-replace: the future owns its state so predicates can be
60        // awaited in order before routing.
61        let whens = self.whens.clone();
62        let otherwise = self.otherwise.clone();
63        Box::pin(async move {
64            let mut whens = whens;
65            for when in whens.iter_mut() {
66                if when.predicate.matches(&exchange).await? {
67                    return when.pipeline.ready().await?.call(exchange).await;
68                }
69            }
70            if let Some(mut otherwise) = otherwise {
71                otherwise.ready().await?.call(exchange).await
72            } else {
73                Ok(exchange)
74            }
75        })
76    }
77}
78
79// ── ChoiceSegment + WhenClauseSegment (ADR-0025 OutcomePipeline) ──────────
80
81/// Outcome-aware structural EIP segment for a single when clause.
82pub struct WhenClauseSegment {
83    pub predicate: camel_api::PredicateSource,
84    pub body: camel_api::OutcomeSegment,
85}
86
87impl Clone for WhenClauseSegment {
88    fn clone(&self) -> Self {
89        Self {
90            predicate: self.predicate.clone(),
91            body: self.body.clone(),
92        }
93    }
94}
95
96/// Outcome-aware structural EIP segment for the Choice pattern.
97///
98/// Evaluates `when` clauses in order. The first matching predicate runs its
99/// `body` (which can return `Completed`, `Stopped`, or `Failed`). If no
100/// predicate matches, the `otherwise` segment runs (if present); otherwise
101/// returns `Completed(original_exchange)`. A failed predicate evaluation
102/// yields `Failed(err)` instead of falling through to `otherwise`.
103///
104/// Unlike `ChoiceService` (which operates at the Tower layer and cannot
105/// preserve `Stopped(ex)` with mutations), `ChoiceSegment` operates at the
106/// `PipelineOutcome` layer and preserves the exchange at the Stop point
107/// including all mutations.
108pub struct ChoiceSegment {
109    pub clauses: Vec<WhenClauseSegment>,
110    pub otherwise: Option<camel_api::OutcomeSegment>,
111}
112
113impl Clone for ChoiceSegment {
114    fn clone(&self) -> Self {
115        Self {
116            clauses: self
117                .clauses
118                .iter()
119                .map(|c| WhenClauseSegment {
120                    predicate: c.predicate.clone(),
121                    body: c.body.clone(),
122                })
123                .collect(),
124            otherwise: self.otherwise.clone(),
125        }
126    }
127}
128
129impl camel_api::OutcomePipeline for ChoiceSegment {
130    fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
131        Box::new(self.clone())
132    }
133
134    fn run<'a>(
135        &'a mut self,
136        exchange: camel_api::Exchange,
137    ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
138        Box::pin(async move {
139            for clause in self.clauses.iter_mut() {
140                match clause.predicate.matches(&exchange).await {
141                    Ok(true) => return clause.body.run(exchange).await,
142                    Ok(false) => continue,
143                    Err(err) => return camel_api::PipelineOutcome::Failed(err),
144                }
145            }
146            if let Some(otherwise) = self.otherwise.as_mut() {
147                otherwise.run(exchange).await
148            } else {
149                camel_api::PipelineOutcome::Completed(exchange)
150            }
151        })
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use camel_api::{Body, BoxProcessorExt, Message, Value};
159    use tower::ServiceExt;
160
161    fn append_body(suffix: &'static str) -> BoxProcessor {
162        BoxProcessor::from_fn(move |mut ex: Exchange| {
163            Box::pin(async move {
164                if let Body::Text(s) = &ex.input.body {
165                    ex.input.body = Body::Text(format!("{s}{suffix}"));
166                }
167                Ok(ex)
168            })
169        })
170    }
171
172    fn failing() -> BoxProcessor {
173        BoxProcessor::from_fn(|_ex| {
174            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
175        })
176    }
177
178    fn pred_header(name: &'static str) -> PredicateSource {
179        PredicateSource::Sync(FilterPredicate::new(move |ex: &Exchange| {
180            ex.input.header(name).is_some()
181        }))
182    }
183
184    // 1. First matching when executes its pipeline.
185    #[tokio::test]
186    async fn test_choice_first_when_matches() {
187        let whens = vec![
188            WhenClause {
189                predicate: pred_header("a"),
190                pipeline: append_body("-A"),
191            },
192            WhenClause {
193                predicate: pred_header("b"),
194                pipeline: append_body("-B"),
195            },
196        ];
197        let mut svc = ChoiceService::new(whens, None);
198        let mut ex = Exchange::new(Message::new("x"));
199        ex.input.set_header("a", Value::Bool(true));
200        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
201        assert_eq!(result.input.body.as_text(), Some("x-A"));
202    }
203
204    // 2. Second when executes when first does not match.
205    #[tokio::test]
206    async fn test_choice_second_when_matches() {
207        let whens = vec![
208            WhenClause {
209                predicate: pred_header("a"),
210                pipeline: append_body("-A"),
211            },
212            WhenClause {
213                predicate: pred_header("b"),
214                pipeline: append_body("-B"),
215            },
216        ];
217        let mut svc = ChoiceService::new(whens, None);
218        let mut ex = Exchange::new(Message::new("x"));
219        ex.input.set_header("b", Value::Bool(true));
220        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
221        assert_eq!(result.input.body.as_text(), Some("x-B"));
222    }
223
224    // 3. Only the FIRST matching when fires (short-circuit — both a and b present).
225    #[tokio::test]
226    async fn test_choice_short_circuits_at_first_match() {
227        let whens = vec![
228            WhenClause {
229                predicate: pred_header("a"),
230                pipeline: append_body("-A"),
231            },
232            WhenClause {
233                predicate: pred_header("b"),
234                pipeline: append_body("-B"),
235            },
236        ];
237        let mut svc = ChoiceService::new(whens, None);
238        let mut ex = Exchange::new(Message::new("x"));
239        ex.input.set_header("a", Value::Bool(true));
240        ex.input.set_header("b", Value::Bool(true));
241        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
242        assert_eq!(result.input.body.as_text(), Some("x-A"));
243    }
244
245    // 4. Otherwise executes when no when matches.
246    #[tokio::test]
247    async fn test_choice_otherwise_fires_when_no_when_matches() {
248        let whens = vec![WhenClause {
249            predicate: pred_header("a"),
250            pipeline: append_body("-A"),
251        }];
252        let mut svc = ChoiceService::new(whens, Some(append_body("-else")));
253        let ex = Exchange::new(Message::new("x"));
254        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
255        assert_eq!(result.input.body.as_text(), Some("x-else"));
256    }
257
258    // 5. No match and no otherwise → exchange passes unchanged.
259    #[tokio::test]
260    async fn test_choice_no_match_no_otherwise_passthrough() {
261        let whens = vec![WhenClause {
262            predicate: pred_header("a"),
263            pipeline: append_body("-A"),
264        }];
265        let mut svc = ChoiceService::new(whens, None);
266        let ex = Exchange::new(Message::new("untouched"));
267        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
268        assert_eq!(result.input.body.as_text(), Some("untouched"));
269    }
270
271    // 6. Errors in a matching when's pipeline propagate.
272    #[tokio::test]
273    async fn test_choice_error_in_when_propagates() {
274        let whens = vec![WhenClause {
275            predicate: pred_header("a"),
276            pipeline: failing(),
277        }];
278        let mut svc = ChoiceService::new(whens, None);
279        let mut ex = Exchange::new(Message::new("x"));
280        ex.input.set_header("a", Value::Bool(true));
281        let result = svc.ready().await.unwrap().call(ex).await;
282        assert!(result.is_err());
283        assert!(result.unwrap_err().to_string().contains("boom"));
284    }
285
286    // 7. Errors in otherwise pipeline propagate.
287    #[tokio::test]
288    async fn test_choice_error_in_otherwise_propagates() {
289        let mut svc = ChoiceService::new(vec![], Some(failing()));
290        let ex = Exchange::new(Message::new("x"));
291        let result = svc.ready().await.unwrap().call(ex).await;
292        assert!(result.is_err());
293    }
294
295    // ── Fallible predicate path (language-value-boundary task 1.4) ──
296
297    use std::sync::Arc;
298    use std::sync::atomic::{AtomicU32, Ordering};
299
300    use camel_api::FilterPredicate;
301    use camel_api::outcome_pipeline::OutcomePipeline as _;
302    use camel_api::{ExpressionErrorClass, PipelineOutcome, PredicateSource};
303
304    fn expression_failed() -> CamelError {
305        CamelError::ExpressionFailed {
306            language: "rhai".to_string(),
307            route_id: "r1".to_string(),
308            step_id: "step#0".to_string(),
309            verb: "choice".to_string(),
310            class: ExpressionErrorClass::Runtime,
311            position: None,
312            conversion: None,
313            cause: None,
314        }
315    }
316
317    fn async_err_predicate(err: CamelError) -> PredicateSource {
318        PredicateSource::Async(Arc::new(move |_: &Exchange| {
319            let err = err.clone();
320            Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
321        }))
322    }
323
324    struct RecordRun(Arc<AtomicU32>);
325
326    impl camel_api::OutcomePipeline for RecordRun {
327        fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
328            Box::new(RecordRun(Arc::clone(&self.0)))
329        }
330        fn run<'a>(
331            &'a mut self,
332            exchange: Exchange,
333        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
334            let c = Arc::clone(&self.0);
335            Box::pin(async move {
336                c.fetch_add(1, Ordering::SeqCst);
337                PipelineOutcome::Completed(exchange)
338            })
339        }
340    }
341
342    #[tokio::test]
343    async fn choice_when_predicate_error_fails_step() {
344        let body_calls = Arc::new(AtomicU32::new(0));
345        let otherwise_calls = Arc::new(AtomicU32::new(0));
346        let mut seg = ChoiceSegment {
347            clauses: vec![WhenClauseSegment {
348                predicate: async_err_predicate(expression_failed()),
349                body: camel_api::OutcomeSegment::new(Box::new(RecordRun(Arc::clone(&body_calls)))),
350            }],
351            otherwise: Some(camel_api::OutcomeSegment::new(Box::new(RecordRun(
352                Arc::clone(&otherwise_calls),
353            )))),
354        };
355        let outcome = seg.run(Exchange::default()).await;
356        match outcome {
357            PipelineOutcome::Failed(err) => {
358                assert!(
359                    matches!(err, CamelError::ExpressionFailed { .. }),
360                    "expected ExpressionFailed, got {err:?}"
361                );
362            }
363            other => panic!("expected PipelineOutcome::Failed, got {other:?}"),
364        }
365        assert_eq!(
366            body_calls.load(Ordering::SeqCst),
367            0,
368            "when body must not run when its predicate errors"
369        );
370        assert_eq!(
371            otherwise_calls.load(Ordering::SeqCst),
372            0,
373            "otherwise must never execute when a when-predicate errors"
374        );
375    }
376}