1use crate::do_try::CatchMatcher;
7use camel_api::error_handler::ExceptionDisposition;
8use camel_api::outcome_pipeline::OutcomePipeline;
9use camel_api::pipeline_outcome::PipelineOutcome;
10use camel_api::{CamelError, Exchange, FilterPredicate};
11use std::future::Future;
12use std::pin::Pin;
13
14#[derive(Clone)]
18pub struct CatchClauseSegment {
19 pub matcher: CatchMatcher,
20 pub on_when: Option<FilterPredicate>,
21 pub body: camel_api::OutcomeSegment,
22 pub disposition: ExceptionDisposition,
23}
24
25#[derive(Clone)]
27pub struct FinallyClauseSegment {
28 pub on_when: Option<FilterPredicate>,
29 pub body: camel_api::OutcomeSegment,
30}
31
32pub struct DoTrySegment {
38 pub try_body: camel_api::OutcomeSegment,
39 pub catches: Vec<CatchClauseSegment>,
40 pub finally: Option<FinallyClauseSegment>,
41}
42
43impl Clone for DoTrySegment {
44 fn clone(&self) -> Self {
45 Self {
46 try_body: self.try_body.clone(),
47 catches: self.catches.clone(),
48 finally: self.finally.clone(),
49 }
50 }
51}
52
53enum FinallyOutcome {
58 Stopped(Box<Exchange>),
59 Failed(CamelError),
60}
61
62#[allow(clippy::result_large_err)]
69async fn run_finally_body(
70 finally: &mut Option<FinallyClauseSegment>,
71 ex: Exchange,
72) -> Result<Exchange, FinallyOutcome> {
73 let Some(f) = finally.as_mut() else {
74 return Ok(ex);
75 };
76 if !f.on_when.as_ref().map(|p| p(&ex)).unwrap_or(true) {
77 return Ok(ex);
78 }
79 match f.body.run(ex).await {
80 PipelineOutcome::Completed(e) => Ok(e),
81 PipelineOutcome::Stopped(e) => Err(FinallyOutcome::Stopped(Box::new(e))),
82 PipelineOutcome::Failed(e) => Err(FinallyOutcome::Failed(e)),
83 }
84}
85
86impl OutcomePipeline for DoTrySegment {
87 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
88 Box::new(self.clone())
89 }
90
91 fn run<'a>(
92 &'a mut self,
93 exchange: Exchange,
94 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
95 Box::pin(async move {
96 let exchange_for_unmatched = exchange.clone();
99
100 let try_outcome = self.try_body.run(exchange).await;
102 let returned_ex = match try_outcome {
103 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
104 PipelineOutcome::Completed(ex) => ex,
105 PipelineOutcome::Failed(err) => {
106 let mut current_ex = exchange_for_unmatched;
110 current_ex.set_error(err.clone());
111 for catch in self.catches.iter_mut() {
112 if catch.matcher.matches(&err, ¤t_ex)
113 && catch
114 .on_when
115 .as_ref()
116 .map(|p| p(¤t_ex))
117 .unwrap_or(true)
118 {
119 match catch.body.run(current_ex).await {
120 PipelineOutcome::Stopped(stopped_ex) => {
122 return PipelineOutcome::Stopped(stopped_ex);
123 }
124 PipelineOutcome::Completed(next) => {
126 match catch.disposition {
127 ExceptionDisposition::Handled => {
128 current_ex = next;
129 break;
130 }
131 ExceptionDisposition::Continued => {
132 current_ex = next;
133 break;
134 }
135 _ => {
138 match run_finally_body(&mut self.finally, next).await {
139 Ok(_) => {}
140 Err(FinallyOutcome::Stopped(e)) => {
141 return PipelineOutcome::Stopped(*e);
142 }
143 Err(FinallyOutcome::Failed(_finally_err)) => {
144 tracing::warn!(
145 error = %err,
146 "doFinally threw during Propagate; \
147 restoring original"
148 );
149 return PipelineOutcome::Failed(err);
150 }
151 }
152 return PipelineOutcome::Failed(err);
153 }
154 }
155 }
156 PipelineOutcome::Failed(catch_err) => {
163 return PipelineOutcome::Failed(catch_err);
164 }
165 }
166 }
167 }
168 current_ex
169 }
170 };
171 match run_finally_body(&mut self.finally, returned_ex).await {
174 Ok(ex) => PipelineOutcome::Completed(ex),
175 Err(FinallyOutcome::Stopped(e)) => PipelineOutcome::Stopped(*e),
176 Err(FinallyOutcome::Failed(finally_err)) => {
177 tracing::warn!(
178 error = %finally_err,
179 "doFinally threw during/after catch; surfacing finally error"
180 );
181 PipelineOutcome::Failed(finally_err)
182 }
183 }
184 })
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::do_try::CatchMatcher;
192 use camel_api::pipeline_outcome::PipelineOutcome;
193 use std::sync::Arc;
194 use std::sync::atomic::{AtomicU32, Ordering};
195
196 struct CompleteSegment;
199
200 impl OutcomePipeline for CompleteSegment {
201 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
202 Box::new(CompleteSegment)
203 }
204 fn run<'a>(
205 &'a mut self,
206 exchange: Exchange,
207 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
208 Box::pin(async move { PipelineOutcome::Completed(exchange) })
209 }
210 }
211
212 fn seg_complete() -> camel_api::OutcomeSegment {
213 camel_api::OutcomeSegment::new(Box::new(CompleteSegment))
214 }
215
216 struct FailSegment(CamelError);
217
218 impl OutcomePipeline for FailSegment {
219 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
220 Box::new(FailSegment(self.0.clone()))
221 }
222 fn run<'a>(
223 &'a mut self,
224 _exchange: Exchange,
225 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
226 let e = self.0.clone();
227 Box::pin(async move { PipelineOutcome::Failed(e) })
228 }
229 }
230
231 fn seg_fail(err: CamelError) -> camel_api::OutcomeSegment {
232 camel_api::OutcomeSegment::new(Box::new(FailSegment(err)))
233 }
234
235 struct MutateThenStop {
236 mutator: Arc<dyn Fn(&mut Exchange) + Send + Sync>,
237 }
238
239 impl OutcomePipeline for MutateThenStop {
240 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
241 Box::new(MutateThenStop {
242 mutator: Arc::clone(&self.mutator),
243 })
244 }
245 fn run<'a>(
246 &'a mut self,
247 mut exchange: Exchange,
248 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
249 let m = Arc::clone(&self.mutator);
250 Box::pin(async move {
251 m(&mut exchange);
252 PipelineOutcome::Stopped(exchange)
253 })
254 }
255 }
256
257 fn seg_stop_with(
258 mutator: impl Fn(&mut Exchange) + Send + Sync + 'static,
259 ) -> camel_api::OutcomeSegment {
260 camel_api::OutcomeSegment::new(Box::new(MutateThenStop {
261 mutator: Arc::new(mutator),
262 }))
263 }
264
265 struct RecordCall {
266 counter: Arc<AtomicU32>,
267 }
268
269 impl OutcomePipeline for RecordCall {
270 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
271 Box::new(RecordCall {
272 counter: Arc::clone(&self.counter),
273 })
274 }
275 fn run<'a>(
276 &'a mut self,
277 exchange: Exchange,
278 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
279 let c = Arc::clone(&self.counter);
280 Box::pin(async move {
281 c.fetch_add(1, Ordering::SeqCst);
282 PipelineOutcome::Completed(exchange)
283 })
284 }
285 }
286
287 fn seg_record(counter: Arc<AtomicU32>) -> camel_api::OutcomeSegment {
288 camel_api::OutcomeSegment::new(Box::new(RecordCall { counter }))
289 }
290
291 #[tokio::test]
294 async fn stop_inside_try_skips_catch_and_finally() {
295 let catch_call = Arc::new(AtomicU32::new(0));
296 let finally_call = Arc::new(AtomicU32::new(0));
297
298 let mut seg = DoTrySegment {
299 try_body: seg_stop_with(|ex| {
300 ex.set_property("mutated", camel_api::Value::Bool(true));
301 }),
302 catches: vec![CatchClauseSegment {
303 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
304 on_when: None,
305 body: seg_record(catch_call.clone()),
306 disposition: ExceptionDisposition::Handled,
307 }],
308 finally: Some(FinallyClauseSegment {
309 on_when: None,
310 body: seg_record(finally_call.clone()),
311 }),
312 };
313
314 let result = seg.run(Exchange::default()).await;
315 match result {
316 PipelineOutcome::Stopped(ex) => {
317 assert_eq!(
318 ex.properties.get("mutated"),
319 Some(&camel_api::Value::Bool(true)),
320 "try body mutation must be preserved in Stopped exchange"
321 );
322 }
323 other => panic!("expected Stopped, got {:?}", other),
324 }
325 assert_eq!(
326 catch_call.load(Ordering::SeqCst),
327 0,
328 "catch must NOT run when try stops"
329 );
330 assert_eq!(
331 finally_call.load(Ordering::SeqCst),
332 0,
333 "finally must NOT run when try stops"
334 );
335 }
336
337 #[tokio::test]
338 async fn stop_inside_catch_skips_finally() {
339 let finally_call = Arc::new(AtomicU32::new(0));
340
341 let mut seg = DoTrySegment {
342 try_body: seg_fail(CamelError::ProcessorError("boom".into())),
343 catches: vec![CatchClauseSegment {
344 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
345 on_when: None,
346 body: seg_stop_with(|ex| {
347 ex.set_property("catch_mutated", camel_api::Value::Bool(true));
348 }),
349 disposition: ExceptionDisposition::Handled,
350 }],
351 finally: Some(FinallyClauseSegment {
352 on_when: None,
353 body: seg_record(finally_call.clone()),
354 }),
355 };
356
357 let result = seg.run(Exchange::default()).await;
358 match result {
359 PipelineOutcome::Stopped(ex) => {
360 assert_eq!(
361 ex.properties.get("catch_mutated"),
362 Some(&camel_api::Value::Bool(true)),
363 "catch body mutation must be preserved in Stopped exchange"
364 );
365 }
366 other => panic!("expected Stopped, got {:?}", other),
367 }
368 assert_eq!(
369 finally_call.load(Ordering::SeqCst),
370 0,
371 "finally must NOT run when catch stops"
372 );
373 }
374
375 #[tokio::test]
376 async fn stop_inside_finally_stops_outer_route() {
377 let mut seg = DoTrySegment {
378 try_body: seg_complete(),
379 catches: vec![],
380 finally: Some(FinallyClauseSegment {
381 on_when: None,
382 body: seg_stop_with(|ex| {
383 ex.set_property("finally_mutated", camel_api::Value::Bool(true));
384 }),
385 }),
386 };
387
388 let result = seg.run(Exchange::default()).await;
389 match result {
390 PipelineOutcome::Stopped(ex) => {
391 assert_eq!(
392 ex.properties.get("finally_mutated"),
393 Some(&camel_api::Value::Bool(true)),
394 "finally body mutation must be preserved in Stopped exchange"
395 );
396 }
397 other => panic!("expected Stopped, got {:?}", other),
398 }
399 }
400
401 #[tokio::test]
402 async fn catch_on_when_false_falls_through_to_next_catch() {
403 let first_call = Arc::new(AtomicU32::new(0));
404 let second_call = Arc::new(AtomicU32::new(0));
405
406 let mut seg = DoTrySegment {
407 try_body: seg_fail(CamelError::Io("disk err".into())),
408 catches: vec![
409 CatchClauseSegment {
410 matcher: CatchMatcher::ByVariant(vec!["Io".into()]),
411 on_when: Some(FilterPredicate::new(|_ex| false)),
412 body: seg_record(first_call.clone()),
413 disposition: ExceptionDisposition::Handled,
414 },
415 CatchClauseSegment {
416 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
417 on_when: None,
418 body: seg_record(second_call.clone()),
419 disposition: ExceptionDisposition::Handled,
420 },
421 ],
422 finally: None,
423 };
424
425 let result = seg.run(Exchange::default()).await;
426 assert!(
427 matches!(result, PipelineOutcome::Completed(_)),
428 "expected Completed after second catch"
429 );
430 assert_eq!(
431 first_call.load(Ordering::SeqCst),
432 0,
433 "first catch must NOT fire (on_when=false)"
434 );
435 assert_eq!(
436 second_call.load(Ordering::SeqCst),
437 1,
438 "second catch must fire"
439 );
440 }
441
442 #[tokio::test]
443 async fn finally_on_when_false_skips_finally_entirely() {
444 let finally_call = Arc::new(AtomicU32::new(0));
445 let mut ex = Exchange::default();
446 ex.set_property("try_set", camel_api::Value::Bool(true));
447
448 let mut seg = DoTrySegment {
449 try_body: seg_complete(),
450 catches: vec![],
451 finally: Some(FinallyClauseSegment {
452 on_when: Some(FilterPredicate::new(|_ex| false)),
453 body: seg_record(finally_call.clone()),
454 }),
455 };
456
457 let result = seg.run(ex).await;
458 match result {
459 PipelineOutcome::Completed(ex) => {
460 assert_eq!(
461 ex.properties.get("try_set"),
462 Some(&camel_api::Value::Bool(true)),
463 "exchange state from try must be preserved"
464 );
465 }
466 other => panic!("expected Completed, got {:?}", other),
467 }
468 assert_eq!(
469 finally_call.load(Ordering::SeqCst),
470 0,
471 "finally must NOT run when on_when=false"
472 );
473 }
474}