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, 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    /// # 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(OpaqueProcessor(BoxProcessor::new(
127                do_try,
128            ))));
129        parent
130    }
131}
132
133impl DoCatchBuilder {
134    /// Add a step to the catch clause's sub-pipeline.
135    pub fn process(mut self, processor: BoxProcessor) -> Self {
136        self.steps.push(processor);
137        self
138    }
139
140    /// Set an additional predicate that must also match for this catch clause to fire.
141    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
142        self.on_when = Some(predicate);
143        self
144    }
145
146    /// Set the disposition for this catch clause.
147    ///
148    /// # Panics
149    /// Panics if `value` is `ExceptionDisposition::Continued`, which is not
150    /// supported in doTry MVP (spec §3).
151    pub fn disposition(mut self, value: ExceptionDisposition) -> Self {
152        if matches!(value, ExceptionDisposition::Continued) {
153            panic!(
154                "ExceptionDisposition::Continued is not supported in doTry MVP (spec §3); \
155                 use Handled or Propagate"
156            );
157        }
158        self.disposition = value;
159        self
160    }
161
162    /// Sugar for `disposition(ExceptionDisposition::Handled)`.
163    ///
164    /// The caught error is marked handled and the catch clause's exchange
165    /// becomes the final result (no re-throw).
166    pub fn handled(self) -> Self {
167        self.disposition(ExceptionDisposition::Handled)
168    }
169
170    /// Sugar for `disposition(ExceptionDisposition::Propagate)`.
171    ///
172    /// The catch clause runs for side-effects and the original error is
173    /// re-thrown.
174    ///
175    /// Note: `.continued()` is intentionally NOT provided — Continued is
176    /// rejected at parse time for doTry MVP per spec §3 (semantically
177    /// ambiguous at catch-clause scope).
178    pub fn propagate(self) -> Self {
179        self.disposition(ExceptionDisposition::Propagate)
180    }
181
182    /// Close the catch clause and return the parent `DoTryBuilder`.
183    pub fn end_do_catch(self) -> DoTryBuilder {
184        let mut parent = self.parent;
185        parent.catch_clauses.push(CatchClause {
186            matcher: self.matcher,
187            on_when: self.on_when,
188            steps: self.steps,
189            disposition: self.disposition,
190        });
191        parent
192    }
193}
194
195impl DoFinallyBuilder {
196    /// Add a step to the finally block.
197    pub fn process(mut self, processor: BoxProcessor) -> Self {
198        self.steps.push(processor);
199        self
200    }
201
202    /// Set an optional predicate that gates whether the finally block runs.
203    pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
204        self.on_when = Some(predicate);
205        self
206    }
207
208    /// Close the finally block and return the parent `DoTryBuilder`.
209    pub fn end_do_finally(self) -> DoTryBuilder {
210        let mut parent = self.parent;
211        parent.finally_set = true;
212        parent.finally_on_when = self.on_when;
213        parent.finally_steps = self.steps;
214        parent
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use crate::RouteBuilder;
221    use camel_api::error_handler::ExceptionDisposition;
222    use camel_api::{BoxProcessor, BoxProcessorExt};
223    use camel_core::route::BuilderStep;
224
225    fn passthrough() -> BoxProcessor {
226        BoxProcessor::from_fn(move |ex| Box::pin(async move { Ok(ex) }))
227    }
228
229    #[test]
230    fn do_try_builder_assembles_correct_shape() {
231        let route = RouteBuilder::from("direct:start")
232            .route_id("do-try-shape")
233            .do_try()
234            .process(passthrough())
235            .do_catch_exception(&["ProcessorError"])
236            .disposition(ExceptionDisposition::Handled)
237            .process(passthrough())
238            .end_do_catch()
239            .do_finally()
240            .process(passthrough())
241            .end_do_finally()
242            .end_do_try();
243
244        let config = route.build().unwrap();
245        assert_eq!(
246            config.steps().len(),
247            1,
248            "expected exactly one step (the DoTryService)"
249        );
250        assert!(
251            matches!(config.steps().first(), Some(BuilderStep::Processor(_))),
252            "the single step must be a Processor variant (the DoTryService)"
253        );
254    }
255
256    #[test]
257    fn do_try_builder_disposition_sugar_methods() {
258        // .handled() and .propagate() are syntactic sugar for the two supported dispositions.
259        // .continued() is intentionally NOT provided — Continued is rejected at parse time
260        // for doTry MVP per spec §3.
261        let _ = RouteBuilder::from("direct:a")
262            .route_id("do-try-sugar-a")
263            .do_try()
264            .process(passthrough())
265            .do_catch_exception(&["Io"])
266            .handled()
267            .end_do_catch()
268            .end_do_try()
269            .build()
270            .unwrap();
271
272        let _ = RouteBuilder::from("direct:b")
273            .route_id("do-try-sugar-b")
274            .do_try()
275            .process(passthrough())
276            .do_catch_exception(&["Io"])
277            .propagate()
278            .end_do_catch()
279            .end_do_try()
280            .build()
281            .unwrap();
282    }
283
284    #[test]
285    #[should_panic(expected = "do_finally can only be called once per do_try scope")]
286    fn do_finally_called_twice_panics() {
287        let _ = RouteBuilder::from("direct:start")
288            .route_id("do-try-double-finally")
289            .do_try()
290            .process(passthrough())
291            .do_finally()
292            .process(passthrough())
293            .end_do_finally()
294            .do_finally();
295    }
296
297    #[test]
298    #[should_panic(expected = "ExceptionDisposition::Continued is not supported in doTry MVP")]
299    fn disposition_continued_panics() {
300        let _ = RouteBuilder::from("direct:start")
301            .route_id("do-try-continued")
302            .do_try()
303            .process(passthrough())
304            .do_catch_exception(&["ProcessorError"])
305            .disposition(ExceptionDisposition::Continued)
306            .end_do_catch()
307            .end_do_try();
308    }
309}