1use std::future::Future;
2use std::pin::Pin;
3
4use camel_api::{BoxProcessor, PredicateSource};
5
6pub 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
22pub 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
39pub 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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}