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(
253 ctx: &CompilationContext,
254 uri: &str,
255) -> Result<BoxProcessor, CamelError> {
256 let parsed = parse_uri(uri)?;
257 let component = ctx
258 .component_ctx
259 .resolve_component(&parsed.scheme)
260 .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
261 let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
262 endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)
263}
264
265pub(super) fn pack_lifecycles(
268 lifecycles: Vec<Arc<dyn StepLifecycle>>,
269) -> Option<Vec<Arc<dyn StepLifecycle>>> {
270 if lifecycles.is_empty() {
271 None
272 } else {
273 Some(lifecycles)
274 }
275}
276
277pub(crate) fn build_registry() -> StepCompilerRegistry {
279 let mut reg = StepCompilerRegistry::new();
280 reg.register(Box::new(core::CoreCompiler));
281 reg.register(Box::new(endpoints::EndpointsCompiler));
282 reg.register(Box::new(transforms::TransformsCompiler));
283 reg.register(Box::new(routing::RoutingCompiler));
284 reg.register(Box::new(control_flow::ControlFlowCompiler));
285 reg.register(Box::new(splitting::SplittingCompiler));
286 reg.register(Box::new(error_handling::ErrorHandlingCompiler));
287 reg
288}
289
290#[cfg(test)]
291mod segment_tests {
292 use super::*;
293 use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
294 use std::future::Future;
295 use std::pin::Pin;
296
297 #[derive(Clone)]
298 struct EchoSegment;
299
300 impl OutcomePipeline for EchoSegment {
301 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
302 Box::new(EchoSegment)
303 }
304 fn run<'a>(
305 &'a mut self,
306 exchange: Exchange,
307 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
308 Box::pin(async move { PipelineOutcome::Completed(exchange) })
309 }
310 }
311
312 #[test]
313 fn compiled_step_segment_clone_compiles() {
314 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
315 let step = CompiledStep::Segment {
316 segment: seg,
317 body_contract: None,
318 lifecycle: None,
319 };
320 let _cloned = step.clone();
321 if let CompiledStep::Segment { segment: _, .. } = _cloned {
322 } else {
324 panic!("clone should preserve variant");
325 }
326 }
327
328 #[test]
329 fn compiled_step_segment_debug_renders() {
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 s = format!("{:?}", step);
337 assert!(
338 s.contains("Segment"),
339 "debug should mention Segment variant: {s}"
340 );
341 }
342
343 #[test]
344 fn outcome_segment_satisfies_clone_send_static() {
345 fn assert_traits<T: Clone + Send + 'static>() {}
346 assert_traits::<camel_api::OutcomeSegment>();
347 }
348
349 #[tokio::test]
350 #[allow(clippy::arc_with_non_send_sync)]
351 async fn outcome_segment_survives_arcswap_swap() {
352 use arc_swap::ArcSwap;
353 use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
354 use std::sync::Arc;
355
356 #[derive(Clone)]
357 struct EchoSegment;
358 impl OutcomePipeline for EchoSegment {
359 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
360 Box::new(EchoSegment)
361 }
362 fn run<'a>(
363 &'a mut self,
364 ex: Exchange,
365 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
366 {
367 Box::pin(async move { PipelineOutcome::Completed(ex) })
368 }
369 }
370
371 let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
372 let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
373 slot.store(Arc::new(Some(seg.clone())));
374 slot.store(Arc::new(Some(seg)));
375
376 let mut borrowed = slot.load().as_ref().clone().unwrap();
377 let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
378 assert!(matches!(outcome, PipelineOutcome::Completed(_)));
379 }
380
381 #[derive(Debug)]
383 struct TestLifecycle;
384
385 #[async_trait::async_trait]
386 impl camel_api::StepLifecycle for TestLifecycle {
387 fn name(&self) -> &'static str {
388 "test-lifecycle"
389 }
390 async fn shutdown(
391 &self,
392 _reason: camel_api::StepShutdownReason,
393 ) -> Result<(), camel_api::CamelError> {
394 Ok(())
395 }
396 }
397
398 struct LifecycleInjectorCompiler {
401 handle: Arc<dyn camel_api::StepLifecycle>,
402 }
403
404 impl StepCompiler for LifecycleInjectorCompiler {
405 fn compile(
406 &self,
407 step: BuilderStep,
408 _step_index: usize,
409 _ctx: &CompilationContext,
410 _registry: &StepCompilerRegistry,
411 ) -> Result<CompileOutcome, CamelError> {
412 match step {
413 BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
414 processor: op.0,
415 body_contract: None,
416 lifecycle: Some(self.handle.clone()),
417 })),
418 other => Ok(CompileOutcome::NotHandled(other)),
419 }
420 }
421 }
422
423 #[tokio::test]
424 async fn compile_children_segments_bubbles_child_lifecycle() {
425 use std::collections::HashMap;
426 use std::sync::Mutex;
427
428 use camel_api::{
429 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
430 };
431 use camel_bean::BeanRegistry;
432 use camel_component_api::{
433 ComponentContext, NoOpComponentContext, RuntimeObservability,
434 test_support::NoopRuntimeObservability,
435 };
436
437 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
438
439 let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
440
441 let mut reg = StepCompilerRegistry::new();
444 reg.register(Box::new(LifecycleInjectorCompiler {
445 handle: handle.clone(),
446 }));
447 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
448
449 let pc = ProducerContext::default();
450 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
451 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
452 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
453 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
454 let staging = FunctionStagingMode::DirectAdd;
455 let idempotent_repositories = crate::IdempotentRegistry::new();
456 let claim_check_repositories = crate::ClaimCheckRegistry::new();
457
458 let ctx = CompilationContext {
459 producer_ctx: &pc,
460 rt,
461 languages: &languages,
462 beans: &beans,
463 function_invoker: None,
464 component_ctx,
465 route_id: None,
466 staging_mode: &staging,
467 idempotent_repositories: &idempotent_repositories,
468 claim_check_repositories: &claim_check_repositories,
469 };
470
471 let filter_step = BuilderStep::Filter {
473 predicate: FilterPredicate::new(|_| true),
474 steps: vec![BuilderStep::Processor(OpaqueProcessor(
475 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
476 ))],
477 };
478
479 let result = reg.compile_step(filter_step, 0, &ctx);
480 let compiled = result
481 .expect("compilation should succeed")
482 .expect("should match");
483
484 match compiled {
485 CompiledStep::Segment {
486 lifecycle,
487 body_contract,
488 ..
489 } => {
490 assert_eq!(body_contract, None, "body_contract should be None");
491 let handles = lifecycle.expect("Segment should have lifecycle handles");
492 assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
493 assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
494 }
495 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
496 }
497 }
498
499 #[derive(Debug)]
501 struct NamedLifecycle(&'static str);
502
503 #[async_trait::async_trait]
504 impl camel_api::StepLifecycle for NamedLifecycle {
505 fn name(&self) -> &'static str {
506 self.0
507 }
508 async fn shutdown(
509 &self,
510 _reason: camel_api::StepShutdownReason,
511 ) -> Result<(), camel_api::CamelError> {
512 Ok(())
513 }
514 }
515
516 #[tokio::test]
518 async fn compile_children_segments_multiple_stateful_children() {
519 use std::collections::HashMap;
520 use std::sync::Mutex;
521
522 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
523 use camel_api::{
524 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
525 };
526 use camel_bean::BeanRegistry;
527 use camel_component_api::{
528 ComponentContext, NoOpComponentContext, RuntimeObservability,
529 test_support::NoopRuntimeObservability,
530 };
531
532 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
533
534 let mut reg = StepCompilerRegistry::new();
535 reg.register(Box::new(LifecycleInjectorCompiler {
536 handle: handle.clone(),
537 }));
538 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
539
540 let pc = ProducerContext::default();
541 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
542 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
543 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
544 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
545 let staging = FunctionStagingMode::DirectAdd;
546 let idempotent_repositories = crate::IdempotentRegistry::new();
547 let claim_check_repositories = crate::ClaimCheckRegistry::new();
548
549 let ctx = CompilationContext {
550 producer_ctx: &pc,
551 rt,
552 languages: &languages,
553 beans: &beans,
554 function_invoker: None,
555 component_ctx,
556 route_id: None,
557 staging_mode: &staging,
558 idempotent_repositories: &idempotent_repositories,
559 claim_check_repositories: &claim_check_repositories,
560 };
561
562 let filter_step = BuilderStep::Filter {
564 predicate: FilterPredicate::new(|_| true),
565 steps: vec![
566 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
567 Box::pin(async move { Ok(ex) })
568 }))),
569 BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
570 Box::pin(async move { Ok(ex) })
571 }))),
572 ],
573 };
574
575 let result = reg.compile_step(filter_step, 0, &ctx);
576 let compiled = result
577 .expect("compilation should succeed")
578 .expect("should match");
579
580 match compiled {
581 CompiledStep::Segment { lifecycle, .. } => {
582 let handles = lifecycle.expect("Segment should have lifecycle handles");
583 assert_eq!(
584 handles.len(),
585 2,
586 "expected 2 lifecycle handles for 2 children"
587 );
588 for h in &handles {
589 assert_eq!(h.name(), "multi", "all handles should be 'multi'");
590 }
591 }
592 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
593 }
594 }
595
596 #[tokio::test]
598 async fn compile_children_segments_multi_branch_accumulation() {
599 use std::collections::HashMap;
600 use std::sync::Mutex;
601
602 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
603 use crate::lifecycle::application::route_definition::WhenStep;
604 use camel_api::{
605 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
606 };
607 use camel_bean::BeanRegistry;
608 use camel_component_api::{
609 ComponentContext, NoOpComponentContext, RuntimeObservability,
610 test_support::NoopRuntimeObservability,
611 };
612
613 let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
614
615 let mut reg = StepCompilerRegistry::new();
616 reg.register(Box::new(LifecycleInjectorCompiler {
617 handle: handle.clone(),
618 }));
619 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
620
621 let pc = ProducerContext::default();
622 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
623 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
624 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
625 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
626 let staging = FunctionStagingMode::DirectAdd;
627 let idempotent_repositories = crate::IdempotentRegistry::new();
628 let claim_check_repositories = crate::ClaimCheckRegistry::new();
629
630 let ctx = CompilationContext {
631 producer_ctx: &pc,
632 rt,
633 languages: &languages,
634 beans: &beans,
635 function_invoker: None,
636 component_ctx,
637 route_id: None,
638 staging_mode: &staging,
639 idempotent_repositories: &idempotent_repositories,
640 claim_check_repositories: &claim_check_repositories,
641 };
642
643 let choice_step = BuilderStep::Choice {
645 whens: vec![
646 WhenStep {
647 predicate: FilterPredicate::new(|_| true),
648 steps: vec![BuilderStep::Processor(OpaqueProcessor(
649 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
650 ))],
651 },
652 WhenStep {
653 predicate: FilterPredicate::new(|_| false),
654 steps: vec![BuilderStep::Processor(OpaqueProcessor(
655 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
656 ))],
657 },
658 ],
659 otherwise: None,
660 };
661
662 let result = reg.compile_step(choice_step, 0, &ctx);
663 let compiled = result
664 .expect("compilation should succeed")
665 .expect("should match");
666
667 match compiled {
668 CompiledStep::Segment { lifecycle, .. } => {
669 let handles = lifecycle.expect("Segment should have lifecycle handles");
670 assert_eq!(
671 handles.len(),
672 2,
673 "expected 2 lifecycle handles from 2 branches"
674 );
675 for h in &handles {
676 assert_eq!(h.name(), "branch", "all handles should be 'branch'");
677 }
678 }
679 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
680 }
681 }
682
683 #[tokio::test]
686 async fn compile_children_segments_nested_segment_flattening() {
687 use std::collections::HashMap;
688 use std::sync::Mutex;
689
690 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
691 use camel_api::{
692 BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
693 };
694 use camel_bean::BeanRegistry;
695 use camel_component_api::{
696 ComponentContext, NoOpComponentContext, RuntimeObservability,
697 test_support::NoopRuntimeObservability,
698 };
699
700 let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
701
702 let mut reg = StepCompilerRegistry::new();
703 reg.register(Box::new(LifecycleInjectorCompiler {
704 handle: inner_handle.clone(),
705 }));
706 reg.register(Box::new(super::control_flow::ControlFlowCompiler));
707
708 let pc = ProducerContext::default();
709 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
710 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
711 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
712 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
713 let staging = FunctionStagingMode::DirectAdd;
714 let idempotent_repositories = crate::IdempotentRegistry::new();
715 let claim_check_repositories = crate::ClaimCheckRegistry::new();
716
717 let ctx = CompilationContext {
718 producer_ctx: &pc,
719 rt,
720 languages: &languages,
721 beans: &beans,
722 function_invoker: None,
723 component_ctx,
724 route_id: None,
725 staging_mode: &staging,
726 idempotent_repositories: &idempotent_repositories,
727 claim_check_repositories: &claim_check_repositories,
728 };
729
730 let inner_filter = BuilderStep::Filter {
734 predicate: FilterPredicate::new(|_| true),
735 steps: vec![BuilderStep::Processor(OpaqueProcessor(
736 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
737 ))],
738 };
739
740 let outer_filter = BuilderStep::Filter {
741 predicate: FilterPredicate::new(|_| true),
742 steps: vec![inner_filter],
743 };
744
745 let result = reg.compile_step(outer_filter, 0, &ctx);
746 let compiled = result
747 .expect("compilation should succeed")
748 .expect("should match");
749
750 match compiled {
751 CompiledStep::Segment { lifecycle, .. } => {
752 let handles = lifecycle.expect("outer Segment should have lifecycle handles");
753 assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
754 assert_eq!(
755 handles[0].name(),
756 "deep",
757 "handle should be from innermost child"
758 );
759 }
760 other => panic!("Expected CompiledStep::Segment, got {other:?}"),
761 }
762 }
763}
764
765#[cfg(test)]
766mod dispatch_tests {
767 use super::*;
768 use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
769 use camel_api::{BoxProcessor, BoxProcessorExt};
770 use camel_bean::BeanRegistry;
771 use camel_component_api::{
772 ComponentContext, NoOpComponentContext, RuntimeObservability,
773 test_support::NoopRuntimeObservability,
774 };
775 use std::collections::HashMap;
776 use std::sync::Mutex;
777
778 struct ToStopCompiler;
780
781 impl StepCompiler for ToStopCompiler {
782 fn compile(
783 &self,
784 step: BuilderStep,
785 _step_index: usize,
786 _ctx: &CompilationContext,
787 _registry: &StepCompilerRegistry,
788 ) -> Result<CompileOutcome, CamelError> {
789 match step {
790 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
791 other => Ok(CompileOutcome::NotHandled(other)),
792 }
793 }
794 }
795
796 struct ToProcessCompiler;
798
799 impl StepCompiler for ToProcessCompiler {
800 fn compile(
801 &self,
802 step: BuilderStep,
803 _step_index: usize,
804 _ctx: &CompilationContext,
805 _registry: &StepCompilerRegistry,
806 ) -> Result<CompileOutcome, CamelError> {
807 match step {
808 BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
809 processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
810 body_contract: None,
811 lifecycle: None,
812 })),
813 other => Ok(CompileOutcome::NotHandled(other)),
814 }
815 }
816 }
817
818 struct PassThroughCompiler;
821
822 impl StepCompiler for PassThroughCompiler {
823 fn compile(
824 &self,
825 step: BuilderStep,
826 _step_index: usize,
827 _ctx: &CompilationContext,
828 _registry: &StepCompilerRegistry,
829 ) -> Result<CompileOutcome, CamelError> {
830 Ok(CompileOutcome::NotHandled(step))
831 }
832 }
833
834 #[allow(clippy::too_many_arguments)]
836 fn ctx<'a>(
837 pc: &'a ProducerContext,
838 rt: Arc<dyn RuntimeObservability>,
839 languages: &'a SharedLanguageRegistry,
840 beans: &'a Arc<Mutex<BeanRegistry>>,
841 component_ctx: Arc<dyn ComponentContext>,
842 staging: &'a FunctionStagingMode,
843 idempotent_repositories: &'a crate::IdempotentRegistry,
844 claim_check_repositories: &'a crate::ClaimCheckRegistry,
845 ) -> CompilationContext<'a> {
846 CompilationContext {
847 producer_ctx: pc,
848 rt,
849 languages,
850 beans,
851 function_invoker: None,
852 component_ctx,
853 route_id: None,
854 staging_mode: staging,
855 idempotent_repositories,
856 claim_check_repositories,
857 }
858 }
859
860 #[test]
863 fn compile_step_preserves_registry_order() {
864 let pc = ProducerContext::default();
865 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
866 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
867 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
868 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
869 let staging = FunctionStagingMode::DirectAdd;
870 let idempotent_repositories = crate::IdempotentRegistry::new();
871 let claim_check_repositories = crate::ClaimCheckRegistry::new();
872
873 let context = ctx(
874 &pc,
875 rt,
876 &languages,
877 &beans,
878 component_ctx,
879 &staging,
880 &idempotent_repositories,
881 &claim_check_repositories,
882 );
883
884 let mut reg = StepCompilerRegistry::new();
887 reg.register(Box::new(ToStopCompiler));
888 reg.register(Box::new(ToProcessCompiler));
889
890 let result = reg
891 .compile_step(BuilderStep::To("test".into()), 0, &context)
892 .expect("compilation should succeed")
893 .expect("should match");
894
895 assert!(
896 matches!(result, CompiledStep::Stop),
897 "expected ToStopCompiler (first registered) to win, got {result:?}"
898 );
899 }
900
901 #[test]
904 fn compile_step_unhandled_returns_ok_none() {
905 let pc = ProducerContext::default();
906 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
907 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
908 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
909 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
910 let staging = FunctionStagingMode::DirectAdd;
911 let idempotent_repositories = crate::IdempotentRegistry::new();
912 let claim_check_repositories = crate::ClaimCheckRegistry::new();
913
914 let context = ctx(
915 &pc,
916 rt,
917 &languages,
918 &beans,
919 component_ctx,
920 &staging,
921 &idempotent_repositories,
922 &claim_check_repositories,
923 );
924
925 let mut reg = StepCompilerRegistry::new();
927 reg.register(Box::new(ToStopCompiler));
928
929 let result = reg.compile_step(
931 BuilderStep::Log {
932 level: camel_processor::LogLevel::Info,
933 message: "unhandled".into(),
934 },
935 0,
936 &context,
937 );
938
939 assert!(
940 matches!(result, Ok(None)),
941 "expected Ok(None), got {result:?}"
942 );
943 }
944
945 #[test]
949 fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
950 let pc = ProducerContext::default();
951 let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
952 let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
953 let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
954 let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
955 let staging = FunctionStagingMode::DirectAdd;
956 let idempotent_repositories = crate::IdempotentRegistry::new();
957 let claim_check_repositories = crate::ClaimCheckRegistry::new();
958
959 let context = ctx(
960 &pc,
961 rt,
962 &languages,
963 &beans,
964 component_ctx,
965 &staging,
966 &idempotent_repositories,
967 &claim_check_repositories,
968 );
969
970 let mut reg = StepCompilerRegistry::new();
973 reg.register(Box::new(PassThroughCompiler));
974 reg.register(Box::new(ToStopCompiler));
975
976 let result = reg
977 .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
978 .expect("compilation should succeed")
979 .expect("should match");
980
981 assert!(
982 matches!(result, CompiledStep::Stop),
983 "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
984 );
985 }
986}