1use std::sync::Arc;
9
10use camel_api::{
11 BodyType, BoxProcessor, CamelError, FunctionInvoker, ProducerContext, StepLifecycle,
12};
13use camel_component_api::{ComponentContext, RuntimeObservability};
14use camel_endpoint::parse_uri;
15
16use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
17use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
18use crate::lifecycle::application::route_definition::BuilderStep;
19use crate::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
20use camel_bean::BeanRegistry;
21
22mod control_flow;
23mod core;
24mod endpoints;
25mod error_handling;
26mod routing;
27mod splitting;
28mod transforms;
29
30#[derive(Debug, Clone)]
44pub enum CompiledStep {
45 Process {
46 processor: BoxProcessor,
47 body_contract: Option<BodyType>,
48 lifecycle: Option<Arc<dyn StepLifecycle>>,
51 },
52 Stop,
55 Segment {
59 segment: camel_api::OutcomeSegment,
60 body_contract: Option<BodyType>,
61 lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
66 },
67}
68
69pub(crate) enum CompileOutcome {
76 Matched(CompiledStep),
77 NotHandled(BuilderStep),
78}
79
80pub(crate) trait StepCompiler: Send + Sync {
87 fn compile(
88 &self,
89 step: BuilderStep,
90 step_index: usize,
91 ctx: &CompilationContext,
92 registry: &StepCompilerRegistry,
93 ) -> Result<CompileOutcome, CamelError>;
94}
95
96pub(crate) struct CompilationContext<'a> {
98 pub producer_ctx: &'a ProducerContext,
99 pub rt: Arc<dyn RuntimeObservability>,
100 pub languages: &'a SharedLanguageRegistry,
101 pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
102 pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
103 pub component_ctx: Arc<dyn ComponentContext>,
104 pub route_id: Option<&'a str>,
105 pub staging_mode: &'a FunctionStagingMode,
106 pub idempotent_repositories: &'a IdempotentRegistry,
109 pub claim_check_repositories: &'a ClaimCheckRegistry,
112 pub cache_repositories: &'a CacheRegistry,
116}
117
118impl<'a> CompilationContext<'a> {
119 pub fn compile_children(
122 &self,
123 steps: Vec<BuilderStep>,
124 registry: &StepCompilerRegistry,
125 ) -> Result<Vec<CompiledStep>, CamelError> {
126 registry.compile_steps(steps, self)
127 }
128
129 #[allow(clippy::type_complexity)]
139 pub fn compile_children_segments(
140 &self,
141 steps: Vec<BuilderStep>,
142 registry: &StepCompilerRegistry,
143 ) -> Result<
144 (
145 Vec<Box<dyn camel_api::OutcomePipeline>>,
146 Vec<Arc<dyn camel_api::StepLifecycle>>,
147 ),
148 CamelError,
149 > {
150 let pairs = self.compile_children(steps, registry)?;
151 let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
152 let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
153 .into_iter()
154 .map(|c| match c {
155 CompiledStep::Process {
156 processor,
157 body_contract,
158 lifecycle,
159 } => {
160 if let Some(lc) = lifecycle {
161 lifecycle_handles.push(lc);
162 }
163 let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
164 crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
165 processor,
166 ),
167 );
168 match body_contract {
169 Some(contract) => Box::new(
170 crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
171 inner, contract,
172 ),
173 ),
174 None => inner,
175 }
176 }
177 CompiledStep::Stop => {
178 Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
179 as Box<dyn camel_api::OutcomePipeline>
180 }
181 CompiledStep::Segment {
182 segment,
183 body_contract: _,
184 lifecycle,
185 } => {
186 if let Some(lcs) = lifecycle {
187 lifecycle_handles.extend(lcs);
188 }
189 Box::new(segment)
190 }
191 })
192 .collect();
193 Ok((segments, lifecycle_handles))
194 }
195}
196
197pub(crate) struct StepCompilerRegistry {
200 compilers: Vec<Box<dyn StepCompiler>>,
201}
202
203impl StepCompilerRegistry {
204 pub fn new() -> Self {
205 Self {
206 compilers: Vec::new(),
207 }
208 }
209
210 pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
211 self.compilers.push(compiler);
212 }
213
214 pub fn compile_step(
218 &self,
219 step: BuilderStep,
220 step_index: usize,
221 ctx: &CompilationContext,
222 ) -> Result<Option<CompiledStep>, CamelError> {
223 let mut step = step;
224 for compiler in &self.compilers {
225 match compiler.compile(step, step_index, ctx, self)? {
226 CompileOutcome::Matched(s) => return Ok(Some(s)),
227 CompileOutcome::NotHandled(s) => step = s,
228 }
229 }
230 Ok(None)
231 }
232
233 pub fn compile_steps(
235 &self,
236 steps: Vec<BuilderStep>,
237 ctx: &CompilationContext,
238 ) -> Result<Vec<CompiledStep>, CamelError> {
239 let mut out = Vec::with_capacity(steps.len());
240 for (i, step) in steps.into_iter().enumerate() {
241 match self.compile_step(step, i, ctx)? {
242 Some(c) => out.push(c),
243 None => {
244 return Err(CamelError::RouteError(
245 "no compiler registered for step variant".into(),
246 ));
247 }
248 }
249 }
250 Ok(out)
251 }
252}
253
254pub(crate) fn resolve_producer(
261 ctx: &CompilationContext,
262 uri: &str,
263) -> Result<BoxProcessor, CamelError> {
264 Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
265}
266
267pub(crate) fn resolve_producer_with_lifecycle(
271 ctx: &CompilationContext,
272 uri: &str,
273) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
274 let parsed = parse_uri(uri)?;
275 let component = ctx
276 .component_ctx
277 .resolve_component(&parsed.scheme)
278 .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
279 let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
280 let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
281 let lifecycle = endpoint.lifecycle();
282 Ok((producer, lifecycle))
283}
284
285pub(super) fn pack_lifecycles(
288 lifecycles: Vec<Arc<dyn StepLifecycle>>,
289) -> Option<Vec<Arc<dyn StepLifecycle>>> {
290 if lifecycles.is_empty() {
291 None
292 } else {
293 Some(lifecycles)
294 }
295}
296
297pub(crate) fn build_registry() -> StepCompilerRegistry {
299 let mut reg = StepCompilerRegistry::new();
300 reg.register(Box::new(core::CoreCompiler));
301 reg.register(Box::new(endpoints::EndpointsCompiler));
302 reg.register(Box::new(transforms::TransformsCompiler));
303 reg.register(Box::new(routing::RoutingCompiler));
304 reg.register(Box::new(control_flow::ControlFlowCompiler));
305 reg.register(Box::new(splitting::SplittingCompiler));
306 reg.register(Box::new(error_handling::ErrorHandlingCompiler));
307 reg
308}
309
310#[cfg(test)]
311mod segment_tests {
312 use super::*;
313 use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
314 use std::future::Future;
315 use std::pin::Pin;
316
317 #[derive(Clone)]
318 struct EchoSegment;
319
320 impl OutcomePipeline for EchoSegment {
321 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
322 Box::new(EchoSegment)
323 }
324 fn run<'a>(
325 &'a mut self,
326 exchange: Exchange,
327 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
328 Box::pin(async move { PipelineOutcome::Completed(exchange) })
329 }
330 }
331
332 #[test]
333 fn compiled_step_segment_clone_compiles() {
334 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
335 let step = CompiledStep::Segment {
336 segment: seg,
337 body_contract: None,
338 lifecycle: None,
339 };
340 let _cloned = step.clone();
341 if let CompiledStep::Segment { segment: _, .. } = _cloned {
342 } else {
344 panic!("clone should preserve variant");
345 }
346 }
347
348 #[test]
349 fn compiled_step_segment_debug_renders() {
350 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
351 let step = CompiledStep::Segment {
352 segment: seg,
353 body_contract: None,
354 lifecycle: None,
355 };
356 let s = format!("{:?}", step);
357 assert!(
358 s.contains("Segment"),
359 "debug should mention Segment variant: {s}"
360 );
361 }
362
363 #[test]
364 fn outcome_segment_satisfies_clone_send_static() {
365 fn assert_traits<T: Clone + Send + 'static>() {}
366 assert_traits::<camel_api::OutcomeSegment>();
367 }
368
369 #[tokio::test]
370 #[allow(clippy::arc_with_non_send_sync)]
371 async fn outcome_segment_survives_arcswap_swap() {
372 use arc_swap::ArcSwap;
373 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
374 use std::sync::Arc;
375
376 #[derive(Clone)]
377 struct EchoSegment;
378 impl OutcomePipeline for EchoSegment {
379 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
380 Box::new(EchoSegment)
381 }
382 fn run<'a>(
383 &'a mut self,
384 ex: Exchange,
385 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
386 {
387 Box::pin(async move { PipelineOutcome::Completed(ex) })
388 }
389 }
390
391 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
392 let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
393 slot.store(Arc::new(Some(seg.clone())));
394 slot.store(Arc::new(Some(seg)));
395
396 let mut borrowed = slot.load().as_ref().clone().unwrap();
397 let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
398 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
399 }
400
401 #[derive(Debug)]
403 struct TestLifecycle;
404
405 #[async_trait::async_trait]
406 impl camel_api::StepLifecycle for TestLifecycle {
407 fn name(&self) -> &'static str {
408 "test-lifecycle"
409 }
410 async fn shutdown(
411 &self,
412 _reason: camel_api::StepShutdownReason,
413 ) -> Result<(), camel_api::CamelError> {
414 Ok(())
415 }
416 }
417
418 struct LifecycleInjectorCompiler {
421 handle: Arc<dyn camel_api::StepLifecycle>,
422 }
423
424 impl StepCompiler for LifecycleInjectorCompiler {
425 fn compile(
426 &self,
427 step: BuilderStep,
428 _step_index: usize,
429 _ctx: &CompilationContext,
430 _registry: &StepCompilerRegistry,
431 ) -> Result<CompileOutcome, CamelError> {
432 match step {
433 BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
434 processor: op.0,
435 body_contract: None,
436 lifecycle: Some(self.handle.clone()),
437 })),
438 other => Ok(CompileOutcome::NotHandled(other)),
439 }
440 }
441 }
442
443 #[tokio::test]
444 async fn compile_children_segments_bubbles_child_lifecycle() {
445 use std::collections::HashMap;
446 use std::sync::Mutex;
447
448 use camel_api::{
449 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
450 };
451 use camel_bean::BeanRegistry;
452 use camel_component_api::{
453 ComponentContext, NoOpComponentContext, RuntimeObservability,
454 test_support::NoopRuntimeObservability,
455 };
456
457 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
458
459 let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
460
461 let mut reg = StepCompilerRegistry::new();
464 reg.register(Box::new(LifecycleInjectorCompiler {
465 handle: handle.clone(),
466 }));
467 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
468
469 let pc = ProducerContext::default();
470 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
471 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
472 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
473 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
474 let staging = FunctionStagingMode::DirectAdd;
475 let idempotent_repositories = crate::IdempotentRegistry::new();
476 let claim_check_repositories = crate::ClaimCheckRegistry::new();
477 let cache_repositories = crate::CacheRegistry::new();
478
479 let ctx = CompilationContext {
480 producer_ctx: &pc,
481 rt,
482 languages: &languages,
483 beans: &beans,
484 function_invoker: None,
485 component_ctx,
486 route_id: None,
487 staging_mode: &staging,
488 idempotent_repositories: &idempotent_repositories,
489 claim_check_repositories: &claim_check_repositories,
490 cache_repositories: &cache_repositories,
491 };
492
493 let filter_step = BuilderStep::Filter {
495 predicate: FilterPredicate::new(|_| true),
496 steps: vec![BuilderStep::Processor(OpaqueProcessor(
497 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
498 ))],
499 };
500
501 let result = reg.compile_step(filter_step, 0, &ctx);
502 let compiled = result
503 .expect("compilation should succeed")
504 .expect("should match");
505
506 match compiled {
507 CompiledStep::Segment {
508 lifecycle,
509 body_contract,
510 ..
511 } => {
512 assert_eq!(body_contract, None, "body_contract should be None");
513 let handles = lifecycle.expect("Segment should have lifecycle handles");
514 assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
515 assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
516 }
517 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
518 }
519 }
520
521 #[derive(Debug)]
523 struct NamedLifecycle(&'static str);
524
525 #[async_trait::async_trait]
526 impl camel_api::StepLifecycle for NamedLifecycle {
527 fn name(&self) -> &'static str {
528 self.0
529 }
530 async fn shutdown(
531 &self,
532 _reason: camel_api::StepShutdownReason,
533 ) -> Result<(), camel_api::CamelError> {
534 Ok(())
535 }
536 }
537
538 #[tokio::test]
540 async fn compile_children_segments_multiple_stateful_children() {
541 use std::collections::HashMap;
542 use std::sync::Mutex;
543
544 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
545 use camel_api::{
546 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
547 };
548 use camel_bean::BeanRegistry;
549 use camel_component_api::{
550 ComponentContext, NoOpComponentContext, RuntimeObservability,
551 test_support::NoopRuntimeObservability,
552 };
553
554 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
555
556 let mut reg = StepCompilerRegistry::new();
557 reg.register(Box::new(LifecycleInjectorCompiler {
558 handle: handle.clone(),
559 }));
560 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
561
562 let pc = ProducerContext::default();
563 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
564 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
565 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
566 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
567 let staging = FunctionStagingMode::DirectAdd;
568 let idempotent_repositories = crate::IdempotentRegistry::new();
569 let claim_check_repositories = crate::ClaimCheckRegistry::new();
570 let cache_repositories = crate::CacheRegistry::new();
571
572 let ctx = CompilationContext {
573 producer_ctx: &pc,
574 rt,
575 languages: &languages,
576 beans: &beans,
577 function_invoker: None,
578 component_ctx,
579 route_id: None,
580 staging_mode: &staging,
581 idempotent_repositories: &idempotent_repositories,
582 claim_check_repositories: &claim_check_repositories,
583 cache_repositories: &cache_repositories,
584 };
585
586 let filter_step = BuilderStep::Filter {
588 predicate: FilterPredicate::new(|_| true),
589 steps: vec![
590 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
591 Box::pin(async move { Ok(ex) })
592 }))),
593 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
594 Box::pin(async move { Ok(ex) })
595 }))),
596 ],
597 };
598
599 let result = reg.compile_step(filter_step, 0, &ctx);
600 let compiled = result
601 .expect("compilation should succeed")
602 .expect("should match");
603
604 match compiled {
605 CompiledStep::Segment { lifecycle, .. } => {
606 let handles = lifecycle.expect("Segment should have lifecycle handles");
607 assert_eq!(
608 handles.len(),
609 2,
610 "expected 2 lifecycle handles for 2 children"
611 );
612 for h in &handles {
613 assert_eq!(h.name(), "multi", "all handles should be 'multi'");
614 }
615 }
616 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
617 }
618 }
619
620 #[tokio::test]
622 async fn compile_children_segments_multi_branch_accumulation() {
623 use std::collections::HashMap;
624 use std::sync::Mutex;
625
626 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
627 use crate::lifecycle::application::route_definition::WhenStep;
628 use camel_api::{
629 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
630 };
631 use camel_bean::BeanRegistry;
632 use camel_component_api::{
633 ComponentContext, NoOpComponentContext, RuntimeObservability,
634 test_support::NoopRuntimeObservability,
635 };
636
637 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
638
639 let mut reg = StepCompilerRegistry::new();
640 reg.register(Box::new(LifecycleInjectorCompiler {
641 handle: handle.clone(),
642 }));
643 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
644
645 let pc = ProducerContext::default();
646 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
647 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
648 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
649 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
650 let staging = FunctionStagingMode::DirectAdd;
651 let idempotent_repositories = crate::IdempotentRegistry::new();
652 let claim_check_repositories = crate::ClaimCheckRegistry::new();
653 let cache_repositories = crate::CacheRegistry::new();
654
655 let ctx = CompilationContext {
656 producer_ctx: &pc,
657 rt,
658 languages: &languages,
659 beans: &beans,
660 function_invoker: None,
661 component_ctx,
662 route_id: None,
663 staging_mode: &staging,
664 idempotent_repositories: &idempotent_repositories,
665 claim_check_repositories: &claim_check_repositories,
666 cache_repositories: &cache_repositories,
667 };
668
669 let choice_step = BuilderStep::Choice {
671 whens: vec![
672 WhenStep {
673 predicate: FilterPredicate::new(|_| true),
674 steps: vec![BuilderStep::Processor(OpaqueProcessor(
675 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
676 ))],
677 },
678 WhenStep {
679 predicate: FilterPredicate::new(|_| false),
680 steps: vec![BuilderStep::Processor(OpaqueProcessor(
681 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
682 ))],
683 },
684 ],
685 otherwise: None,
686 };
687
688 let result = reg.compile_step(choice_step, 0, &ctx);
689 let compiled = result
690 .expect("compilation should succeed")
691 .expect("should match");
692
693 match compiled {
694 CompiledStep::Segment { lifecycle, .. } => {
695 let handles = lifecycle.expect("Segment should have lifecycle handles");
696 assert_eq!(
697 handles.len(),
698 2,
699 "expected 2 lifecycle handles from 2 branches"
700 );
701 for h in &handles {
702 assert_eq!(h.name(), "branch", "all handles should be 'branch'");
703 }
704 }
705 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
706 }
707 }
708
709 #[tokio::test]
712 async fn compile_children_segments_nested_segment_flattening() {
713 use std::collections::HashMap;
714 use std::sync::Mutex;
715
716 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
717 use camel_api::{
718 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
719 };
720 use camel_bean::BeanRegistry;
721 use camel_component_api::{
722 ComponentContext, NoOpComponentContext, RuntimeObservability,
723 test_support::NoopRuntimeObservability,
724 };
725
726 let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
727
728 let mut reg = StepCompilerRegistry::new();
729 reg.register(Box::new(LifecycleInjectorCompiler {
730 handle: inner_handle.clone(),
731 }));
732 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
733
734 let pc = ProducerContext::default();
735 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
736 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
737 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
738 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
739 let staging = FunctionStagingMode::DirectAdd;
740 let idempotent_repositories = crate::IdempotentRegistry::new();
741 let claim_check_repositories = crate::ClaimCheckRegistry::new();
742 let cache_repositories = crate::CacheRegistry::new();
743
744 let ctx = CompilationContext {
745 producer_ctx: &pc,
746 rt,
747 languages: &languages,
748 beans: &beans,
749 function_invoker: None,
750 component_ctx,
751 route_id: None,
752 staging_mode: &staging,
753 idempotent_repositories: &idempotent_repositories,
754 claim_check_repositories: &claim_check_repositories,
755 cache_repositories: &cache_repositories,
756 };
757
758 let inner_filter = BuilderStep::Filter {
762 predicate: FilterPredicate::new(|_| true),
763 steps: vec![BuilderStep::Processor(OpaqueProcessor(
764 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
765 ))],
766 };
767
768 let outer_filter = BuilderStep::Filter {
769 predicate: FilterPredicate::new(|_| true),
770 steps: vec![inner_filter],
771 };
772
773 let result = reg.compile_step(outer_filter, 0, &ctx);
774 let compiled = result
775 .expect("compilation should succeed")
776 .expect("should match");
777
778 match compiled {
779 CompiledStep::Segment { lifecycle, .. } => {
780 let handles = lifecycle.expect("outer Segment should have lifecycle handles");
781 assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
782 assert_eq!(
783 handles[0].name(),
784 "deep",
785 "handle should be from innermost child"
786 );
787 }
788 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
789 }
790 }
791}
792
793#[cfg(test)]
794mod dispatch_tests {
795 use super::*;
796 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
797 use camel_api::{BoxProcessor, BoxProcessorExt};
798 use camel_bean::BeanRegistry;
799 use camel_component_api::{
800 ComponentContext, NoOpComponentContext, RuntimeObservability,
801 test_support::NoopRuntimeObservability,
802 };
803 use std::collections::HashMap;
804 use std::sync::Mutex;
805
806 struct ToStopCompiler;
808
809 impl StepCompiler for ToStopCompiler {
810 fn compile(
811 &self,
812 step: BuilderStep,
813 _step_index: usize,
814 _ctx: &CompilationContext,
815 _registry: &StepCompilerRegistry,
816 ) -> Result<CompileOutcome, CamelError> {
817 match step {
818 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
819 other => Ok(CompileOutcome::NotHandled(other)),
820 }
821 }
822 }
823
824 struct ToProcessCompiler;
826
827 impl StepCompiler for ToProcessCompiler {
828 fn compile(
829 &self,
830 step: BuilderStep,
831 _step_index: usize,
832 _ctx: &CompilationContext,
833 _registry: &StepCompilerRegistry,
834 ) -> Result<CompileOutcome, CamelError> {
835 match step {
836 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
837 processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
838 body_contract: None,
839 lifecycle: None,
840 })),
841 other => Ok(CompileOutcome::NotHandled(other)),
842 }
843 }
844 }
845
846 struct PassThroughCompiler;
849
850 impl StepCompiler for PassThroughCompiler {
851 fn compile(
852 &self,
853 step: BuilderStep,
854 _step_index: usize,
855 _ctx: &CompilationContext,
856 _registry: &StepCompilerRegistry,
857 ) -> Result<CompileOutcome, CamelError> {
858 Ok(CompileOutcome::NotHandled(step))
859 }
860 }
861
862 #[allow(clippy::too_many_arguments)]
864 fn ctx<'a>(
865 pc: &'a ProducerContext,
866 rt: Arc<dyn RuntimeObservability>,
867 languages: &'a SharedLanguageRegistry,
868 beans: &'a Arc<Mutex<BeanRegistry>>,
869 component_ctx: Arc<dyn ComponentContext>,
870 staging: &'a FunctionStagingMode,
871 idempotent_repositories: &'a crate::IdempotentRegistry,
872 claim_check_repositories: &'a crate::ClaimCheckRegistry,
873 cache_repositories: &'a crate::CacheRegistry,
874 ) -> CompilationContext<'a> {
875 CompilationContext {
876 producer_ctx: pc,
877 rt,
878 languages,
879 beans,
880 function_invoker: None,
881 component_ctx,
882 route_id: None,
883 staging_mode: staging,
884 idempotent_repositories,
885 claim_check_repositories,
886 cache_repositories,
887 }
888 }
889
890 #[test]
893 fn compile_step_preserves_registry_order() {
894 let pc = ProducerContext::default();
895 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
896 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
897 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
898 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
899 let staging = FunctionStagingMode::DirectAdd;
900 let idempotent_repositories = crate::IdempotentRegistry::new();
901 let claim_check_repositories = crate::ClaimCheckRegistry::new();
902 let cache_repositories = crate::CacheRegistry::new();
903
904 let context = ctx(
905 &pc,
906 rt,
907 &languages,
908 &beans,
909 component_ctx,
910 &staging,
911 &idempotent_repositories,
912 &claim_check_repositories,
913 &cache_repositories,
914 );
915
916 let mut reg = StepCompilerRegistry::new();
919 reg.register(Box::new(ToStopCompiler));
920 reg.register(Box::new(ToProcessCompiler));
921
922 let result = reg
923 .compile_step(BuilderStep::To("test".into()), 0, &context)
924 .expect("compilation should succeed")
925 .expect("should match");
926
927 assert!(
928 matches!(result, CompiledStep::Stop),
929 "expected ToStopCompiler (first registered) to win, got {result:?}"
930 );
931 }
932
933 #[test]
936 fn compile_step_unhandled_returns_ok_none() {
937 let pc = ProducerContext::default();
938 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
939 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
940 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
941 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
942 let staging = FunctionStagingMode::DirectAdd;
943 let idempotent_repositories = crate::IdempotentRegistry::new();
944 let claim_check_repositories = crate::ClaimCheckRegistry::new();
945 let cache_repositories = crate::CacheRegistry::new();
946
947 let context = ctx(
948 &pc,
949 rt,
950 &languages,
951 &beans,
952 component_ctx,
953 &staging,
954 &idempotent_repositories,
955 &claim_check_repositories,
956 &cache_repositories,
957 );
958
959 let mut reg = StepCompilerRegistry::new();
961 reg.register(Box::new(ToStopCompiler));
962
963 let result = reg.compile_step(
965 BuilderStep::Log {
966 level: camel_processor::LogLevel::Info,
967 message: "unhandled".into(),
968 },
969 0,
970 &context,
971 );
972
973 assert!(
974 matches!(result, Ok(None)),
975 "expected Ok(None), got {result:?}"
976 );
977 }
978
979 #[test]
983 fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
984 let pc = ProducerContext::default();
985 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
986 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
987 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
988 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
989 let staging = FunctionStagingMode::DirectAdd;
990 let idempotent_repositories = crate::IdempotentRegistry::new();
991 let claim_check_repositories = crate::ClaimCheckRegistry::new();
992 let cache_repositories = crate::CacheRegistry::new();
993
994 let context = ctx(
995 &pc,
996 rt,
997 &languages,
998 &beans,
999 component_ctx,
1000 &staging,
1001 &idempotent_repositories,
1002 &claim_check_repositories,
1003 &cache_repositories,
1004 );
1005
1006 let mut reg = StepCompilerRegistry::new();
1009 reg.register(Box::new(PassThroughCompiler));
1010 reg.register(Box::new(ToStopCompiler));
1011
1012 let result = reg
1013 .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1014 .expect("compilation should succeed")
1015 .expect("should match");
1016
1017 assert!(
1018 matches!(result, CompiledStep::Stop),
1019 "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1020 );
1021 }
1022}