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