1use 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
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(OpaqueProcessor(BoxProcessor::new(
127 do_try,
128 ))));
129 parent
130 }
131}
132
133impl DoCatchBuilder {
134 pub fn process(mut self, processor: BoxProcessor) -> Self {
136 self.steps.push(processor);
137 self
138 }
139
140 pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
142 self.on_when = Some(predicate);
143 self
144 }
145
146 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 pub fn handled(self) -> Self {
167 self.disposition(ExceptionDisposition::Handled)
168 }
169
170 pub fn propagate(self) -> Self {
179 self.disposition(ExceptionDisposition::Propagate)
180 }
181
182 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 pub fn process(mut self, processor: BoxProcessor) -> Self {
198 self.steps.push(processor);
199 self
200 }
201
202 pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
204 self.on_when = Some(predicate);
205 self
206 }
207
208 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 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}