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, CamelError, FilterPredicate, OpaqueProcessor};
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    /// # Errors
103    ///
104    /// Returns `Err(CamelError::RouteError(_))` if `do_finally` has already
105    /// been called on this scope.
106    pub fn do_finally(self) -> Result<DoFinallyBuilder, CamelError> {
107        if self.finally_set {
108            return Err(CamelError::RouteError(
109                "do_finally can only be called once per do_try scope".into(),
110            ));
111        }
112        Ok(DoFinallyBuilder {
113            parent: self,
114            steps: Vec::new(),
115            on_when: None,
116        })
117    }
118
119    /// Close the `doTry` scope and return the parent `RouteBuilder`.
120    pub fn end_do_try(self) -> RouteBuilder {
121        let do_try = DoTryService {
122            try_steps: self.try_steps,
123            catch_clauses: self.catch_clauses,
124            finally_steps: self.finally_steps,
125            finally_on_when: self.finally_on_when,
126        };
127        let mut parent = self.parent;
128        parent
129            .steps
130            .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
131                do_try,
132            ))));
133        parent
134    }
135}
136
137impl DoCatchBuilder {
138    /// Add a step to the catch clause's sub-pipeline.
139    pub fn process(mut self, processor: BoxProcessor) -> Self {
140        self.steps.push(processor);
141        self
142    }
143
144    /// Set an additional predicate that must also match for this catch clause to fire.
145    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
146        self.on_when = Some(predicate);
147        self
148    }
149
150    /// Mark this catch clause as handled: the caught error is absorbed
151    /// and the clause's exchange becomes the final result (no re-throw).
152    ///
153    /// Only `Handled` and `Propagate` are supported. There is intentionally
154    /// no general `disposition(value)` setter, so `Continued` is
155    /// unrepresentable at the type level.
156    ///
157    /// Valid use (compiles):
158    ///
159    /// ```
160    /// # use camel_builder::RouteBuilder;
161    /// let _ = RouteBuilder::from("direct:start").route_id("x").do_try()
162    ///     .do_catch_exception(&["E"])
163    ///     .handled();
164    /// ```
165    ///
166    /// Rejected — does not compile (no `disposition` method exists):
167    ///
168    /// ```compile_fail
169    /// # use camel_builder::RouteBuilder;
170    /// # use camel_api::error_handler::ExceptionDisposition;
171    /// let b = RouteBuilder::from("direct:start").route_id("x").do_try()
172    ///     .do_catch_exception(&["E"]);
173    /// b.disposition(ExceptionDisposition::Continued);
174    /// ```
175    pub fn handled(mut self) -> Self {
176        self.disposition = ExceptionDisposition::Handled;
177        self
178    }
179
180    /// Mark this catch clause as propagating: the clause runs for
181    /// side-effects and the original error is re-thrown.
182    ///
183    /// Only `Handled` and `Propagate` are supported. There is intentionally
184    /// no general `disposition(value)` setter, so `Continued` is
185    /// unrepresentable at the type level (semantically ambiguous at
186    /// catch-clause scope; `.continued()` is deliberately not provided).
187    pub fn propagate(mut self) -> Self {
188        self.disposition = ExceptionDisposition::Propagate;
189        self
190    }
191
192    /// Close the catch clause and return the parent `DoTryBuilder`.
193    pub fn end_do_catch(self) -> DoTryBuilder {
194        let mut parent = self.parent;
195        parent.catch_clauses.push(CatchClause {
196            matcher: self.matcher,
197            on_when: self.on_when,
198            steps: self.steps,
199            disposition: self.disposition,
200        });
201        parent
202    }
203}
204
205impl DoFinallyBuilder {
206    /// Add a step to the finally block.
207    pub fn process(mut self, processor: BoxProcessor) -> Self {
208        self.steps.push(processor);
209        self
210    }
211
212    /// Set an optional predicate that gates whether the finally block runs.
213    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
214        self.on_when = Some(predicate);
215        self
216    }
217
218    /// Close the finally block and return the parent `DoTryBuilder`.
219    pub fn end_do_finally(self) -> DoTryBuilder {
220        let mut parent = self.parent;
221        parent.finally_set = true;
222        parent.finally_on_when = self.on_when;
223        parent.finally_steps = self.steps;
224        parent
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use crate::RouteBuilder;
231    use camel_api::error_handler::ExceptionDisposition;
232    use camel_api::{BoxProcessor, BoxProcessorExt, CamelError};
233    use camel_core::route::BuilderStep;
234
235    fn passthrough() -> BoxProcessor {
236        BoxProcessor::from_fn(move |ex| Box::pin(async move { Ok(ex) }))
237    }
238
239    #[test]
240    fn do_try_builder_assembles_correct_shape() {
241        let route = RouteBuilder::from("direct:start")
242            .route_id("do-try-shape")
243            .do_try()
244            .process(passthrough())
245            .do_catch_exception(&["ProcessorError"])
246            .handled()
247            .process(passthrough())
248            .end_do_catch()
249            .do_finally()
250            .unwrap()
251            .process(passthrough())
252            .end_do_finally()
253            .end_do_try();
254
255        let config = route.build().unwrap();
256        assert_eq!(
257            config.steps().len(),
258            1,
259            "expected exactly one step (the DoTryService)"
260        );
261        assert!(
262            matches!(config.steps().first(), Some(BuilderStep::Processor(_))),
263            "the single step must be a Processor variant (the DoTryService)"
264        );
265    }
266
267    #[test]
268    fn do_try_builder_disposition_sugar_methods() {
269        // handled route
270        let catch = RouteBuilder::from("direct:a")
271            .route_id("do-try-sugar-a")
272            .do_try()
273            .process(passthrough())
274            .do_catch_exception(&["Io"])
275            .handled();
276        assert_eq!(catch.disposition, ExceptionDisposition::Handled);
277        let _ = catch.end_do_catch().end_do_try().build().unwrap();
278
279        // propagate route
280        let catch = RouteBuilder::from("direct:b")
281            .route_id("do-try-sugar-b")
282            .do_try()
283            .process(passthrough())
284            .do_catch_exception(&["Io"])
285            .propagate();
286        assert_eq!(catch.disposition, ExceptionDisposition::Propagate);
287        let _ = catch.end_do_catch().end_do_try().build().unwrap();
288    }
289
290    #[test]
291    fn do_finally_called_twice_returns_err() {
292        let result = RouteBuilder::from("direct:start")
293            .route_id("do-try-double-finally")
294            .do_try()
295            .process(passthrough())
296            .do_finally()
297            .unwrap()
298            .process(passthrough())
299            .end_do_finally()
300            .do_finally();
301        match result {
302            Err(CamelError::RouteError(msg)) => {
303                assert!(
304                    msg.contains("do_finally can only be called once"),
305                    "unexpected message: {msg}"
306                );
307            }
308            _ => panic!("expected Err(RouteError)"),
309        }
310    }
311}