Skip to main content

camel_builder/
do_try.rs

1//! Builder types for the `doTry` / `doCatch` / `doFinally` EIP pattern.
2//!
3//! These builders provide a fluent API for constructing doTry scopes within
4//! a Camel route. Example:
5//!
6//! ```ignore
7//! RouteBuilder::from("direct:start")
8//!     .do_try()
9//!         .process(try_step)
10//!         .do_catch_exception(&["SomeError"])
11//!             .process(catch_step)
12//!         .end_do_catch()
13//!     .end_do_try()
14//! ```
15
16use crate::RouteBuilder;
17use camel_api::error_handler::ExceptionDisposition;
18use camel_api::{BoxProcessor, FilterPredicate};
19use camel_core::route::BuilderStep;
20use camel_processor::{CatchClause, CatchMatcher, DoTryService};
21
22// ── doTry / doCatch / doFinally builders ────────────────────────────────────
23
24/// Builder for a `.do_try()` ... `.end_do_try()` block.
25pub struct DoTryBuilder {
26    parent: RouteBuilder,
27    try_steps: Vec<BoxProcessor>,
28    catch_clauses: Vec<CatchClause>,
29    finally_steps: Vec<BoxProcessor>,
30    finally_on_when: Option<FilterPredicate>,
31    finally_set: bool,
32}
33
34/// Builder for a `.do_catch_exception()` / `.do_catch_when()` / `.do_catch_all()` clause.
35pub struct DoCatchBuilder {
36    parent: DoTryBuilder,
37    matcher: CatchMatcher,
38    on_when: Option<FilterPredicate>,
39    steps: Vec<BoxProcessor>,
40    disposition: ExceptionDisposition,
41}
42
43/// Builder for a `.do_finally()` ... `.end_do_finally()` block.
44pub struct DoFinallyBuilder {
45    parent: DoTryBuilder,
46    steps: Vec<BoxProcessor>,
47    on_when: Option<FilterPredicate>,
48}
49
50impl RouteBuilder {
51    /// Open a `doTry` scope. Steps inside are protected by catch and finally clauses.
52    pub fn do_try(self) -> DoTryBuilder {
53        DoTryBuilder {
54            parent: self,
55            try_steps: Vec::new(),
56            catch_clauses: Vec::new(),
57            finally_steps: Vec::new(),
58            finally_on_when: None,
59            finally_set: false,
60        }
61    }
62}
63
64impl DoTryBuilder {
65    /// Add a step to the try block.
66    pub fn process(mut self, processor: BoxProcessor) -> Self {
67        self.try_steps.push(processor);
68        self
69    }
70
71    /// Open a catch clause that matches errors by variant name(s).
72    ///
73    /// Use `"*"` to match any variant (catch-all).
74    pub fn do_catch_exception(self, variants: &[&str]) -> DoCatchBuilder {
75        DoCatchBuilder {
76            parent: self,
77            matcher: CatchMatcher::ByVariant(variants.iter().map(|s| (*s).to_string()).collect()),
78            on_when: None,
79            steps: Vec::new(),
80            disposition: ExceptionDisposition::Handled,
81        }
82    }
83
84    /// Open a catch clause that matches errors by a predicate over the exchange.
85    pub fn do_catch_when(self, predicate: FilterPredicate) -> DoCatchBuilder {
86        DoCatchBuilder {
87            parent: self,
88            matcher: CatchMatcher::Predicate(predicate),
89            on_when: None,
90            steps: Vec::new(),
91            disposition: ExceptionDisposition::Handled,
92        }
93    }
94
95    /// Open a catch-all clause (matches any error variant).
96    pub fn do_catch_all(self) -> DoCatchBuilder {
97        self.do_catch_exception(&["*"])
98    }
99
100    /// Open a `doFinally` block.
101    ///
102    /// # Panics
103    /// Panics if `do_finally` has already been called on this scope.
104    pub fn do_finally(self) -> DoFinallyBuilder {
105        if self.finally_set {
106            panic!("do_finally can only be called once per do_try scope");
107        }
108        DoFinallyBuilder {
109            parent: self,
110            steps: Vec::new(),
111            on_when: None,
112        }
113    }
114
115    /// Close the `doTry` scope and return the parent `RouteBuilder`.
116    pub fn end_do_try(self) -> RouteBuilder {
117        let do_try = DoTryService {
118            try_steps: self.try_steps,
119            catch_clauses: self.catch_clauses,
120            finally_steps: self.finally_steps,
121            finally_on_when: self.finally_on_when,
122        };
123        let mut parent = self.parent;
124        parent
125            .steps
126            .push(BuilderStep::Processor(BoxProcessor::new(do_try)));
127        parent
128    }
129}
130
131impl DoCatchBuilder {
132    /// Add a step to the catch clause's sub-pipeline.
133    pub fn process(mut self, processor: BoxProcessor) -> Self {
134        self.steps.push(processor);
135        self
136    }
137
138    /// Set an additional predicate that must also match for this catch clause to fire.
139    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
140        self.on_when = Some(predicate);
141        self
142    }
143
144    /// Set the disposition for this catch clause.
145    ///
146    /// # Panics
147    /// Panics if `value` is `ExceptionDisposition::Continued`, which is not
148    /// supported in doTry MVP (spec §3).
149    pub fn disposition(mut self, value: ExceptionDisposition) -> Self {
150        if matches!(value, ExceptionDisposition::Continued) {
151            panic!(
152                "ExceptionDisposition::Continued is not supported in doTry MVP (spec §3); \
153                 use Handled or Propagate"
154            );
155        }
156        self.disposition = value;
157        self
158    }
159
160    /// Sugar for `disposition(ExceptionDisposition::Handled)`.
161    ///
162    /// The caught error is marked handled and the catch clause's exchange
163    /// becomes the final result (no re-throw).
164    pub fn handled(self) -> Self {
165        self.disposition(ExceptionDisposition::Handled)
166    }
167
168    /// Sugar for `disposition(ExceptionDisposition::Propagate)`.
169    ///
170    /// The catch clause runs for side-effects and the original error is
171    /// re-thrown.
172    ///
173    /// Note: `.continued()` is intentionally NOT provided — Continued is
174    /// rejected at parse time for doTry MVP per spec §3 (semantically
175    /// ambiguous at catch-clause scope).
176    pub fn propagate(self) -> Self {
177        self.disposition(ExceptionDisposition::Propagate)
178    }
179
180    /// Close the catch clause and return the parent `DoTryBuilder`.
181    pub fn end_do_catch(self) -> DoTryBuilder {
182        let mut parent = self.parent;
183        parent.catch_clauses.push(CatchClause {
184            matcher: self.matcher,
185            on_when: self.on_when,
186            steps: self.steps,
187            disposition: self.disposition,
188        });
189        parent
190    }
191}
192
193impl DoFinallyBuilder {
194    /// Add a step to the finally block.
195    pub fn process(mut self, processor: BoxProcessor) -> Self {
196        self.steps.push(processor);
197        self
198    }
199
200    /// Set an optional predicate that gates whether the finally block runs.
201    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
202        self.on_when = Some(predicate);
203        self
204    }
205
206    /// Close the finally block and return the parent `DoTryBuilder`.
207    pub fn end_do_finally(self) -> DoTryBuilder {
208        let mut parent = self.parent;
209        parent.finally_set = true;
210        parent.finally_on_when = self.on_when;
211        parent.finally_steps = self.steps;
212        parent
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use crate::RouteBuilder;
219    use camel_api::error_handler::ExceptionDisposition;
220    use camel_api::{BoxProcessor, BoxProcessorExt};
221    use camel_core::route::BuilderStep;
222
223    fn passthrough() -> BoxProcessor {
224        BoxProcessor::from_fn(move |ex| Box::pin(async move { Ok(ex) }))
225    }
226
227    #[test]
228    fn do_try_builder_assembles_correct_shape() {
229        let route = RouteBuilder::from("direct:start")
230            .route_id("do-try-shape")
231            .do_try()
232            .process(passthrough())
233            .do_catch_exception(&["ProcessorError"])
234            .disposition(ExceptionDisposition::Handled)
235            .process(passthrough())
236            .end_do_catch()
237            .do_finally()
238            .process(passthrough())
239            .end_do_finally()
240            .end_do_try();
241
242        let config = route.build().unwrap();
243        assert_eq!(
244            config.steps().len(),
245            1,
246            "expected exactly one step (the DoTryService)"
247        );
248        assert!(
249            matches!(config.steps().first(), Some(BuilderStep::Processor(_))),
250            "the single step must be a Processor variant (the DoTryService)"
251        );
252    }
253
254    #[test]
255    fn do_try_builder_disposition_sugar_methods() {
256        // .handled() and .propagate() are syntactic sugar for the two supported dispositions.
257        // .continued() is intentionally NOT provided — Continued is rejected at parse time
258        // for doTry MVP per spec §3.
259        let _ = RouteBuilder::from("direct:a")
260            .route_id("do-try-sugar-a")
261            .do_try()
262            .process(passthrough())
263            .do_catch_exception(&["Io"])
264            .handled()
265            .end_do_catch()
266            .end_do_try()
267            .build()
268            .unwrap();
269
270        let _ = RouteBuilder::from("direct:b")
271            .route_id("do-try-sugar-b")
272            .do_try()
273            .process(passthrough())
274            .do_catch_exception(&["Io"])
275            .propagate()
276            .end_do_catch()
277            .end_do_try()
278            .build()
279            .unwrap();
280    }
281
282    #[test]
283    #[should_panic(expected = "do_finally can only be called once per do_try scope")]
284    fn do_finally_called_twice_panics() {
285        let _ = RouteBuilder::from("direct:start")
286            .route_id("do-try-double-finally")
287            .do_try()
288            .process(passthrough())
289            .do_finally()
290            .process(passthrough())
291            .end_do_finally()
292            .do_finally();
293    }
294
295    #[test]
296    #[should_panic(expected = "ExceptionDisposition::Continued is not supported in doTry MVP")]
297    fn disposition_continued_panics() {
298        let _ = RouteBuilder::from("direct:start")
299            .route_id("do-try-continued")
300            .do_try()
301            .process(passthrough())
302            .do_catch_exception(&["ProcessorError"])
303            .disposition(ExceptionDisposition::Continued)
304            .end_do_catch()
305            .end_do_try();
306    }
307}