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;
6
7use camel_api::{BoxProcessor, CamelError, Exchange, FilterPredicate};
8
9/// A single when-clause: a predicate + the sub-pipeline to execute when it matches.
10pub struct WhenClause {
11    pub predicate: FilterPredicate,
12    pub pipeline: BoxProcessor,
13}
14
15impl Clone for WhenClause {
16    fn clone(&self) -> Self {
17        Self {
18            predicate: self.predicate.clone(),
19            pipeline: self.pipeline.clone(),
20        }
21    }
22}
23
24/// Tower Service implementing the Choice EIP (Content-Based Router).
25///
26/// Evaluates `when` clauses in order. The first matching predicate routes the
27/// exchange through its sub-pipeline. If no predicate matches, the `otherwise`
28/// pipeline is used (if present); otherwise the exchange passes through unchanged.
29#[derive(Clone)]
30pub struct ChoiceService {
31    whens: Vec<WhenClause>,
32    otherwise: Option<BoxProcessor>,
33}
34
35impl ChoiceService {
36    /// Create a new `ChoiceService`.
37    ///
38    /// `whens` — ordered list of `(predicate, sub_pipeline)` pairs.
39    /// `otherwise` — optional fallback pipeline (executed when no `when` matches).
40    pub fn new(whens: Vec<WhenClause>, otherwise: Option<BoxProcessor>) -> Self {
41        Self { whens, otherwise }
42    }
43}
44
45impl Service<Exchange> for ChoiceService {
46    type Response = Exchange;
47    type Error = CamelError;
48    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
49
50    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
51        Poll::Ready(Ok(()))
52    }
53
54    fn call(&mut self, exchange: Exchange) -> Self::Future {
55        for when in &mut self.whens {
56            if (when.predicate)(&exchange) {
57                let fut = when.pipeline.call(exchange);
58                return Box::pin(fut);
59            }
60        }
61        if let Some(otherwise) = &mut self.otherwise {
62            let fut = otherwise.call(exchange);
63            return Box::pin(fut);
64        }
65        Box::pin(async move { Ok(exchange) })
66    }
67}
68
69// ── ChoiceSegment + WhenClauseSegment (ADR-0025 OutcomePipeline) ──────────
70
71/// Outcome-aware structural EIP segment for a single when clause.
72pub struct WhenClauseSegment {
73    pub predicate: camel_api::FilterPredicate,
74    pub body: camel_api::OutcomeSegment,
75}
76
77impl Clone for WhenClauseSegment {
78    fn clone(&self) -> Self {
79        Self {
80            predicate: self.predicate.clone(),
81            body: self.body.clone(),
82        }
83    }
84}
85
86/// Outcome-aware structural EIP segment for the Choice pattern.
87///
88/// Evaluates `when` clauses in order. The first matching predicate runs its
89/// `body` (which can return `Completed`, `Stopped`, or `Failed`). If no
90/// predicate matches, the `otherwise` segment runs (if present); otherwise
91/// returns `Completed(original_exchange)`.
92///
93/// Unlike `ChoiceService` (which operates at the Tower layer and cannot
94/// preserve `Stopped(ex)` with mutations), `ChoiceSegment` operates at the
95/// `PipelineOutcome` layer and preserves the exchange at the Stop point
96/// including all mutations.
97pub struct ChoiceSegment {
98    pub clauses: Vec<WhenClauseSegment>,
99    pub otherwise: Option<camel_api::OutcomeSegment>,
100}
101
102impl Clone for ChoiceSegment {
103    fn clone(&self) -> Self {
104        Self {
105            clauses: self
106                .clauses
107                .iter()
108                .map(|c| WhenClauseSegment {
109                    predicate: c.predicate.clone(),
110                    body: c.body.clone(),
111                })
112                .collect(),
113            otherwise: self.otherwise.clone(),
114        }
115    }
116}
117
118impl camel_api::OutcomePipeline for ChoiceSegment {
119    fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
120        Box::new(self.clone())
121    }
122
123    fn run<'a>(
124        &'a mut self,
125        exchange: camel_api::Exchange,
126    ) -> Pin<Box<dyn Future<Output = camel_api::PipelineOutcome> + Send + 'a>> {
127        Box::pin(async move {
128            for clause in self.clauses.iter_mut() {
129                if (clause.predicate)(&exchange) {
130                    return clause.body.run(exchange).await;
131                }
132            }
133            if let Some(otherwise) = self.otherwise.as_mut() {
134                otherwise.run(exchange).await
135            } else {
136                camel_api::PipelineOutcome::Completed(exchange)
137            }
138        })
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use camel_api::{Body, BoxProcessorExt, Message, Value};
146    use tower::ServiceExt;
147
148    fn append_body(suffix: &'static str) -> BoxProcessor {
149        BoxProcessor::from_fn(move |mut ex: Exchange| {
150            Box::pin(async move {
151                if let Body::Text(s) = &ex.input.body {
152                    ex.input.body = Body::Text(format!("{s}{suffix}"));
153                }
154                Ok(ex)
155            })
156        })
157    }
158
159    fn failing() -> BoxProcessor {
160        BoxProcessor::from_fn(|_ex| {
161            Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
162        })
163    }
164
165    fn pred_header(name: &'static str) -> FilterPredicate {
166        FilterPredicate::new(move |ex: &Exchange| ex.input.header(name).is_some())
167    }
168
169    // 1. First matching when executes its pipeline.
170    #[tokio::test]
171    async fn test_choice_first_when_matches() {
172        let whens = vec![
173            WhenClause {
174                predicate: pred_header("a"),
175                pipeline: append_body("-A"),
176            },
177            WhenClause {
178                predicate: pred_header("b"),
179                pipeline: append_body("-B"),
180            },
181        ];
182        let mut svc = ChoiceService::new(whens, None);
183        let mut ex = Exchange::new(Message::new("x"));
184        ex.input.set_header("a", Value::Bool(true));
185        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
186        assert_eq!(result.input.body.as_text(), Some("x-A"));
187    }
188
189    // 2. Second when executes when first does not match.
190    #[tokio::test]
191    async fn test_choice_second_when_matches() {
192        let whens = vec![
193            WhenClause {
194                predicate: pred_header("a"),
195                pipeline: append_body("-A"),
196            },
197            WhenClause {
198                predicate: pred_header("b"),
199                pipeline: append_body("-B"),
200            },
201        ];
202        let mut svc = ChoiceService::new(whens, None);
203        let mut ex = Exchange::new(Message::new("x"));
204        ex.input.set_header("b", Value::Bool(true));
205        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
206        assert_eq!(result.input.body.as_text(), Some("x-B"));
207    }
208
209    // 3. Only the FIRST matching when fires (short-circuit — both a and b present).
210    #[tokio::test]
211    async fn test_choice_short_circuits_at_first_match() {
212        let whens = vec![
213            WhenClause {
214                predicate: pred_header("a"),
215                pipeline: append_body("-A"),
216            },
217            WhenClause {
218                predicate: pred_header("b"),
219                pipeline: append_body("-B"),
220            },
221        ];
222        let mut svc = ChoiceService::new(whens, None);
223        let mut ex = Exchange::new(Message::new("x"));
224        ex.input.set_header("a", Value::Bool(true));
225        ex.input.set_header("b", Value::Bool(true));
226        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
227        assert_eq!(result.input.body.as_text(), Some("x-A"));
228    }
229
230    // 4. Otherwise executes when no when matches.
231    #[tokio::test]
232    async fn test_choice_otherwise_fires_when_no_when_matches() {
233        let whens = vec![WhenClause {
234            predicate: pred_header("a"),
235            pipeline: append_body("-A"),
236        }];
237        let mut svc = ChoiceService::new(whens, Some(append_body("-else")));
238        let ex = Exchange::new(Message::new("x"));
239        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
240        assert_eq!(result.input.body.as_text(), Some("x-else"));
241    }
242
243    // 5. No match and no otherwise → exchange passes unchanged.
244    #[tokio::test]
245    async fn test_choice_no_match_no_otherwise_passthrough() {
246        let whens = vec![WhenClause {
247            predicate: pred_header("a"),
248            pipeline: append_body("-A"),
249        }];
250        let mut svc = ChoiceService::new(whens, None);
251        let ex = Exchange::new(Message::new("untouched"));
252        let result = svc.ready().await.unwrap().call(ex).await.unwrap();
253        assert_eq!(result.input.body.as_text(), Some("untouched"));
254    }
255
256    // 6. Errors in a matching when's pipeline propagate.
257    #[tokio::test]
258    async fn test_choice_error_in_when_propagates() {
259        let whens = vec![WhenClause {
260            predicate: pred_header("a"),
261            pipeline: failing(),
262        }];
263        let mut svc = ChoiceService::new(whens, None);
264        let mut ex = Exchange::new(Message::new("x"));
265        ex.input.set_header("a", Value::Bool(true));
266        let result = svc.ready().await.unwrap().call(ex).await;
267        assert!(result.is_err());
268        assert!(result.unwrap_err().to_string().contains("boom"));
269    }
270
271    // 7. Errors in otherwise pipeline propagate.
272    #[tokio::test]
273    async fn test_choice_error_in_otherwise_propagates() {
274        let mut svc = ChoiceService::new(vec![], Some(failing()));
275        let ex = Exchange::new(Message::new("x"));
276        let result = svc.ready().await.unwrap().call(ex).await;
277        assert!(result.is_err());
278    }
279}