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