1use 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
22pub 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
34pub struct DoCatchBuilder {
36 parent: DoTryBuilder,
37 matcher: CatchMatcher,
38 on_when: Option<FilterPredicate>,
39 steps: Vec<BoxProcessor>,
40 disposition: ExceptionDisposition,
41}
42
43pub struct DoFinallyBuilder {
45 parent: DoTryBuilder,
46 steps: Vec<BoxProcessor>,
47 on_when: Option<FilterPredicate>,
48}
49
50impl RouteBuilder {
51 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 pub fn process(mut self, processor: BoxProcessor) -> Self {
67 self.try_steps.push(processor);
68 self
69 }
70
71 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 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 pub fn do_catch_all(self) -> DoCatchBuilder {
97 self.do_catch_exception(&["*"])
98 }
99
100 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 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 pub fn process(mut self, processor: BoxProcessor) -> Self {
134 self.steps.push(processor);
135 self
136 }
137
138 pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
140 self.on_when = Some(predicate);
141 self
142 }
143
144 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 pub fn handled(self) -> Self {
165 self.disposition(ExceptionDisposition::Handled)
166 }
167
168 pub fn propagate(self) -> Self {
177 self.disposition(ExceptionDisposition::Propagate)
178 }
179
180 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 pub fn process(mut self, processor: BoxProcessor) -> Self {
196 self.steps.push(processor);
197 self
198 }
199
200 pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
202 self.on_when = Some(predicate);
203 self
204 }
205
206 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 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}