Skip to main content

camel_core/lifecycle/adapters/step_compilers/
mod.rs

1//! StepCompiler registry pattern — extract from step_resolution.rs
2//!
3//! Each compiler group is responsible for matching specific `BuilderStep` variants.
4//! The registry dispatches each step to compilers in registration order; the first
5//! compiler that returns `Matched` wins. Compilers that don't handle a variant
6//! return `NotHandled(step)` to pass it to the next compiler.
7
8use 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/// A compiled pipeline step.
31///
32/// `Process` is the normal case: a boxed processor plus its optional body
33/// contract. `Stop` (added in Task 3b) is the Stop EIP marker — `run_steps`
34/// recognises it and produces `PipelineOutcome::Stopped` without invoking a
35/// Tower service. `Segment` (added in Task 3) wraps an `OutcomeSegment` for
36/// structural EIPs with outcome-aware sub-pipelines.
37///
38/// **Boundary:** `CompiledStep` is the compile-time representation. At runtime,
39/// `run_steps` consumes a `Vec<CompiledStep>` (Stop variants included) and
40/// produces a `PipelineOutcome`; the wrapping `Service<Exchange>` impl
41/// translates `PipelineOutcome` back to `Result<Exchange, CamelError>`. See
42/// ADR-0024.
43#[derive(Debug, Clone)]
44pub enum CompiledStep {
45    Process {
46        processor: BoxProcessor,
47        body_contract: Option<BodyType>,
48        /// Lifecycle handle for this processor, if it is stateful.
49        /// `None` for stateless processors (the common case).
50        lifecycle: Option<Arc<dyn StepLifecycle>>,
51    },
52    /// Stop EIP marker. `run_steps` produces `PipelineOutcome::Stopped(ex)`
53    /// without invoking a Tower service. Replaces `StopService` (Task 7).
54    Stop,
55    /// Outcome-aware structural EIP segment. `run_steps` invokes
56    /// `segment.run(ex)` and matches on the returned `PipelineOutcome`.
57    /// See ADR-0025.
58    Segment {
59        segment: camel_api::OutcomeSegment,
60        body_contract: Option<BodyType>,
61        /// Lifecycle handles from children nested inside this segment.
62        /// `Option<Vec<...>>` (not `Option<Arc<...>>`) so multiple stateful
63        /// children (e.g. Idempotent+Resequencer inside Filter) each
64        /// register independently.
65        lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
66    },
67}
68
69/// Result from a compiler: either it handled the step, or it did not recognize
70/// the variant and returns the step for the next compiler.
71///
72/// `Result` is intentionally outside this enum (returned by `StepCompiler::compile`)
73/// so that compiler arms can use the `?` operator for propagated errors instead of
74/// wrapping every `Err` in `Matched(Err(e))`.
75pub(crate) enum CompileOutcome {
76    Matched(CompiledStep),
77    NotHandled(BuilderStep),
78}
79
80/// A compiler that can handle one or more `BuilderStep` variants.
81///
82/// The `compile` method receives ownership of the step. If the compiler recognizes
83/// the variant it returns `Ok(CompileOutcome::Matched(...))`. Otherwise it returns
84/// `Ok(CompileOutcome::NotHandled(step))` to pass the step to the next compiler.
85/// Compilation errors are returned as `Err(CamelError)`.
86pub(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
96/// Shared context passed to every compiler invocation.
97pub(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    /// Idempotent repository registry. Used by the `IdempotentConsumer`
107    /// compiler arm to resolve repository names into `Arc<dyn IdempotentRepository>`.
108    pub idempotent_repositories: &'a IdempotentRegistry,
109    /// Claim check repository registry. Used by the `ClaimCheck` compiler arm
110    /// to resolve repository names into `Arc<dyn ClaimCheckRepository>`.
111    pub claim_check_repositories: &'a ClaimCheckRegistry,
112}
113
114impl<'a> CompilationContext<'a> {
115    /// Recursively compile child steps. Used by compilers that have sub-pipelines
116    /// (Filter, Choice, Split, Loop, etc.).
117    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    /// Recursively compile child steps and map them into outcome-aware segments.
126    ///
127    /// Each `CompiledStep` variant is converted to a `Box<dyn OutcomePipeline>`:
128    /// - `Process` → `BoxProcessorSegment`, optionally wrapped in `BodyCoercingSegment`
129    /// - `Stop` → `StopSegment` (produces `PipelineOutcome::Stopped(ex)`)
130    /// - `Segment` → its inner `OutcomeSegment` (which now implements OutcomePipeline)
131    ///
132    /// This replaces the 22-line duplicated closure in Filter/DeclarativeFilter
133    /// (and will prevent 14+ more duplicates in T9–T16).
134    #[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
193/// Registry of step compilers. Steps are dispatched to compilers in registration
194/// order. The first matching compiler handles the step.
195pub(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    /// Try each compiler in order. The first to return `Matched` wins.
211    /// If all return `NotHandled`, returns `Ok(None)`. Compilation errors
212    /// short-circuit and return `Err(CamelError)`.
213    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    /// Compile all steps in a vector.
230    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
250/// Parse a URI and create a producer, reusing `component_ctx`, `rt`, and `producer_ctx`
251/// from the compilation context.
252pub(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
265/// Pack a lifecycle Vec into `None` when empty, `Some` when non-empty.
266/// Preserves the invariant that `Some` always implies ≥1 handle.
267pub(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
277/// Build the full registry with all compiler groups.
278pub(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            // ok
323        } 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    /// Test lifecycle handle used by compile_children_segments_bubbles_child_lifecycle.
382    #[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    /// Custom compiler that injects a lifecycle handle into every
399    /// `BuilderStep::Processor` it compiles.
400    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        // Register lifecycle injector + real control-flow compiler so
442        // compile_children_segments runs through a structural EIP path.
443        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        // Compile a Filter with a child Processor step.
472        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    /// A lifecycle handle with a configurable name for multi-handle tests.
500    #[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    /// Test A: Multiple stateful children in one Segment → Vec length 2.
517    #[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        // Filter with TWO child Processors → both get the same lifecycle handle.
563        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    /// Test B: Multi-branch accumulation across Choice when-clauses.
597    #[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        // Choice with 2 when branches, each containing 1 stateful child.
644        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    /// Test C: Nested Segment-in-Segment flattening — outer Segment contains
684    /// innermost lifecycle handle from a grandchild Processor.
685    #[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        // Outer Filter containing an inner Filter that has a stateful Processor.
731        // The outer Segment's lifecycle should contain the innermost handle
732        // (proves recursive flattening through compile_children_segments).
733        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    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Stop`.
779    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    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Process`.
797    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    /// Compiler that passes all steps through (never handles any variant).
819    /// Returns `NotHandled(step)` for every input, proving by-value pass-through.
820    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    /// Shared context builder — avoids repeating the 15-line setup in every test.
835    #[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 that the dispatcher respects registration order: the first matching
861    /// compiler wins even when a later compiler also matches.
862    #[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        // Register ToStopCompiler FIRST, ToProcessCompiler SECOND.
885        // Both match BuilderStep::To. The first-registered should win.
886        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 that when no compiler handles a variant, the dispatcher returns
902    /// `Ok(None)` rather than an error.
903    #[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        // Register a compiler that only handles BuilderStep::To.
926        let mut reg = StepCompilerRegistry::new();
927        reg.register(Box::new(ToStopCompiler));
928
929        // Send a BuilderStep::Log variant — ToStopCompiler does not match it.
930        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 that `NotHandled(step)` passes the step by-value to the next
946    /// compiler. Compiler N returns `NotHandled(step)` for a variant that
947    /// compiler N+1 handles — proving the step is not dropped or replaced.
948    #[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        // Register PassThroughCompiler FIRST (never handles anything),
971        // ToStopCompiler SECOND (handles BuilderStep::To).
972        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}