Skip to main content

camel_processor/
choice.rs

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