use crate::RouteBuilder;
use camel_api::error_handler::ExceptionDisposition;
use camel_api::{BoxProcessor, FilterPredicate};
use camel_core::route::BuilderStep;
use camel_processor::{CatchClause, CatchMatcher, DoTryService};
pub struct DoTryBuilder {
parent: RouteBuilder,
try_steps: Vec<BoxProcessor>,
catch_clauses: Vec<CatchClause>,
finally_steps: Vec<BoxProcessor>,
finally_on_when: Option<FilterPredicate>,
finally_set: bool,
}
pub struct DoCatchBuilder {
parent: DoTryBuilder,
matcher: CatchMatcher,
on_when: Option<FilterPredicate>,
steps: Vec<BoxProcessor>,
disposition: ExceptionDisposition,
}
pub struct DoFinallyBuilder {
parent: DoTryBuilder,
steps: Vec<BoxProcessor>,
on_when: Option<FilterPredicate>,
}
impl RouteBuilder {
pub fn do_try(self) -> DoTryBuilder {
DoTryBuilder {
parent: self,
try_steps: Vec::new(),
catch_clauses: Vec::new(),
finally_steps: Vec::new(),
finally_on_when: None,
finally_set: false,
}
}
}
impl DoTryBuilder {
pub fn process(mut self, processor: BoxProcessor) -> Self {
self.try_steps.push(processor);
self
}
pub fn do_catch_exception(self, variants: &[&str]) -> DoCatchBuilder {
DoCatchBuilder {
parent: self,
matcher: CatchMatcher::ByVariant(variants.iter().map(|s| (*s).to_string()).collect()),
on_when: None,
steps: Vec::new(),
disposition: ExceptionDisposition::Handled,
}
}
pub fn do_catch_when(self, predicate: FilterPredicate) -> DoCatchBuilder {
DoCatchBuilder {
parent: self,
matcher: CatchMatcher::Predicate(predicate),
on_when: None,
steps: Vec::new(),
disposition: ExceptionDisposition::Handled,
}
}
pub fn do_catch_all(self) -> DoCatchBuilder {
self.do_catch_exception(&["*"])
}
pub fn do_finally(self) -> DoFinallyBuilder {
if self.finally_set {
panic!("do_finally can only be called once per do_try scope");
}
DoFinallyBuilder {
parent: self,
steps: Vec::new(),
on_when: None,
}
}
pub fn end_do_try(self) -> RouteBuilder {
let do_try = DoTryService {
try_steps: self.try_steps,
catch_clauses: self.catch_clauses,
finally_steps: self.finally_steps,
finally_on_when: self.finally_on_when,
};
let mut parent = self.parent;
parent
.steps
.push(BuilderStep::Processor(BoxProcessor::new(do_try)));
parent
}
}
impl DoCatchBuilder {
pub fn process(mut self, processor: BoxProcessor) -> Self {
self.steps.push(processor);
self
}
pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
self.on_when = Some(predicate);
self
}
pub fn disposition(mut self, value: ExceptionDisposition) -> Self {
if matches!(value, ExceptionDisposition::Continued) {
panic!(
"ExceptionDisposition::Continued is not supported in doTry MVP (spec §3); \
use Handled or Propagate"
);
}
self.disposition = value;
self
}
pub fn handled(self) -> Self {
self.disposition(ExceptionDisposition::Handled)
}
pub fn propagate(self) -> Self {
self.disposition(ExceptionDisposition::Propagate)
}
pub fn end_do_catch(self) -> DoTryBuilder {
let mut parent = self.parent;
parent.catch_clauses.push(CatchClause {
matcher: self.matcher,
on_when: self.on_when,
steps: self.steps,
disposition: self.disposition,
});
parent
}
}
impl DoFinallyBuilder {
pub fn process(mut self, processor: BoxProcessor) -> Self {
self.steps.push(processor);
self
}
pub fn on_when(mut self, predicate: FilterPredicate) -> Self {
self.on_when = Some(predicate);
self
}
pub fn end_do_finally(self) -> DoTryBuilder {
let mut parent = self.parent;
parent.finally_set = true;
parent.finally_on_when = self.on_when;
parent.finally_steps = self.steps;
parent
}
}
#[cfg(test)]
mod tests {
use crate::RouteBuilder;
use camel_api::error_handler::ExceptionDisposition;
use camel_api::{BoxProcessor, BoxProcessorExt};
use camel_core::route::BuilderStep;
fn passthrough() -> BoxProcessor {
BoxProcessor::from_fn(move |ex| Box::pin(async move { Ok(ex) }))
}
#[test]
fn do_try_builder_assembles_correct_shape() {
let route = RouteBuilder::from("direct:start")
.route_id("do-try-shape")
.do_try()
.process(passthrough())
.do_catch_exception(&["ProcessorError"])
.disposition(ExceptionDisposition::Handled)
.process(passthrough())
.end_do_catch()
.do_finally()
.process(passthrough())
.end_do_finally()
.end_do_try();
let config = route.build().unwrap();
assert_eq!(
config.steps().len(),
1,
"expected exactly one step (the DoTryService)"
);
assert!(
matches!(config.steps().first(), Some(BuilderStep::Processor(_))),
"the single step must be a Processor variant (the DoTryService)"
);
}
#[test]
fn do_try_builder_disposition_sugar_methods() {
let _ = RouteBuilder::from("direct:a")
.route_id("do-try-sugar-a")
.do_try()
.process(passthrough())
.do_catch_exception(&["Io"])
.handled()
.end_do_catch()
.end_do_try()
.build()
.unwrap();
let _ = RouteBuilder::from("direct:b")
.route_id("do-try-sugar-b")
.do_try()
.process(passthrough())
.do_catch_exception(&["Io"])
.propagate()
.end_do_catch()
.end_do_try()
.build()
.unwrap();
}
#[test]
#[should_panic(expected = "do_finally can only be called once per do_try scope")]
fn do_finally_called_twice_panics() {
let _ = RouteBuilder::from("direct:start")
.route_id("do-try-double-finally")
.do_try()
.process(passthrough())
.do_finally()
.process(passthrough())
.end_do_finally()
.do_finally();
}
#[test]
#[should_panic(expected = "ExceptionDisposition::Continued is not supported in doTry MVP")]
fn disposition_continued_panics() {
let _ = RouteBuilder::from("direct:start")
.route_id("do-try-continued")
.do_try()
.process(passthrough())
.do_catch_exception(&["ProcessorError"])
.disposition(ExceptionDisposition::Continued)
.end_do_catch()
.end_do_try();
}
}