1use std::sync::Arc;
9
10use camel_api::{
11 BodyType, BoxProcessor, CamelError, FunctionInvoker, ProducerContext, SpanKindHint,
12 StepLifecycle,
13};
14use camel_component_api::{ComponentContext, RuntimeObservability};
15use camel_endpoint::parse_uri;
16
17use crate::intercept::InterceptRules;
18use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
19use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
20use crate::lifecycle::application::route_definition::BuilderStep;
21use crate::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
22use camel_bean::BeanRegistry;
23
24mod control_flow;
25mod core;
26mod endpoints;
27mod error_handling;
28mod routing;
29mod splitting;
30mod transforms;
31
32#[derive(Debug, Clone)]
46pub enum CompiledStep {
47 Process {
48 processor: BoxProcessor,
49 body_contract: Option<BodyType>,
50 lifecycle: Option<Arc<dyn StepLifecycle>>,
53 label: Option<Arc<str>>,
59 kind_hint: SpanKindHint,
65 to_uri: Option<Arc<str>>,
73 },
74 Stop,
77 Segment {
81 segment: camel_api::OutcomeSegment,
82 body_contract: Option<BodyType>,
83 lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
88 label: Option<Arc<str>>,
91 },
92}
93
94impl CompiledStep {
95 pub(crate) fn set_label(&mut self, label: Option<String>) {
98 let label = label.map(Arc::from);
99 match self {
100 CompiledStep::Process { label: slot, .. }
101 | CompiledStep::Segment { label: slot, .. } => {
102 *slot = label;
103 }
104 CompiledStep::Stop => {}
105 }
106 }
107
108 pub(crate) fn set_kind_hint(&mut self, hint: SpanKindHint) {
112 match self {
113 CompiledStep::Process {
114 kind_hint: slot, ..
115 } => *slot = hint,
116 CompiledStep::Stop | CompiledStep::Segment { .. } => {}
117 }
118 }
119
120 pub(crate) fn set_to_uri(&mut self, to_uri: Option<Arc<str>>) {
124 match self {
125 CompiledStep::Process { to_uri: slot, .. } => *slot = to_uri,
126 CompiledStep::Stop | CompiledStep::Segment { .. } => {}
127 }
128 }
129}
130
131pub(crate) enum CompileOutcome {
138 Matched(CompiledStep),
139 NotHandled(BuilderStep),
140}
141
142pub(crate) trait StepCompiler: Send + Sync {
149 fn compile(
150 &self,
151 step: BuilderStep,
152 step_index: usize,
153 ctx: &CompilationContext,
154 registry: &StepCompilerRegistry,
155 ) -> Result<CompileOutcome, CamelError>;
156}
157
158pub(crate) struct CompilationContext<'a> {
160 pub producer_ctx: &'a ProducerContext,
161 pub rt: Arc<dyn RuntimeObservability>,
162 pub languages: &'a SharedLanguageRegistry,
163 pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
164 pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
165 pub component_ctx: Arc<dyn ComponentContext>,
166 pub route_id: Option<&'a str>,
167 pub staging_mode: &'a FunctionStagingMode,
168 pub idempotent_repositories: &'a IdempotentRegistry,
171 pub claim_check_repositories: &'a ClaimCheckRegistry,
174 pub cache_repositories: &'a CacheRegistry,
178 pub intercept: InterceptRules,
181}
182
183impl<'a> CompilationContext<'a> {
184 pub fn compile_children(
187 &self,
188 steps: Vec<BuilderStep>,
189 registry: &StepCompilerRegistry,
190 ) -> Result<Vec<CompiledStep>, CamelError> {
191 registry.compile_steps(steps, self)
192 }
193
194 #[allow(clippy::type_complexity)]
204 pub fn compile_children_segments(
205 &self,
206 steps: Vec<BuilderStep>,
207 registry: &StepCompilerRegistry,
208 ) -> Result<
209 (
210 Vec<Box<dyn camel_api::OutcomePipeline>>,
211 Vec<Arc<dyn camel_api::StepLifecycle>>,
212 ),
213 CamelError,
214 > {
215 let pairs = self.compile_children(steps, registry)?;
216 let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
217 let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
218 .into_iter()
219 .map(|c| match c {
220 CompiledStep::Process {
221 processor,
222 body_contract,
223 lifecycle,
224 label: _,
225 kind_hint: _,
226 to_uri: _,
227 } => {
228 if let Some(lc) = lifecycle {
229 lifecycle_handles.push(lc);
230 }
231 let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
232 crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
233 processor,
234 ),
235 );
236 match body_contract {
237 Some(contract) => Box::new(
238 crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
239 inner, contract,
240 ),
241 ),
242 None => inner,
243 }
244 }
245 CompiledStep::Stop => {
246 Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
247 as Box<dyn camel_api::OutcomePipeline>
248 }
249 CompiledStep::Segment {
250 segment,
251 body_contract: _,
252 lifecycle,
253 label: _,
254 } => {
255 if let Some(lcs) = lifecycle {
256 lifecycle_handles.extend(lcs);
257 }
258 Box::new(segment)
259 }
260 })
261 .collect();
262 Ok((segments, lifecycle_handles))
263 }
264}
265
266pub(crate) struct StepCompilerRegistry {
269 compilers: Vec<Box<dyn StepCompiler>>,
270}
271
272impl StepCompilerRegistry {
273 pub fn new() -> Self {
274 Self {
275 compilers: Vec::new(),
276 }
277 }
278
279 pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
280 self.compilers.push(compiler);
281 }
282
283 pub fn compile_step(
287 &self,
288 step: BuilderStep,
289 step_index: usize,
290 ctx: &CompilationContext,
291 ) -> Result<Option<CompiledStep>, CamelError> {
292 let label = step.span_label();
297 let kind_hint = step.span_kind_hint();
298 let to_uri = step.to_uri_metadata();
299 let mut step = step;
300 for compiler in &self.compilers {
301 match compiler.compile(step, step_index, ctx, self)? {
302 CompileOutcome::Matched(mut s) => {
303 s.set_label(label);
304 s.set_kind_hint(kind_hint);
305 s.set_to_uri(to_uri);
306 return Ok(Some(s));
307 }
308 CompileOutcome::NotHandled(s) => step = s,
309 }
310 }
311 Ok(None)
312 }
313
314 pub fn compile_steps(
316 &self,
317 steps: Vec<BuilderStep>,
318 ctx: &CompilationContext,
319 ) -> Result<Vec<CompiledStep>, CamelError> {
320 let mut out = Vec::with_capacity(steps.len());
321 for (i, step) in steps.into_iter().enumerate() {
322 match self.compile_step(step, i, ctx)? {
323 Some(c) => out.push(c),
324 None => {
325 return Err(CamelError::RouteError(
326 "no compiler registered for step variant".into(),
327 ));
328 }
329 }
330 }
331 Ok(out)
332 }
333}
334
335pub(super) struct ResolvedSend {
338 pub producer: BoxProcessor,
339 pub body_contract: Option<BodyType>,
340 pub lifecycle: Option<Arc<dyn StepLifecycle>>,
341}
342
343pub(super) fn resolve_send(
348 ctx: &CompilationContext,
349 uri: &str,
350) -> Result<ResolvedSend, CamelError> {
351 let parsed = parse_uri(uri)?;
352 let component = ctx
353 .component_ctx
354 .resolve_component(&parsed.scheme)
355 .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
356 let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
357 let body_contract = endpoint.body_contract();
358 let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
359 let lifecycle: Option<Arc<dyn StepLifecycle>> = endpoint.lifecycle();
363 Ok(ResolvedSend {
364 producer,
365 body_contract,
366 lifecycle,
367 })
368}
369
370pub(crate) fn resolve_producer_with_lifecycle(
375 ctx: &CompilationContext,
376 uri: &str,
377) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
378 let resolved = resolve_send(ctx, uri)?;
379 Ok((resolved.producer, resolved.lifecycle))
380}
381
382pub(crate) fn resolve_producer(
388 ctx: &CompilationContext,
389 uri: &str,
390) -> Result<BoxProcessor, CamelError> {
391 Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
392}
393
394pub(super) fn pack_lifecycles(
397 lifecycles: Vec<Arc<dyn StepLifecycle>>,
398) -> Option<Vec<Arc<dyn StepLifecycle>>> {
399 if lifecycles.is_empty() {
400 None
401 } else {
402 Some(lifecycles)
403 }
404}
405
406pub(crate) fn build_registry() -> StepCompilerRegistry {
408 let mut reg = StepCompilerRegistry::new();
409 reg.register(Box::new(core::CoreCompiler));
410 reg.register(Box::new(endpoints::EndpointsCompiler));
411 reg.register(Box::new(transforms::TransformsCompiler));
412 reg.register(Box::new(routing::RoutingCompiler));
413 reg.register(Box::new(control_flow::ControlFlowCompiler));
414 reg.register(Box::new(splitting::SplittingCompiler));
415 reg.register(Box::new(error_handling::ErrorHandlingCompiler));
416 reg
417}
418
419#[cfg(test)]
420mod segment_tests {
421 use super::*;
422 use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
423 use std::future::Future;
424 use std::pin::Pin;
425
426 #[derive(Clone)]
427 struct EchoSegment;
428
429 impl OutcomePipeline for EchoSegment {
430 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
431 Box::new(EchoSegment)
432 }
433 fn run<'a>(
434 &'a mut self,
435 exchange: Exchange,
436 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
437 Box::pin(async move { PipelineOutcome::Completed(exchange) })
438 }
439 }
440
441 #[test]
442 fn compiled_step_segment_clone_compiles() {
443 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
444 let step = CompiledStep::Segment {
445 segment: seg,
446 body_contract: None,
447 lifecycle: None,
448 label: None,
449 };
450 let _cloned = step.clone();
451 if let CompiledStep::Segment { .. } = _cloned {
452 } else {
454 panic!("clone should preserve variant");
455 }
456 }
457
458 #[test]
459 fn compiled_step_segment_debug_renders() {
460 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
461 let step = CompiledStep::Segment {
462 segment: seg,
463 body_contract: None,
464 lifecycle: None,
465 label: None,
466 };
467 let s = format!("{:?}", step);
468 assert!(
469 s.contains("Segment"),
470 "debug should mention Segment variant: {s}"
471 );
472 }
473
474 #[test]
475 fn outcome_segment_satisfies_clone_send_static() {
476 fn assert_traits<T: Clone + Send + 'static>() {}
477 assert_traits::<camel_api::OutcomeSegment>();
478 }
479
480 #[tokio::test]
481 #[allow(clippy::arc_with_non_send_sync)]
482 async fn outcome_segment_survives_arcswap_swap() {
483 use arc_swap::ArcSwap;
484 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
485 use std::sync::Arc;
486
487 #[derive(Clone)]
488 struct EchoSegment;
489 impl OutcomePipeline for EchoSegment {
490 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
491 Box::new(EchoSegment)
492 }
493 fn run<'a>(
494 &'a mut self,
495 ex: Exchange,
496 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
497 {
498 Box::pin(async move { PipelineOutcome::Completed(ex) })
499 }
500 }
501
502 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
503 let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
504 slot.store(Arc::new(Some(seg.clone())));
505 slot.store(Arc::new(Some(seg)));
506
507 let mut borrowed = slot.load().as_ref().clone().unwrap();
508 let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
509 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
510 }
511
512 #[derive(Debug)]
514 struct TestLifecycle;
515
516 #[async_trait::async_trait]
517 impl camel_api::StepLifecycle for TestLifecycle {
518 fn name(&self) -> &'static str {
519 "test-lifecycle"
520 }
521 async fn shutdown(
522 &self,
523 _reason: camel_api::StepShutdownReason,
524 ) -> Result<(), camel_api::CamelError> {
525 Ok(())
526 }
527 }
528
529 struct LifecycleInjectorCompiler {
532 handle: Arc<dyn camel_api::StepLifecycle>,
533 }
534
535 impl StepCompiler for LifecycleInjectorCompiler {
536 fn compile(
537 &self,
538 step: BuilderStep,
539 _step_index: usize,
540 _ctx: &CompilationContext,
541 _registry: &StepCompilerRegistry,
542 ) -> Result<CompileOutcome, CamelError> {
543 match step {
544 BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
545 processor: op.0,
546 body_contract: None,
547 lifecycle: Some(self.handle.clone()),
548 label: None,
549 kind_hint: SpanKindHint::Internal,
550 to_uri: None,
551 })),
552 other => Ok(CompileOutcome::NotHandled(other)),
553 }
554 }
555 }
556
557 #[tokio::test]
558 async fn compile_children_segments_bubbles_child_lifecycle() {
559 use std::collections::HashMap;
560 use std::sync::Mutex;
561
562 use camel_api::{
563 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
564 };
565 use camel_bean::BeanRegistry;
566 use camel_component_api::{
567 ComponentContext, NoOpComponentContext, RuntimeObservability,
568 test_support::NoopRuntimeObservability,
569 };
570
571 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
572
573 let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
574
575 let mut reg = StepCompilerRegistry::new();
578 reg.register(Box::new(LifecycleInjectorCompiler {
579 handle: handle.clone(),
580 }));
581 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
582
583 let pc = ProducerContext::default();
584 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
585 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
586 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
587 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
588 let staging = FunctionStagingMode::DirectAdd;
589 let idempotent_repositories = crate::IdempotentRegistry::new();
590 let claim_check_repositories = crate::ClaimCheckRegistry::new();
591 let cache_repositories = crate::CacheRegistry::new();
592
593 let ctx = CompilationContext {
594 producer_ctx: &pc,
595 rt,
596 languages: &languages,
597 beans: &beans,
598 function_invoker: None,
599 component_ctx,
600 route_id: None,
601 staging_mode: &staging,
602 idempotent_repositories: &idempotent_repositories,
603 claim_check_repositories: &claim_check_repositories,
604 cache_repositories: &cache_repositories,
605 intercept: InterceptRules::default(),
606 };
607
608 let filter_step = BuilderStep::Filter {
610 predicate: FilterPredicate::new(|_| true),
611 steps: vec![BuilderStep::Processor(OpaqueProcessor(
612 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
613 ))],
614 };
615
616 let result = reg.compile_step(filter_step, 0, &ctx);
617 let compiled = result
618 .expect("compilation should succeed")
619 .expect("should match");
620
621 match compiled {
622 CompiledStep::Segment {
623 lifecycle,
624 body_contract,
625 ..
626 } => {
627 assert_eq!(body_contract, None, "body_contract should be None");
628 let handles = lifecycle.expect("Segment should have lifecycle handles");
629 assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
630 assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
631 }
632 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
633 }
634 }
635
636 #[derive(Debug)]
638 struct NamedLifecycle(&'static str);
639
640 #[async_trait::async_trait]
641 impl camel_api::StepLifecycle for NamedLifecycle {
642 fn name(&self) -> &'static str {
643 self.0
644 }
645 async fn shutdown(
646 &self,
647 _reason: camel_api::StepShutdownReason,
648 ) -> Result<(), camel_api::CamelError> {
649 Ok(())
650 }
651 }
652
653 #[tokio::test]
655 async fn compile_children_segments_multiple_stateful_children() {
656 use std::collections::HashMap;
657 use std::sync::Mutex;
658
659 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
660 use camel_api::{
661 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
662 };
663 use camel_bean::BeanRegistry;
664 use camel_component_api::{
665 ComponentContext, NoOpComponentContext, RuntimeObservability,
666 test_support::NoopRuntimeObservability,
667 };
668
669 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
670
671 let mut reg = StepCompilerRegistry::new();
672 reg.register(Box::new(LifecycleInjectorCompiler {
673 handle: handle.clone(),
674 }));
675 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
676
677 let pc = ProducerContext::default();
678 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
679 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
680 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
681 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
682 let staging = FunctionStagingMode::DirectAdd;
683 let idempotent_repositories = crate::IdempotentRegistry::new();
684 let claim_check_repositories = crate::ClaimCheckRegistry::new();
685 let cache_repositories = crate::CacheRegistry::new();
686
687 let ctx = CompilationContext {
688 producer_ctx: &pc,
689 rt,
690 languages: &languages,
691 beans: &beans,
692 function_invoker: None,
693 component_ctx,
694 route_id: None,
695 staging_mode: &staging,
696 idempotent_repositories: &idempotent_repositories,
697 claim_check_repositories: &claim_check_repositories,
698 cache_repositories: &cache_repositories,
699 intercept: InterceptRules::default(),
700 };
701
702 let filter_step = BuilderStep::Filter {
704 predicate: FilterPredicate::new(|_| true),
705 steps: vec![
706 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
707 Box::pin(async move { Ok(ex) })
708 }))),
709 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
710 Box::pin(async move { Ok(ex) })
711 }))),
712 ],
713 };
714
715 let result = reg.compile_step(filter_step, 0, &ctx);
716 let compiled = result
717 .expect("compilation should succeed")
718 .expect("should match");
719
720 match compiled {
721 CompiledStep::Segment { lifecycle, .. } => {
722 let handles = lifecycle.expect("Segment should have lifecycle handles");
723 assert_eq!(
724 handles.len(),
725 2,
726 "expected 2 lifecycle handles for 2 children"
727 );
728 for h in &handles {
729 assert_eq!(h.name(), "multi", "all handles should be 'multi'");
730 }
731 }
732 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
733 }
734 }
735
736 #[tokio::test]
738 async fn compile_children_segments_multi_branch_accumulation() {
739 use std::collections::HashMap;
740 use std::sync::Mutex;
741
742 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
743 use crate::lifecycle::application::route_definition::WhenStep;
744 use camel_api::{
745 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
746 };
747 use camel_bean::BeanRegistry;
748 use camel_component_api::{
749 ComponentContext, NoOpComponentContext, RuntimeObservability,
750 test_support::NoopRuntimeObservability,
751 };
752
753 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
754
755 let mut reg = StepCompilerRegistry::new();
756 reg.register(Box::new(LifecycleInjectorCompiler {
757 handle: handle.clone(),
758 }));
759 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
760
761 let pc = ProducerContext::default();
762 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
763 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
764 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
765 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
766 let staging = FunctionStagingMode::DirectAdd;
767 let idempotent_repositories = crate::IdempotentRegistry::new();
768 let claim_check_repositories = crate::ClaimCheckRegistry::new();
769 let cache_repositories = crate::CacheRegistry::new();
770
771 let ctx = CompilationContext {
772 producer_ctx: &pc,
773 rt,
774 languages: &languages,
775 beans: &beans,
776 function_invoker: None,
777 component_ctx,
778 route_id: None,
779 staging_mode: &staging,
780 idempotent_repositories: &idempotent_repositories,
781 claim_check_repositories: &claim_check_repositories,
782 cache_repositories: &cache_repositories,
783 intercept: InterceptRules::default(),
784 };
785
786 let choice_step = BuilderStep::Choice {
788 whens: vec![
789 WhenStep {
790 predicate: FilterPredicate::new(|_| true),
791 steps: vec![BuilderStep::Processor(OpaqueProcessor(
792 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
793 ))],
794 },
795 WhenStep {
796 predicate: FilterPredicate::new(|_| false),
797 steps: vec![BuilderStep::Processor(OpaqueProcessor(
798 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
799 ))],
800 },
801 ],
802 otherwise: None,
803 };
804
805 let result = reg.compile_step(choice_step, 0, &ctx);
806 let compiled = result
807 .expect("compilation should succeed")
808 .expect("should match");
809
810 match compiled {
811 CompiledStep::Segment { lifecycle, .. } => {
812 let handles = lifecycle.expect("Segment should have lifecycle handles");
813 assert_eq!(
814 handles.len(),
815 2,
816 "expected 2 lifecycle handles from 2 branches"
817 );
818 for h in &handles {
819 assert_eq!(h.name(), "branch", "all handles should be 'branch'");
820 }
821 }
822 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
823 }
824 }
825
826 #[tokio::test]
829 async fn compile_children_segments_nested_segment_flattening() {
830 use std::collections::HashMap;
831 use std::sync::Mutex;
832
833 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
834 use camel_api::{
835 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
836 };
837 use camel_bean::BeanRegistry;
838 use camel_component_api::{
839 ComponentContext, NoOpComponentContext, RuntimeObservability,
840 test_support::NoopRuntimeObservability,
841 };
842
843 let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
844
845 let mut reg = StepCompilerRegistry::new();
846 reg.register(Box::new(LifecycleInjectorCompiler {
847 handle: inner_handle.clone(),
848 }));
849 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
850
851 let pc = ProducerContext::default();
852 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
853 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
854 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
855 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
856 let staging = FunctionStagingMode::DirectAdd;
857 let idempotent_repositories = crate::IdempotentRegistry::new();
858 let claim_check_repositories = crate::ClaimCheckRegistry::new();
859 let cache_repositories = crate::CacheRegistry::new();
860
861 let ctx = CompilationContext {
862 producer_ctx: &pc,
863 rt,
864 languages: &languages,
865 beans: &beans,
866 function_invoker: None,
867 component_ctx,
868 route_id: None,
869 staging_mode: &staging,
870 idempotent_repositories: &idempotent_repositories,
871 claim_check_repositories: &claim_check_repositories,
872 cache_repositories: &cache_repositories,
873 intercept: InterceptRules::default(),
874 };
875
876 let inner_filter = BuilderStep::Filter {
880 predicate: FilterPredicate::new(|_| true),
881 steps: vec![BuilderStep::Processor(OpaqueProcessor(
882 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
883 ))],
884 };
885
886 let outer_filter = BuilderStep::Filter {
887 predicate: FilterPredicate::new(|_| true),
888 steps: vec![inner_filter],
889 };
890
891 let result = reg.compile_step(outer_filter, 0, &ctx);
892 let compiled = result
893 .expect("compilation should succeed")
894 .expect("should match");
895
896 match compiled {
897 CompiledStep::Segment { lifecycle, .. } => {
898 let handles = lifecycle.expect("outer Segment should have lifecycle handles");
899 assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
900 assert_eq!(
901 handles[0].name(),
902 "deep",
903 "handle should be from innermost child"
904 );
905 }
906 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
907 }
908 }
909}
910
911#[cfg(test)]
912mod dispatch_tests {
913 use super::*;
914 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
915 use camel_api::{BoxProcessor, BoxProcessorExt};
916 use camel_bean::BeanRegistry;
917 use camel_component_api::{
918 ComponentContext, NoOpComponentContext, RuntimeObservability,
919 test_support::NoopRuntimeObservability,
920 };
921 use std::collections::HashMap;
922 use std::sync::Mutex;
923
924 struct ToStopCompiler;
926
927 impl StepCompiler for ToStopCompiler {
928 fn compile(
929 &self,
930 step: BuilderStep,
931 _step_index: usize,
932 _ctx: &CompilationContext,
933 _registry: &StepCompilerRegistry,
934 ) -> Result<CompileOutcome, CamelError> {
935 match step {
936 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
937 other => Ok(CompileOutcome::NotHandled(other)),
938 }
939 }
940 }
941
942 struct ToProcessCompiler;
944
945 impl StepCompiler for ToProcessCompiler {
946 fn compile(
947 &self,
948 step: BuilderStep,
949 _step_index: usize,
950 _ctx: &CompilationContext,
951 _registry: &StepCompilerRegistry,
952 ) -> Result<CompileOutcome, CamelError> {
953 match step {
954 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
955 processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
956 body_contract: None,
957 lifecycle: None,
958 label: None,
959 kind_hint: SpanKindHint::Internal,
960 to_uri: None,
961 })),
962 other => Ok(CompileOutcome::NotHandled(other)),
963 }
964 }
965 }
966
967 #[derive(Clone)]
969 struct NoopPipeline;
970
971 impl camel_api::OutcomePipeline for NoopPipeline {
972 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
973 Box::new(NoopPipeline)
974 }
975
976 fn run<'a>(
977 &'a mut self,
978 exchange: camel_api::Exchange,
979 ) -> std::pin::Pin<
980 Box<dyn std::future::Future<Output = camel_api::PipelineOutcome> + Send + 'a>,
981 > {
982 Box::pin(async move { camel_api::PipelineOutcome::Completed(exchange) })
983 }
984 }
985
986 struct PassThroughCompiler;
989
990 impl StepCompiler for PassThroughCompiler {
991 fn compile(
992 &self,
993 step: BuilderStep,
994 _step_index: usize,
995 _ctx: &CompilationContext,
996 _registry: &StepCompilerRegistry,
997 ) -> Result<CompileOutcome, CamelError> {
998 Ok(CompileOutcome::NotHandled(step))
999 }
1000 }
1001
1002 #[allow(clippy::too_many_arguments)]
1004 fn ctx<'a>(
1005 pc: &'a ProducerContext,
1006 rt: Arc<dyn RuntimeObservability>,
1007 languages: &'a SharedLanguageRegistry,
1008 beans: &'a Arc<Mutex<BeanRegistry>>,
1009 component_ctx: Arc<dyn ComponentContext>,
1010 staging: &'a FunctionStagingMode,
1011 idempotent_repositories: &'a crate::IdempotentRegistry,
1012 claim_check_repositories: &'a crate::ClaimCheckRegistry,
1013 cache_repositories: &'a crate::CacheRegistry,
1014 ) -> CompilationContext<'a> {
1015 CompilationContext {
1016 producer_ctx: pc,
1017 rt,
1018 languages,
1019 beans,
1020 function_invoker: None,
1021 component_ctx,
1022 route_id: None,
1023 staging_mode: staging,
1024 idempotent_repositories,
1025 claim_check_repositories,
1026 cache_repositories,
1027 intercept: InterceptRules::default(),
1028 }
1029 }
1030
1031 #[test]
1034 fn compile_step_preserves_registry_order() {
1035 let pc = ProducerContext::default();
1036 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1037 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1038 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1039 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1040 let staging = FunctionStagingMode::DirectAdd;
1041 let idempotent_repositories = crate::IdempotentRegistry::new();
1042 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1043 let cache_repositories = crate::CacheRegistry::new();
1044
1045 let context = ctx(
1046 &pc,
1047 rt,
1048 &languages,
1049 &beans,
1050 component_ctx,
1051 &staging,
1052 &idempotent_repositories,
1053 &claim_check_repositories,
1054 &cache_repositories,
1055 );
1056
1057 let mut reg = StepCompilerRegistry::new();
1060 reg.register(Box::new(ToStopCompiler));
1061 reg.register(Box::new(ToProcessCompiler));
1062
1063 let result = reg
1064 .compile_step(BuilderStep::To("test".into()), 0, &context)
1065 .expect("compilation should succeed")
1066 .expect("should match");
1067
1068 assert!(
1069 matches!(result, CompiledStep::Stop),
1070 "expected ToStopCompiler (first registered) to win, got {result:?}"
1071 );
1072 }
1073
1074 #[test]
1076 fn set_label_noop_on_stop() {
1077 let mut step = CompiledStep::Stop;
1078 step.set_label(Some("x".into()));
1079 assert!(
1080 matches!(step, CompiledStep::Stop),
1081 "set_label must not change a Stop step, got {step:?}"
1082 );
1083 }
1084
1085 #[test]
1088 fn compile_step_stamps_label() {
1089 let pc = ProducerContext::default();
1090 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1091 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1092 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1093 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1094 let staging = FunctionStagingMode::DirectAdd;
1095 let idempotent_repositories = crate::IdempotentRegistry::new();
1096 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1097 let cache_repositories = crate::CacheRegistry::new();
1098
1099 let context = ctx(
1100 &pc,
1101 rt,
1102 &languages,
1103 &beans,
1104 component_ctx,
1105 &staging,
1106 &idempotent_repositories,
1107 &claim_check_repositories,
1108 &cache_repositories,
1109 );
1110
1111 let mut reg = StepCompilerRegistry::new();
1112 reg.register(Box::new(ToProcessCompiler));
1113
1114 let result = reg
1115 .compile_step(BuilderStep::To("direct:x".into()), 0, &context)
1116 .expect("compilation should succeed")
1117 .expect("should match");
1118
1119 match result {
1120 CompiledStep::Process { label, .. } => {
1121 assert_eq!(label.as_deref(), Some("to:direct"));
1122 }
1123 other => panic!("expected Process, got {other:?}"),
1124 }
1125 }
1126
1127 #[test]
1131 fn set_kind_hint_noop_on_stop_and_segment() {
1132 use camel_api::SpanKindHint;
1133
1134 let mut step = CompiledStep::Stop;
1136 step.set_kind_hint(SpanKindHint::Client);
1137 assert!(
1138 matches!(step, CompiledStep::Stop),
1139 "set_kind_hint must not change a Stop step, got {step:?}"
1140 );
1141
1142 let mut step = CompiledStep::Segment {
1144 segment: camel_api::OutcomeSegment::new(Box::new(NoopPipeline)),
1145 body_contract: None,
1146 lifecycle: None,
1147 label: Some("seg".into()),
1148 };
1149 step.set_kind_hint(SpanKindHint::Client);
1150 match step {
1151 CompiledStep::Segment { label, .. } => {
1152 assert_eq!(label.as_deref(), Some("seg"));
1153 }
1154 other => panic!("expected Segment to stay a Segment, got {other:?}"),
1155 }
1156 }
1157
1158 #[test]
1161 fn compile_step_stamps_kind_hint() {
1162 use camel_api::SpanKindHint;
1163
1164 let pc = ProducerContext::default();
1165 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1166 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1167 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1168 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1169 let staging = FunctionStagingMode::DirectAdd;
1170 let idempotent_repositories = crate::IdempotentRegistry::new();
1171 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1172 let cache_repositories = crate::CacheRegistry::new();
1173
1174 let context = ctx(
1175 &pc,
1176 rt,
1177 &languages,
1178 &beans,
1179 component_ctx,
1180 &staging,
1181 &idempotent_repositories,
1182 &claim_check_repositories,
1183 &cache_repositories,
1184 );
1185
1186 let mut reg = StepCompilerRegistry::new();
1187 reg.register(Box::new(ToProcessCompiler));
1188
1189 let result = reg
1190 .compile_step(BuilderStep::To("kafka:orders".into()), 0, &context)
1191 .expect("compilation should succeed")
1192 .expect("should match");
1193
1194 match result {
1195 CompiledStep::Process {
1196 kind_hint, label, ..
1197 } => {
1198 assert_eq!(kind_hint, SpanKindHint::Producer);
1199 assert_eq!(label.as_deref(), Some("to:kafka"));
1200 }
1201 other => panic!("expected Process, got {other:?}"),
1202 }
1203 }
1204
1205 #[test]
1210 fn compiled_to_step_retains_declared_uri() {
1211 let pc = ProducerContext::default();
1212 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1213 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1214 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1215 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1216 let staging = FunctionStagingMode::DirectAdd;
1217 let idempotent_repositories = crate::IdempotentRegistry::new();
1218 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1219 let cache_repositories = crate::CacheRegistry::new();
1220
1221 let context = ctx(
1222 &pc,
1223 rt,
1224 &languages,
1225 &beans,
1226 component_ctx,
1227 &staging,
1228 &idempotent_repositories,
1229 &claim_check_repositories,
1230 &cache_repositories,
1231 );
1232
1233 let mut reg = StepCompilerRegistry::new();
1234 reg.register(Box::new(ToProcessCompiler));
1235
1236 let result = reg
1237 .compile_step(BuilderStep::To("direct:orders".into()), 0, &context)
1238 .expect("compilation should succeed")
1239 .expect("should match");
1240
1241 match result {
1242 CompiledStep::Process { to_uri, .. } => {
1243 assert_eq!(to_uri, Some(Arc::from("direct:orders")));
1244 }
1245 other => panic!("expected Process, got {other:?}"),
1246 }
1247 }
1248
1249 #[test]
1251 fn compiled_processor_step_has_no_declared_uri() {
1252 use camel_api::OpaqueProcessor;
1253
1254 let pc = ProducerContext::default();
1255 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1256 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1257 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1258 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1259 let staging = FunctionStagingMode::DirectAdd;
1260 let idempotent_repositories = crate::IdempotentRegistry::new();
1261 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1262 let cache_repositories = crate::CacheRegistry::new();
1263
1264 let context = ctx(
1265 &pc,
1266 rt,
1267 &languages,
1268 &beans,
1269 component_ctx,
1270 &staging,
1271 &idempotent_repositories,
1272 &claim_check_repositories,
1273 &cache_repositories,
1274 );
1275
1276 let mut reg = StepCompilerRegistry::new();
1277 reg.register(Box::new(super::core::CoreCompiler));
1278
1279 let result = reg
1280 .compile_step(
1281 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
1282 Box::pin(async move { Ok(ex) })
1283 }))),
1284 0,
1285 &context,
1286 )
1287 .expect("compilation should succeed")
1288 .expect("should match");
1289
1290 match result {
1291 CompiledStep::Process { to_uri, .. } => {
1292 assert_eq!(to_uri, None);
1293 }
1294 other => panic!("expected Process, got {other:?}"),
1295 }
1296 }
1297
1298 #[test]
1301 fn compile_step_unhandled_returns_ok_none() {
1302 let pc = ProducerContext::default();
1303 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1304 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1305 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1306 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1307 let staging = FunctionStagingMode::DirectAdd;
1308 let idempotent_repositories = crate::IdempotentRegistry::new();
1309 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1310 let cache_repositories = crate::CacheRegistry::new();
1311
1312 let context = ctx(
1313 &pc,
1314 rt,
1315 &languages,
1316 &beans,
1317 component_ctx,
1318 &staging,
1319 &idempotent_repositories,
1320 &claim_check_repositories,
1321 &cache_repositories,
1322 );
1323
1324 let mut reg = StepCompilerRegistry::new();
1326 reg.register(Box::new(ToStopCompiler));
1327
1328 let result = reg.compile_step(
1330 BuilderStep::Log {
1331 level: camel_processor::LogLevel::Info,
1332 message: "unhandled".into(),
1333 },
1334 0,
1335 &context,
1336 );
1337
1338 assert!(
1339 matches!(result, Ok(None)),
1340 "expected Ok(None), got {result:?}"
1341 );
1342 }
1343
1344 #[test]
1348 fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
1349 let pc = ProducerContext::default();
1350 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1351 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1352 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1353 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1354 let staging = FunctionStagingMode::DirectAdd;
1355 let idempotent_repositories = crate::IdempotentRegistry::new();
1356 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1357 let cache_repositories = crate::CacheRegistry::new();
1358
1359 let context = ctx(
1360 &pc,
1361 rt,
1362 &languages,
1363 &beans,
1364 component_ctx,
1365 &staging,
1366 &idempotent_repositories,
1367 &claim_check_repositories,
1368 &cache_repositories,
1369 );
1370
1371 let mut reg = StepCompilerRegistry::new();
1374 reg.register(Box::new(PassThroughCompiler));
1375 reg.register(Box::new(ToStopCompiler));
1376
1377 let result = reg
1378 .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1379 .expect("compilation should succeed")
1380 .expect("should match");
1381
1382 assert!(
1383 matches!(result, CompiledStep::Stop),
1384 "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1385 );
1386 }
1387}