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 },
66 Stop,
69 Segment {
73 segment: camel_api::OutcomeSegment,
74 body_contract: Option<BodyType>,
75 lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
80 label: Option<Arc<str>>,
83 },
84}
85
86impl CompiledStep {
87 pub(crate) fn set_label(&mut self, label: Option<String>) {
90 let label = label.map(Arc::from);
91 match self {
92 CompiledStep::Process { label: slot, .. }
93 | CompiledStep::Segment { label: slot, .. } => {
94 *slot = label;
95 }
96 CompiledStep::Stop => {}
97 }
98 }
99
100 pub(crate) fn set_kind_hint(&mut self, hint: SpanKindHint) {
104 match self {
105 CompiledStep::Process {
106 kind_hint: slot, ..
107 } => *slot = hint,
108 CompiledStep::Stop | CompiledStep::Segment { .. } => {}
109 }
110 }
111}
112
113pub(crate) enum CompileOutcome {
120 Matched(CompiledStep),
121 NotHandled(BuilderStep),
122}
123
124pub(crate) trait StepCompiler: Send + Sync {
131 fn compile(
132 &self,
133 step: BuilderStep,
134 step_index: usize,
135 ctx: &CompilationContext,
136 registry: &StepCompilerRegistry,
137 ) -> Result<CompileOutcome, CamelError>;
138}
139
140pub(crate) struct CompilationContext<'a> {
142 pub producer_ctx: &'a ProducerContext,
143 pub rt: Arc<dyn RuntimeObservability>,
144 pub languages: &'a SharedLanguageRegistry,
145 pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
146 pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
147 pub component_ctx: Arc<dyn ComponentContext>,
148 pub route_id: Option<&'a str>,
149 pub staging_mode: &'a FunctionStagingMode,
150 pub idempotent_repositories: &'a IdempotentRegistry,
153 pub claim_check_repositories: &'a ClaimCheckRegistry,
156 pub cache_repositories: &'a CacheRegistry,
160 pub intercept: InterceptRules,
163}
164
165impl<'a> CompilationContext<'a> {
166 pub fn compile_children(
169 &self,
170 steps: Vec<BuilderStep>,
171 registry: &StepCompilerRegistry,
172 ) -> Result<Vec<CompiledStep>, CamelError> {
173 registry.compile_steps(steps, self)
174 }
175
176 #[allow(clippy::type_complexity)]
186 pub fn compile_children_segments(
187 &self,
188 steps: Vec<BuilderStep>,
189 registry: &StepCompilerRegistry,
190 ) -> Result<
191 (
192 Vec<Box<dyn camel_api::OutcomePipeline>>,
193 Vec<Arc<dyn camel_api::StepLifecycle>>,
194 ),
195 CamelError,
196 > {
197 let pairs = self.compile_children(steps, registry)?;
198 let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
199 let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
200 .into_iter()
201 .map(|c| match c {
202 CompiledStep::Process {
203 processor,
204 body_contract,
205 lifecycle,
206 label: _,
207 kind_hint: _,
208 } => {
209 if let Some(lc) = lifecycle {
210 lifecycle_handles.push(lc);
211 }
212 let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
213 crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
214 processor,
215 ),
216 );
217 match body_contract {
218 Some(contract) => Box::new(
219 crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
220 inner, contract,
221 ),
222 ),
223 None => inner,
224 }
225 }
226 CompiledStep::Stop => {
227 Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
228 as Box<dyn camel_api::OutcomePipeline>
229 }
230 CompiledStep::Segment {
231 segment,
232 body_contract: _,
233 lifecycle,
234 label: _,
235 } => {
236 if let Some(lcs) = lifecycle {
237 lifecycle_handles.extend(lcs);
238 }
239 Box::new(segment)
240 }
241 })
242 .collect();
243 Ok((segments, lifecycle_handles))
244 }
245}
246
247pub(crate) struct StepCompilerRegistry {
250 compilers: Vec<Box<dyn StepCompiler>>,
251}
252
253impl StepCompilerRegistry {
254 pub fn new() -> Self {
255 Self {
256 compilers: Vec::new(),
257 }
258 }
259
260 pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
261 self.compilers.push(compiler);
262 }
263
264 pub fn compile_step(
268 &self,
269 step: BuilderStep,
270 step_index: usize,
271 ctx: &CompilationContext,
272 ) -> Result<Option<CompiledStep>, CamelError> {
273 let label = step.span_label();
276 let kind_hint = step.span_kind_hint();
277 let mut step = step;
278 for compiler in &self.compilers {
279 match compiler.compile(step, step_index, ctx, self)? {
280 CompileOutcome::Matched(mut s) => {
281 s.set_label(label);
282 s.set_kind_hint(kind_hint);
283 return Ok(Some(s));
284 }
285 CompileOutcome::NotHandled(s) => step = s,
286 }
287 }
288 Ok(None)
289 }
290
291 pub fn compile_steps(
293 &self,
294 steps: Vec<BuilderStep>,
295 ctx: &CompilationContext,
296 ) -> Result<Vec<CompiledStep>, CamelError> {
297 let mut out = Vec::with_capacity(steps.len());
298 for (i, step) in steps.into_iter().enumerate() {
299 match self.compile_step(step, i, ctx)? {
300 Some(c) => out.push(c),
301 None => {
302 return Err(CamelError::RouteError(
303 "no compiler registered for step variant".into(),
304 ));
305 }
306 }
307 }
308 Ok(out)
309 }
310}
311
312pub(super) struct ResolvedSend {
315 pub producer: BoxProcessor,
316 pub body_contract: Option<BodyType>,
317 pub lifecycle: Option<Arc<dyn StepLifecycle>>,
318}
319
320pub(super) fn resolve_send(
325 ctx: &CompilationContext,
326 uri: &str,
327) -> Result<ResolvedSend, CamelError> {
328 let parsed = parse_uri(uri)?;
329 let component = ctx
330 .component_ctx
331 .resolve_component(&parsed.scheme)
332 .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
333 let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
334 let body_contract = endpoint.body_contract();
335 let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
336 let lifecycle: Option<Arc<dyn StepLifecycle>> = endpoint.lifecycle();
340 Ok(ResolvedSend {
341 producer,
342 body_contract,
343 lifecycle,
344 })
345}
346
347pub(crate) fn resolve_producer_with_lifecycle(
352 ctx: &CompilationContext,
353 uri: &str,
354) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
355 let resolved = resolve_send(ctx, uri)?;
356 Ok((resolved.producer, resolved.lifecycle))
357}
358
359pub(crate) fn resolve_producer(
365 ctx: &CompilationContext,
366 uri: &str,
367) -> Result<BoxProcessor, CamelError> {
368 Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
369}
370
371pub(super) fn pack_lifecycles(
374 lifecycles: Vec<Arc<dyn StepLifecycle>>,
375) -> Option<Vec<Arc<dyn StepLifecycle>>> {
376 if lifecycles.is_empty() {
377 None
378 } else {
379 Some(lifecycles)
380 }
381}
382
383pub(crate) fn build_registry() -> StepCompilerRegistry {
385 let mut reg = StepCompilerRegistry::new();
386 reg.register(Box::new(core::CoreCompiler));
387 reg.register(Box::new(endpoints::EndpointsCompiler));
388 reg.register(Box::new(transforms::TransformsCompiler));
389 reg.register(Box::new(routing::RoutingCompiler));
390 reg.register(Box::new(control_flow::ControlFlowCompiler));
391 reg.register(Box::new(splitting::SplittingCompiler));
392 reg.register(Box::new(error_handling::ErrorHandlingCompiler));
393 reg
394}
395
396#[cfg(test)]
397mod segment_tests {
398 use super::*;
399 use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
400 use std::future::Future;
401 use std::pin::Pin;
402
403 #[derive(Clone)]
404 struct EchoSegment;
405
406 impl OutcomePipeline for EchoSegment {
407 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
408 Box::new(EchoSegment)
409 }
410 fn run<'a>(
411 &'a mut self,
412 exchange: Exchange,
413 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
414 Box::pin(async move { PipelineOutcome::Completed(exchange) })
415 }
416 }
417
418 #[test]
419 fn compiled_step_segment_clone_compiles() {
420 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
421 let step = CompiledStep::Segment {
422 segment: seg,
423 body_contract: None,
424 lifecycle: None,
425 label: None,
426 };
427 let _cloned = step.clone();
428 if let CompiledStep::Segment { .. } = _cloned {
429 } else {
431 panic!("clone should preserve variant");
432 }
433 }
434
435 #[test]
436 fn compiled_step_segment_debug_renders() {
437 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
438 let step = CompiledStep::Segment {
439 segment: seg,
440 body_contract: None,
441 lifecycle: None,
442 label: None,
443 };
444 let s = format!("{:?}", step);
445 assert!(
446 s.contains("Segment"),
447 "debug should mention Segment variant: {s}"
448 );
449 }
450
451 #[test]
452 fn outcome_segment_satisfies_clone_send_static() {
453 fn assert_traits<T: Clone + Send + 'static>() {}
454 assert_traits::<camel_api::OutcomeSegment>();
455 }
456
457 #[tokio::test]
458 #[allow(clippy::arc_with_non_send_sync)]
459 async fn outcome_segment_survives_arcswap_swap() {
460 use arc_swap::ArcSwap;
461 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
462 use std::sync::Arc;
463
464 #[derive(Clone)]
465 struct EchoSegment;
466 impl OutcomePipeline for EchoSegment {
467 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
468 Box::new(EchoSegment)
469 }
470 fn run<'a>(
471 &'a mut self,
472 ex: Exchange,
473 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
474 {
475 Box::pin(async move { PipelineOutcome::Completed(ex) })
476 }
477 }
478
479 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
480 let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
481 slot.store(Arc::new(Some(seg.clone())));
482 slot.store(Arc::new(Some(seg)));
483
484 let mut borrowed = slot.load().as_ref().clone().unwrap();
485 let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
486 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
487 }
488
489 #[derive(Debug)]
491 struct TestLifecycle;
492
493 #[async_trait::async_trait]
494 impl camel_api::StepLifecycle for TestLifecycle {
495 fn name(&self) -> &'static str {
496 "test-lifecycle"
497 }
498 async fn shutdown(
499 &self,
500 _reason: camel_api::StepShutdownReason,
501 ) -> Result<(), camel_api::CamelError> {
502 Ok(())
503 }
504 }
505
506 struct LifecycleInjectorCompiler {
509 handle: Arc<dyn camel_api::StepLifecycle>,
510 }
511
512 impl StepCompiler for LifecycleInjectorCompiler {
513 fn compile(
514 &self,
515 step: BuilderStep,
516 _step_index: usize,
517 _ctx: &CompilationContext,
518 _registry: &StepCompilerRegistry,
519 ) -> Result<CompileOutcome, CamelError> {
520 match step {
521 BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
522 processor: op.0,
523 body_contract: None,
524 lifecycle: Some(self.handle.clone()),
525 label: None,
526 kind_hint: SpanKindHint::Internal,
527 })),
528 other => Ok(CompileOutcome::NotHandled(other)),
529 }
530 }
531 }
532
533 #[tokio::test]
534 async fn compile_children_segments_bubbles_child_lifecycle() {
535 use std::collections::HashMap;
536 use std::sync::Mutex;
537
538 use camel_api::{
539 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
540 };
541 use camel_bean::BeanRegistry;
542 use camel_component_api::{
543 ComponentContext, NoOpComponentContext, RuntimeObservability,
544 test_support::NoopRuntimeObservability,
545 };
546
547 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
548
549 let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
550
551 let mut reg = StepCompilerRegistry::new();
554 reg.register(Box::new(LifecycleInjectorCompiler {
555 handle: handle.clone(),
556 }));
557 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
558
559 let pc = ProducerContext::default();
560 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
561 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
562 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
563 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
564 let staging = FunctionStagingMode::DirectAdd;
565 let idempotent_repositories = crate::IdempotentRegistry::new();
566 let claim_check_repositories = crate::ClaimCheckRegistry::new();
567 let cache_repositories = crate::CacheRegistry::new();
568
569 let ctx = CompilationContext {
570 producer_ctx: &pc,
571 rt,
572 languages: &languages,
573 beans: &beans,
574 function_invoker: None,
575 component_ctx,
576 route_id: None,
577 staging_mode: &staging,
578 idempotent_repositories: &idempotent_repositories,
579 claim_check_repositories: &claim_check_repositories,
580 cache_repositories: &cache_repositories,
581 intercept: InterceptRules::default(),
582 };
583
584 let filter_step = BuilderStep::Filter {
586 predicate: FilterPredicate::new(|_| true),
587 steps: vec![BuilderStep::Processor(OpaqueProcessor(
588 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
589 ))],
590 };
591
592 let result = reg.compile_step(filter_step, 0, &ctx);
593 let compiled = result
594 .expect("compilation should succeed")
595 .expect("should match");
596
597 match compiled {
598 CompiledStep::Segment {
599 lifecycle,
600 body_contract,
601 ..
602 } => {
603 assert_eq!(body_contract, None, "body_contract should be None");
604 let handles = lifecycle.expect("Segment should have lifecycle handles");
605 assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
606 assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
607 }
608 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
609 }
610 }
611
612 #[derive(Debug)]
614 struct NamedLifecycle(&'static str);
615
616 #[async_trait::async_trait]
617 impl camel_api::StepLifecycle for NamedLifecycle {
618 fn name(&self) -> &'static str {
619 self.0
620 }
621 async fn shutdown(
622 &self,
623 _reason: camel_api::StepShutdownReason,
624 ) -> Result<(), camel_api::CamelError> {
625 Ok(())
626 }
627 }
628
629 #[tokio::test]
631 async fn compile_children_segments_multiple_stateful_children() {
632 use std::collections::HashMap;
633 use std::sync::Mutex;
634
635 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
636 use camel_api::{
637 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
638 };
639 use camel_bean::BeanRegistry;
640 use camel_component_api::{
641 ComponentContext, NoOpComponentContext, RuntimeObservability,
642 test_support::NoopRuntimeObservability,
643 };
644
645 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
646
647 let mut reg = StepCompilerRegistry::new();
648 reg.register(Box::new(LifecycleInjectorCompiler {
649 handle: handle.clone(),
650 }));
651 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
652
653 let pc = ProducerContext::default();
654 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
655 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
656 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
657 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
658 let staging = FunctionStagingMode::DirectAdd;
659 let idempotent_repositories = crate::IdempotentRegistry::new();
660 let claim_check_repositories = crate::ClaimCheckRegistry::new();
661 let cache_repositories = crate::CacheRegistry::new();
662
663 let ctx = CompilationContext {
664 producer_ctx: &pc,
665 rt,
666 languages: &languages,
667 beans: &beans,
668 function_invoker: None,
669 component_ctx,
670 route_id: None,
671 staging_mode: &staging,
672 idempotent_repositories: &idempotent_repositories,
673 claim_check_repositories: &claim_check_repositories,
674 cache_repositories: &cache_repositories,
675 intercept: InterceptRules::default(),
676 };
677
678 let filter_step = BuilderStep::Filter {
680 predicate: FilterPredicate::new(|_| true),
681 steps: vec![
682 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
683 Box::pin(async move { Ok(ex) })
684 }))),
685 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
686 Box::pin(async move { Ok(ex) })
687 }))),
688 ],
689 };
690
691 let result = reg.compile_step(filter_step, 0, &ctx);
692 let compiled = result
693 .expect("compilation should succeed")
694 .expect("should match");
695
696 match compiled {
697 CompiledStep::Segment { lifecycle, .. } => {
698 let handles = lifecycle.expect("Segment should have lifecycle handles");
699 assert_eq!(
700 handles.len(),
701 2,
702 "expected 2 lifecycle handles for 2 children"
703 );
704 for h in &handles {
705 assert_eq!(h.name(), "multi", "all handles should be 'multi'");
706 }
707 }
708 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
709 }
710 }
711
712 #[tokio::test]
714 async fn compile_children_segments_multi_branch_accumulation() {
715 use std::collections::HashMap;
716 use std::sync::Mutex;
717
718 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
719 use crate::lifecycle::application::route_definition::WhenStep;
720 use camel_api::{
721 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
722 };
723 use camel_bean::BeanRegistry;
724 use camel_component_api::{
725 ComponentContext, NoOpComponentContext, RuntimeObservability,
726 test_support::NoopRuntimeObservability,
727 };
728
729 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
730
731 let mut reg = StepCompilerRegistry::new();
732 reg.register(Box::new(LifecycleInjectorCompiler {
733 handle: handle.clone(),
734 }));
735 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
736
737 let pc = ProducerContext::default();
738 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
739 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
740 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
741 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
742 let staging = FunctionStagingMode::DirectAdd;
743 let idempotent_repositories = crate::IdempotentRegistry::new();
744 let claim_check_repositories = crate::ClaimCheckRegistry::new();
745 let cache_repositories = crate::CacheRegistry::new();
746
747 let ctx = CompilationContext {
748 producer_ctx: &pc,
749 rt,
750 languages: &languages,
751 beans: &beans,
752 function_invoker: None,
753 component_ctx,
754 route_id: None,
755 staging_mode: &staging,
756 idempotent_repositories: &idempotent_repositories,
757 claim_check_repositories: &claim_check_repositories,
758 cache_repositories: &cache_repositories,
759 intercept: InterceptRules::default(),
760 };
761
762 let choice_step = BuilderStep::Choice {
764 whens: vec![
765 WhenStep {
766 predicate: FilterPredicate::new(|_| true),
767 steps: vec![BuilderStep::Processor(OpaqueProcessor(
768 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
769 ))],
770 },
771 WhenStep {
772 predicate: FilterPredicate::new(|_| false),
773 steps: vec![BuilderStep::Processor(OpaqueProcessor(
774 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
775 ))],
776 },
777 ],
778 otherwise: None,
779 };
780
781 let result = reg.compile_step(choice_step, 0, &ctx);
782 let compiled = result
783 .expect("compilation should succeed")
784 .expect("should match");
785
786 match compiled {
787 CompiledStep::Segment { lifecycle, .. } => {
788 let handles = lifecycle.expect("Segment should have lifecycle handles");
789 assert_eq!(
790 handles.len(),
791 2,
792 "expected 2 lifecycle handles from 2 branches"
793 );
794 for h in &handles {
795 assert_eq!(h.name(), "branch", "all handles should be 'branch'");
796 }
797 }
798 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
799 }
800 }
801
802 #[tokio::test]
805 async fn compile_children_segments_nested_segment_flattening() {
806 use std::collections::HashMap;
807 use std::sync::Mutex;
808
809 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
810 use camel_api::{
811 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
812 };
813 use camel_bean::BeanRegistry;
814 use camel_component_api::{
815 ComponentContext, NoOpComponentContext, RuntimeObservability,
816 test_support::NoopRuntimeObservability,
817 };
818
819 let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
820
821 let mut reg = StepCompilerRegistry::new();
822 reg.register(Box::new(LifecycleInjectorCompiler {
823 handle: inner_handle.clone(),
824 }));
825 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
826
827 let pc = ProducerContext::default();
828 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
829 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
830 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
831 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
832 let staging = FunctionStagingMode::DirectAdd;
833 let idempotent_repositories = crate::IdempotentRegistry::new();
834 let claim_check_repositories = crate::ClaimCheckRegistry::new();
835 let cache_repositories = crate::CacheRegistry::new();
836
837 let ctx = CompilationContext {
838 producer_ctx: &pc,
839 rt,
840 languages: &languages,
841 beans: &beans,
842 function_invoker: None,
843 component_ctx,
844 route_id: None,
845 staging_mode: &staging,
846 idempotent_repositories: &idempotent_repositories,
847 claim_check_repositories: &claim_check_repositories,
848 cache_repositories: &cache_repositories,
849 intercept: InterceptRules::default(),
850 };
851
852 let inner_filter = BuilderStep::Filter {
856 predicate: FilterPredicate::new(|_| true),
857 steps: vec![BuilderStep::Processor(OpaqueProcessor(
858 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
859 ))],
860 };
861
862 let outer_filter = BuilderStep::Filter {
863 predicate: FilterPredicate::new(|_| true),
864 steps: vec![inner_filter],
865 };
866
867 let result = reg.compile_step(outer_filter, 0, &ctx);
868 let compiled = result
869 .expect("compilation should succeed")
870 .expect("should match");
871
872 match compiled {
873 CompiledStep::Segment { lifecycle, .. } => {
874 let handles = lifecycle.expect("outer Segment should have lifecycle handles");
875 assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
876 assert_eq!(
877 handles[0].name(),
878 "deep",
879 "handle should be from innermost child"
880 );
881 }
882 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
883 }
884 }
885}
886
887#[cfg(test)]
888mod dispatch_tests {
889 use super::*;
890 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
891 use camel_api::{BoxProcessor, BoxProcessorExt};
892 use camel_bean::BeanRegistry;
893 use camel_component_api::{
894 ComponentContext, NoOpComponentContext, RuntimeObservability,
895 test_support::NoopRuntimeObservability,
896 };
897 use std::collections::HashMap;
898 use std::sync::Mutex;
899
900 struct ToStopCompiler;
902
903 impl StepCompiler for ToStopCompiler {
904 fn compile(
905 &self,
906 step: BuilderStep,
907 _step_index: usize,
908 _ctx: &CompilationContext,
909 _registry: &StepCompilerRegistry,
910 ) -> Result<CompileOutcome, CamelError> {
911 match step {
912 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
913 other => Ok(CompileOutcome::NotHandled(other)),
914 }
915 }
916 }
917
918 struct ToProcessCompiler;
920
921 impl StepCompiler for ToProcessCompiler {
922 fn compile(
923 &self,
924 step: BuilderStep,
925 _step_index: usize,
926 _ctx: &CompilationContext,
927 _registry: &StepCompilerRegistry,
928 ) -> Result<CompileOutcome, CamelError> {
929 match step {
930 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
931 processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
932 body_contract: None,
933 lifecycle: None,
934 label: None,
935 kind_hint: SpanKindHint::Internal,
936 })),
937 other => Ok(CompileOutcome::NotHandled(other)),
938 }
939 }
940 }
941
942 #[derive(Clone)]
944 struct NoopPipeline;
945
946 impl camel_api::OutcomePipeline for NoopPipeline {
947 fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
948 Box::new(NoopPipeline)
949 }
950
951 fn run<'a>(
952 &'a mut self,
953 exchange: camel_api::Exchange,
954 ) -> std::pin::Pin<
955 Box<dyn std::future::Future<Output = camel_api::PipelineOutcome> + Send + 'a>,
956 > {
957 Box::pin(async move { camel_api::PipelineOutcome::Completed(exchange) })
958 }
959 }
960
961 struct PassThroughCompiler;
964
965 impl StepCompiler for PassThroughCompiler {
966 fn compile(
967 &self,
968 step: BuilderStep,
969 _step_index: usize,
970 _ctx: &CompilationContext,
971 _registry: &StepCompilerRegistry,
972 ) -> Result<CompileOutcome, CamelError> {
973 Ok(CompileOutcome::NotHandled(step))
974 }
975 }
976
977 #[allow(clippy::too_many_arguments)]
979 fn ctx<'a>(
980 pc: &'a ProducerContext,
981 rt: Arc<dyn RuntimeObservability>,
982 languages: &'a SharedLanguageRegistry,
983 beans: &'a Arc<Mutex<BeanRegistry>>,
984 component_ctx: Arc<dyn ComponentContext>,
985 staging: &'a FunctionStagingMode,
986 idempotent_repositories: &'a crate::IdempotentRegistry,
987 claim_check_repositories: &'a crate::ClaimCheckRegistry,
988 cache_repositories: &'a crate::CacheRegistry,
989 ) -> CompilationContext<'a> {
990 CompilationContext {
991 producer_ctx: pc,
992 rt,
993 languages,
994 beans,
995 function_invoker: None,
996 component_ctx,
997 route_id: None,
998 staging_mode: staging,
999 idempotent_repositories,
1000 claim_check_repositories,
1001 cache_repositories,
1002 intercept: InterceptRules::default(),
1003 }
1004 }
1005
1006 #[test]
1009 fn compile_step_preserves_registry_order() {
1010 let pc = ProducerContext::default();
1011 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1012 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1013 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1014 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1015 let staging = FunctionStagingMode::DirectAdd;
1016 let idempotent_repositories = crate::IdempotentRegistry::new();
1017 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1018 let cache_repositories = crate::CacheRegistry::new();
1019
1020 let context = ctx(
1021 &pc,
1022 rt,
1023 &languages,
1024 &beans,
1025 component_ctx,
1026 &staging,
1027 &idempotent_repositories,
1028 &claim_check_repositories,
1029 &cache_repositories,
1030 );
1031
1032 let mut reg = StepCompilerRegistry::new();
1035 reg.register(Box::new(ToStopCompiler));
1036 reg.register(Box::new(ToProcessCompiler));
1037
1038 let result = reg
1039 .compile_step(BuilderStep::To("test".into()), 0, &context)
1040 .expect("compilation should succeed")
1041 .expect("should match");
1042
1043 assert!(
1044 matches!(result, CompiledStep::Stop),
1045 "expected ToStopCompiler (first registered) to win, got {result:?}"
1046 );
1047 }
1048
1049 #[test]
1051 fn set_label_noop_on_stop() {
1052 let mut step = CompiledStep::Stop;
1053 step.set_label(Some("x".into()));
1054 assert!(
1055 matches!(step, CompiledStep::Stop),
1056 "set_label must not change a Stop step, got {step:?}"
1057 );
1058 }
1059
1060 #[test]
1063 fn compile_step_stamps_label() {
1064 let pc = ProducerContext::default();
1065 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1066 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1067 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1068 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1069 let staging = FunctionStagingMode::DirectAdd;
1070 let idempotent_repositories = crate::IdempotentRegistry::new();
1071 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1072 let cache_repositories = crate::CacheRegistry::new();
1073
1074 let context = ctx(
1075 &pc,
1076 rt,
1077 &languages,
1078 &beans,
1079 component_ctx,
1080 &staging,
1081 &idempotent_repositories,
1082 &claim_check_repositories,
1083 &cache_repositories,
1084 );
1085
1086 let mut reg = StepCompilerRegistry::new();
1087 reg.register(Box::new(ToProcessCompiler));
1088
1089 let result = reg
1090 .compile_step(BuilderStep::To("direct:x".into()), 0, &context)
1091 .expect("compilation should succeed")
1092 .expect("should match");
1093
1094 match result {
1095 CompiledStep::Process { label, .. } => {
1096 assert_eq!(label.as_deref(), Some("to:direct"));
1097 }
1098 other => panic!("expected Process, got {other:?}"),
1099 }
1100 }
1101
1102 #[test]
1106 fn set_kind_hint_noop_on_stop_and_segment() {
1107 use camel_api::SpanKindHint;
1108
1109 let mut step = CompiledStep::Stop;
1111 step.set_kind_hint(SpanKindHint::Client);
1112 assert!(
1113 matches!(step, CompiledStep::Stop),
1114 "set_kind_hint must not change a Stop step, got {step:?}"
1115 );
1116
1117 let mut step = CompiledStep::Segment {
1119 segment: camel_api::OutcomeSegment::new(Box::new(NoopPipeline)),
1120 body_contract: None,
1121 lifecycle: None,
1122 label: Some("seg".into()),
1123 };
1124 step.set_kind_hint(SpanKindHint::Client);
1125 match step {
1126 CompiledStep::Segment { label, .. } => {
1127 assert_eq!(label.as_deref(), Some("seg"));
1128 }
1129 other => panic!("expected Segment to stay a Segment, got {other:?}"),
1130 }
1131 }
1132
1133 #[test]
1136 fn compile_step_stamps_kind_hint() {
1137 use camel_api::SpanKindHint;
1138
1139 let pc = ProducerContext::default();
1140 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1141 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1142 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1143 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1144 let staging = FunctionStagingMode::DirectAdd;
1145 let idempotent_repositories = crate::IdempotentRegistry::new();
1146 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1147 let cache_repositories = crate::CacheRegistry::new();
1148
1149 let context = ctx(
1150 &pc,
1151 rt,
1152 &languages,
1153 &beans,
1154 component_ctx,
1155 &staging,
1156 &idempotent_repositories,
1157 &claim_check_repositories,
1158 &cache_repositories,
1159 );
1160
1161 let mut reg = StepCompilerRegistry::new();
1162 reg.register(Box::new(ToProcessCompiler));
1163
1164 let result = reg
1165 .compile_step(BuilderStep::To("kafka:orders".into()), 0, &context)
1166 .expect("compilation should succeed")
1167 .expect("should match");
1168
1169 match result {
1170 CompiledStep::Process {
1171 kind_hint, label, ..
1172 } => {
1173 assert_eq!(kind_hint, SpanKindHint::Producer);
1174 assert_eq!(label.as_deref(), Some("to:kafka"));
1175 }
1176 other => panic!("expected Process, got {other:?}"),
1177 }
1178 }
1179
1180 #[test]
1183 fn compile_step_unhandled_returns_ok_none() {
1184 let pc = ProducerContext::default();
1185 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1186 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1187 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1188 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1189 let staging = FunctionStagingMode::DirectAdd;
1190 let idempotent_repositories = crate::IdempotentRegistry::new();
1191 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1192 let cache_repositories = crate::CacheRegistry::new();
1193
1194 let context = ctx(
1195 &pc,
1196 rt,
1197 &languages,
1198 &beans,
1199 component_ctx,
1200 &staging,
1201 &idempotent_repositories,
1202 &claim_check_repositories,
1203 &cache_repositories,
1204 );
1205
1206 let mut reg = StepCompilerRegistry::new();
1208 reg.register(Box::new(ToStopCompiler));
1209
1210 let result = reg.compile_step(
1212 BuilderStep::Log {
1213 level: camel_processor::LogLevel::Info,
1214 message: "unhandled".into(),
1215 },
1216 0,
1217 &context,
1218 );
1219
1220 assert!(
1221 matches!(result, Ok(None)),
1222 "expected Ok(None), got {result:?}"
1223 );
1224 }
1225
1226 #[test]
1230 fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
1231 let pc = ProducerContext::default();
1232 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1233 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1234 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1235 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1236 let staging = FunctionStagingMode::DirectAdd;
1237 let idempotent_repositories = crate::IdempotentRegistry::new();
1238 let claim_check_repositories = crate::ClaimCheckRegistry::new();
1239 let cache_repositories = crate::CacheRegistry::new();
1240
1241 let context = ctx(
1242 &pc,
1243 rt,
1244 &languages,
1245 &beans,
1246 component_ctx,
1247 &staging,
1248 &idempotent_repositories,
1249 &claim_check_repositories,
1250 &cache_repositories,
1251 );
1252
1253 let mut reg = StepCompilerRegistry::new();
1256 reg.register(Box::new(PassThroughCompiler));
1257 reg.register(Box::new(ToStopCompiler));
1258
1259 let result = reg
1260 .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1261 .expect("compilation should succeed")
1262 .expect("should match");
1263
1264 assert!(
1265 matches!(result, CompiledStep::Stop),
1266 "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1267 );
1268 }
1269}