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::intercept::InterceptRules;
17use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
18use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
19use crate::lifecycle::application::route_definition::BuilderStep;
20use crate::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
21use camel_bean::BeanRegistry;
22
23mod control_flow;
24mod core;
25mod endpoints;
26mod error_handling;
27mod routing;
28mod splitting;
29mod transforms;
30
31/// A compiled pipeline step.
32///
33/// `Process` is the normal case: a boxed processor plus its optional body
34/// contract. `Stop` (added in Task 3b) is the Stop EIP marker — `run_steps`
35/// recognises it and produces `PipelineOutcome::Stopped` without invoking a
36/// Tower service. `Segment` (added in Task 3) wraps an `OutcomeSegment` for
37/// structural EIPs with outcome-aware sub-pipelines.
38///
39/// **Boundary:** `CompiledStep` is the compile-time representation. At runtime,
40/// `run_steps` consumes a `Vec<CompiledStep>` (Stop variants included) and
41/// produces a `PipelineOutcome`; the wrapping `Service<Exchange>` impl
42/// translates `PipelineOutcome` back to `Result<Exchange, CamelError>`. See
43/// ADR-0024.
44#[derive(Debug, Clone)]
45pub enum CompiledStep {
46    Process {
47        processor: BoxProcessor,
48        body_contract: Option<BodyType>,
49        /// Lifecycle handle for this processor, if it is stateful.
50        /// `None` for stateless processors (the common case).
51        lifecycle: Option<Arc<dyn StepLifecycle>>,
52    },
53    /// Stop EIP marker. `run_steps` produces `PipelineOutcome::Stopped(ex)`
54    /// without invoking a Tower service. Replaces `StopService` (Task 7).
55    Stop,
56    /// Outcome-aware structural EIP segment. `run_steps` invokes
57    /// `segment.run(ex)` and matches on the returned `PipelineOutcome`.
58    /// See ADR-0025.
59    Segment {
60        segment: camel_api::OutcomeSegment,
61        body_contract: Option<BodyType>,
62        /// Lifecycle handles from children nested inside this segment.
63        /// `Option<Vec<...>>` (not `Option<Arc<...>>`) so multiple stateful
64        /// children (e.g. Idempotent+Resequencer inside Filter) each
65        /// register independently.
66        lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
67    },
68}
69
70/// Result from a compiler: either it handled the step, or it did not recognize
71/// the variant and returns the step for the next compiler.
72///
73/// `Result` is intentionally outside this enum (returned by `StepCompiler::compile`)
74/// so that compiler arms can use the `?` operator for propagated errors instead of
75/// wrapping every `Err` in `Matched(Err(e))`.
76pub(crate) enum CompileOutcome {
77    Matched(CompiledStep),
78    NotHandled(BuilderStep),
79}
80
81/// A compiler that can handle one or more `BuilderStep` variants.
82///
83/// The `compile` method receives ownership of the step. If the compiler recognizes
84/// the variant it returns `Ok(CompileOutcome::Matched(...))`. Otherwise it returns
85/// `Ok(CompileOutcome::NotHandled(step))` to pass the step to the next compiler.
86/// Compilation errors are returned as `Err(CamelError)`.
87pub(crate) trait StepCompiler: Send + Sync {
88    fn compile(
89        &self,
90        step: BuilderStep,
91        step_index: usize,
92        ctx: &CompilationContext,
93        registry: &StepCompilerRegistry,
94    ) -> Result<CompileOutcome, CamelError>;
95}
96
97/// Shared context passed to every compiler invocation.
98pub(crate) struct CompilationContext<'a> {
99    pub producer_ctx: &'a ProducerContext,
100    pub rt: Arc<dyn RuntimeObservability>,
101    pub languages: &'a SharedLanguageRegistry,
102    pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
103    pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
104    pub component_ctx: Arc<dyn ComponentContext>,
105    pub route_id: Option<&'a str>,
106    pub staging_mode: &'a FunctionStagingMode,
107    /// Idempotent repository registry. Used by the `IdempotentConsumer`
108    /// compiler arm to resolve repository names into `Arc<dyn IdempotentRepository>`.
109    pub idempotent_repositories: &'a IdempotentRegistry,
110    /// Claim check repository registry. Used by the `ClaimCheck` compiler arm
111    /// to resolve repository names into `Arc<dyn ClaimCheckRepository>`.
112    pub claim_check_repositories: &'a ClaimCheckRegistry,
113    /// Cache repository registry. Used by the `Cache`, `CacheInvalidate`, and
114    /// `CachePeekStale` compiler arms to resolve repository names into
115    /// `Arc<dyn CacheRepository>`.
116    pub cache_repositories: &'a CacheRegistry,
117    /// Route send-point interception rules captured at compile time.
118    /// Consulted by send-step compilation (Task 4/5).
119    pub intercept: InterceptRules,
120}
121
122impl<'a> CompilationContext<'a> {
123    /// Recursively compile child steps. Used by compilers that have sub-pipelines
124    /// (Filter, Choice, Split, Loop, etc.).
125    pub fn compile_children(
126        &self,
127        steps: Vec<BuilderStep>,
128        registry: &StepCompilerRegistry,
129    ) -> Result<Vec<CompiledStep>, CamelError> {
130        registry.compile_steps(steps, self)
131    }
132
133    /// Recursively compile child steps and map them into outcome-aware segments.
134    ///
135    /// Each `CompiledStep` variant is converted to a `Box<dyn OutcomePipeline>`:
136    /// - `Process` → `BoxProcessorSegment`, optionally wrapped in `BodyCoercingSegment`
137    /// - `Stop` → `StopSegment` (produces `PipelineOutcome::Stopped(ex)`)
138    /// - `Segment` → its inner `OutcomeSegment` (which now implements OutcomePipeline)
139    ///
140    /// This replaces the 22-line duplicated closure in Filter/DeclarativeFilter
141    /// (and will prevent 14+ more duplicates in T9–T16).
142    #[allow(clippy::type_complexity)]
143    pub fn compile_children_segments(
144        &self,
145        steps: Vec<BuilderStep>,
146        registry: &StepCompilerRegistry,
147    ) -> Result<
148        (
149            Vec<Box<dyn camel_api::OutcomePipeline>>,
150            Vec<Arc<dyn camel_api::StepLifecycle>>,
151        ),
152        CamelError,
153    > {
154        let pairs = self.compile_children(steps, registry)?;
155        let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
156        let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
157            .into_iter()
158            .map(|c| match c {
159                CompiledStep::Process {
160                    processor,
161                    body_contract,
162                    lifecycle,
163                } => {
164                    if let Some(lc) = lifecycle {
165                        lifecycle_handles.push(lc);
166                    }
167                    let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
168                        crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
169                            processor,
170                        ),
171                    );
172                    match body_contract {
173                        Some(contract) => Box::new(
174                            crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
175                                inner, contract,
176                            ),
177                        ),
178                        None => inner,
179                    }
180                }
181                CompiledStep::Stop => {
182                    Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
183                        as Box<dyn camel_api::OutcomePipeline>
184                }
185                CompiledStep::Segment {
186                    segment,
187                    body_contract: _,
188                    lifecycle,
189                } => {
190                    if let Some(lcs) = lifecycle {
191                        lifecycle_handles.extend(lcs);
192                    }
193                    Box::new(segment)
194                }
195            })
196            .collect();
197        Ok((segments, lifecycle_handles))
198    }
199}
200
201/// Registry of step compilers. Steps are dispatched to compilers in registration
202/// order. The first matching compiler handles the step.
203pub(crate) struct StepCompilerRegistry {
204    compilers: Vec<Box<dyn StepCompiler>>,
205}
206
207impl StepCompilerRegistry {
208    pub fn new() -> Self {
209        Self {
210            compilers: Vec::new(),
211        }
212    }
213
214    pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
215        self.compilers.push(compiler);
216    }
217
218    /// Try each compiler in order. The first to return `Matched` wins.
219    /// If all return `NotHandled`, returns `Ok(None)`. Compilation errors
220    /// short-circuit and return `Err(CamelError)`.
221    pub fn compile_step(
222        &self,
223        step: BuilderStep,
224        step_index: usize,
225        ctx: &CompilationContext,
226    ) -> Result<Option<CompiledStep>, CamelError> {
227        let mut step = step;
228        for compiler in &self.compilers {
229            match compiler.compile(step, step_index, ctx, self)? {
230                CompileOutcome::Matched(s) => return Ok(Some(s)),
231                CompileOutcome::NotHandled(s) => step = s,
232            }
233        }
234        Ok(None)
235    }
236
237    /// Compile all steps in a vector.
238    pub fn compile_steps(
239        &self,
240        steps: Vec<BuilderStep>,
241        ctx: &CompilationContext,
242    ) -> Result<Vec<CompiledStep>, CamelError> {
243        let mut out = Vec::with_capacity(steps.len());
244        for (i, step) in steps.into_iter().enumerate() {
245            match self.compile_step(step, i, ctx)? {
246                Some(c) => out.push(c),
247                None => {
248                    return Err(CamelError::RouteError(
249                        "no compiler registered for step variant".into(),
250                    ));
251                }
252            }
253        }
254        Ok(out)
255    }
256}
257
258/// A resolved send target: the pieces the un-intercepted `To` path produces
259/// for one URI.
260pub(super) struct ResolvedSend {
261    pub producer: BoxProcessor,
262    pub body_contract: Option<BodyType>,
263    pub lifecycle: Option<Arc<dyn StepLifecycle>>,
264}
265
266/// Resolve a send URI into producer/body-contract/lifecycle, exactly as the
267/// un-intercepted `To` path does. Canonical resolution path for send steps;
268/// [`resolve_producer_with_lifecycle`] and [`resolve_producer`] are thin
269/// wrappers over this.
270pub(super) fn resolve_send(
271    ctx: &CompilationContext,
272    uri: &str,
273) -> Result<ResolvedSend, CamelError> {
274    let parsed = parse_uri(uri)?;
275    let component = ctx
276        .component_ctx
277        .resolve_component(&parsed.scheme)
278        .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
279    let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
280    let body_contract = endpoint.body_contract();
281    let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
282    // Capture the endpoint's lifecycle handle so the route controller can
283    // start/shut it down in route order (ADR-0022). Default is `None` for
284    // stateless endpoints — see `Endpoint::lifecycle` in camel-component-api.
285    let lifecycle: Option<Arc<dyn StepLifecycle>> = endpoint.lifecycle();
286    Ok(ResolvedSend {
287        producer,
288        body_contract,
289        lifecycle,
290    })
291}
292
293/// Parse a URI and create a producer, also returning the endpoint's
294/// [`StepLifecycle`] handle (if any). Thin wrapper over [`resolve_send`] that
295/// discards the body contract. Use directly when the lifecycle is needed
296/// (e.g. `WireTap` propagation).
297pub(crate) fn resolve_producer_with_lifecycle(
298    ctx: &CompilationContext,
299    uri: &str,
300) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
301    let resolved = resolve_send(ctx, uri)?;
302    Ok((resolved.producer, resolved.lifecycle))
303}
304
305/// Parse a URI and create a producer, reusing `component_ctx`, `rt`, and `producer_ctx`
306/// from the compilation context.
307///
308/// Thin wrapper over [`resolve_producer_with_lifecycle`] that discards the
309/// endpoint's [`StepLifecycle`] handle.
310pub(crate) fn resolve_producer(
311    ctx: &CompilationContext,
312    uri: &str,
313) -> Result<BoxProcessor, CamelError> {
314    Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
315}
316
317/// Pack a lifecycle Vec into `None` when empty, `Some` when non-empty.
318/// Preserves the invariant that `Some` always implies ≥1 handle.
319pub(super) fn pack_lifecycles(
320    lifecycles: Vec<Arc<dyn StepLifecycle>>,
321) -> Option<Vec<Arc<dyn StepLifecycle>>> {
322    if lifecycles.is_empty() {
323        None
324    } else {
325        Some(lifecycles)
326    }
327}
328
329/// Build the full registry with all compiler groups.
330pub(crate) fn build_registry() -> StepCompilerRegistry {
331    let mut reg = StepCompilerRegistry::new();
332    reg.register(Box::new(core::CoreCompiler));
333    reg.register(Box::new(endpoints::EndpointsCompiler));
334    reg.register(Box::new(transforms::TransformsCompiler));
335    reg.register(Box::new(routing::RoutingCompiler));
336    reg.register(Box::new(control_flow::ControlFlowCompiler));
337    reg.register(Box::new(splitting::SplittingCompiler));
338    reg.register(Box::new(error_handling::ErrorHandlingCompiler));
339    reg
340}
341
342#[cfg(test)]
343mod segment_tests {
344    use super::*;
345    use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
346    use std::future::Future;
347    use std::pin::Pin;
348
349    #[derive(Clone)]
350    struct EchoSegment;
351
352    impl OutcomePipeline for EchoSegment {
353        fn clone_box(&self) -> Box<dyn OutcomePipeline> {
354            Box::new(EchoSegment)
355        }
356        fn run<'a>(
357            &'a mut self,
358            exchange: Exchange,
359        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
360            Box::pin(async move { PipelineOutcome::Completed(exchange) })
361        }
362    }
363
364    #[test]
365    fn compiled_step_segment_clone_compiles() {
366        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
367        let step = CompiledStep::Segment {
368            segment: seg,
369            body_contract: None,
370            lifecycle: None,
371        };
372        let _cloned = step.clone();
373        if let CompiledStep::Segment { .. } = _cloned {
374            // ok
375        } else {
376            panic!("clone should preserve variant");
377        }
378    }
379
380    #[test]
381    fn compiled_step_segment_debug_renders() {
382        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
383        let step = CompiledStep::Segment {
384            segment: seg,
385            body_contract: None,
386            lifecycle: None,
387        };
388        let s = format!("{:?}", step);
389        assert!(
390            s.contains("Segment"),
391            "debug should mention Segment variant: {s}"
392        );
393    }
394
395    #[test]
396    fn outcome_segment_satisfies_clone_send_static() {
397        fn assert_traits<T: Clone + Send + 'static>() {}
398        assert_traits::<camel_api::OutcomeSegment>();
399    }
400
401    #[tokio::test]
402    #[allow(clippy::arc_with_non_send_sync)]
403    async fn outcome_segment_survives_arcswap_swap() {
404        use arc_swap::ArcSwap;
405        use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
406        use std::sync::Arc;
407
408        #[derive(Clone)]
409        struct EchoSegment;
410        impl OutcomePipeline for EchoSegment {
411            fn clone_box(&self) -> Box<dyn OutcomePipeline> {
412                Box::new(EchoSegment)
413            }
414            fn run<'a>(
415                &'a mut self,
416                ex: Exchange,
417            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
418            {
419                Box::pin(async move { PipelineOutcome::Completed(ex) })
420            }
421        }
422
423        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
424        let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
425        slot.store(Arc::new(Some(seg.clone())));
426        slot.store(Arc::new(Some(seg)));
427
428        let mut borrowed = slot.load().as_ref().clone().unwrap();
429        let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
430        assert!(matches!(outcome, PipelineOutcome::Completed(_)));
431    }
432
433    /// Test lifecycle handle used by compile_children_segments_bubbles_child_lifecycle.
434    #[derive(Debug)]
435    struct TestLifecycle;
436
437    #[async_trait::async_trait]
438    impl camel_api::StepLifecycle for TestLifecycle {
439        fn name(&self) -> &'static str {
440            "test-lifecycle"
441        }
442        async fn shutdown(
443            &self,
444            _reason: camel_api::StepShutdownReason,
445        ) -> Result<(), camel_api::CamelError> {
446            Ok(())
447        }
448    }
449
450    /// Custom compiler that injects a lifecycle handle into every
451    /// `BuilderStep::Processor` it compiles.
452    struct LifecycleInjectorCompiler {
453        handle: Arc<dyn camel_api::StepLifecycle>,
454    }
455
456    impl StepCompiler for LifecycleInjectorCompiler {
457        fn compile(
458            &self,
459            step: BuilderStep,
460            _step_index: usize,
461            _ctx: &CompilationContext,
462            _registry: &StepCompilerRegistry,
463        ) -> Result<CompileOutcome, CamelError> {
464            match step {
465                BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
466                    processor: op.0,
467                    body_contract: None,
468                    lifecycle: Some(self.handle.clone()),
469                })),
470                other => Ok(CompileOutcome::NotHandled(other)),
471            }
472        }
473    }
474
475    #[tokio::test]
476    async fn compile_children_segments_bubbles_child_lifecycle() {
477        use std::collections::HashMap;
478        use std::sync::Mutex;
479
480        use camel_api::{
481            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
482        };
483        use camel_bean::BeanRegistry;
484        use camel_component_api::{
485            ComponentContext, NoOpComponentContext, RuntimeObservability,
486            test_support::NoopRuntimeObservability,
487        };
488
489        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
490
491        let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
492
493        // Register lifecycle injector + real control-flow compiler so
494        // compile_children_segments runs through a structural EIP path.
495        let mut reg = StepCompilerRegistry::new();
496        reg.register(Box::new(LifecycleInjectorCompiler {
497            handle: handle.clone(),
498        }));
499        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
500
501        let pc = ProducerContext::default();
502        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
503        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
504        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
505        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
506        let staging = FunctionStagingMode::DirectAdd;
507        let idempotent_repositories = crate::IdempotentRegistry::new();
508        let claim_check_repositories = crate::ClaimCheckRegistry::new();
509        let cache_repositories = crate::CacheRegistry::new();
510
511        let ctx = CompilationContext {
512            producer_ctx: &pc,
513            rt,
514            languages: &languages,
515            beans: &beans,
516            function_invoker: None,
517            component_ctx,
518            route_id: None,
519            staging_mode: &staging,
520            idempotent_repositories: &idempotent_repositories,
521            claim_check_repositories: &claim_check_repositories,
522            cache_repositories: &cache_repositories,
523            intercept: InterceptRules::default(),
524        };
525
526        // Compile a Filter with a child Processor step.
527        let filter_step = BuilderStep::Filter {
528            predicate: FilterPredicate::new(|_| true),
529            steps: vec![BuilderStep::Processor(OpaqueProcessor(
530                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
531            ))],
532        };
533
534        let result = reg.compile_step(filter_step, 0, &ctx);
535        let compiled = result
536            .expect("compilation should succeed")
537            .expect("should match");
538
539        match compiled {
540            CompiledStep::Segment {
541                lifecycle,
542                body_contract,
543                ..
544            } => {
545                assert_eq!(body_contract, None, "body_contract should be None");
546                let handles = lifecycle.expect("Segment should have lifecycle handles");
547                assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
548                assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
549            }
550            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
551        }
552    }
553
554    /// A lifecycle handle with a configurable name for multi-handle tests.
555    #[derive(Debug)]
556    struct NamedLifecycle(&'static str);
557
558    #[async_trait::async_trait]
559    impl camel_api::StepLifecycle for NamedLifecycle {
560        fn name(&self) -> &'static str {
561            self.0
562        }
563        async fn shutdown(
564            &self,
565            _reason: camel_api::StepShutdownReason,
566        ) -> Result<(), camel_api::CamelError> {
567            Ok(())
568        }
569    }
570
571    /// Test A: Multiple stateful children in one Segment → Vec length 2.
572    #[tokio::test]
573    async fn compile_children_segments_multiple_stateful_children() {
574        use std::collections::HashMap;
575        use std::sync::Mutex;
576
577        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
578        use camel_api::{
579            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
580        };
581        use camel_bean::BeanRegistry;
582        use camel_component_api::{
583            ComponentContext, NoOpComponentContext, RuntimeObservability,
584            test_support::NoopRuntimeObservability,
585        };
586
587        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
588
589        let mut reg = StepCompilerRegistry::new();
590        reg.register(Box::new(LifecycleInjectorCompiler {
591            handle: handle.clone(),
592        }));
593        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
594
595        let pc = ProducerContext::default();
596        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
597        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
598        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
599        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
600        let staging = FunctionStagingMode::DirectAdd;
601        let idempotent_repositories = crate::IdempotentRegistry::new();
602        let claim_check_repositories = crate::ClaimCheckRegistry::new();
603        let cache_repositories = crate::CacheRegistry::new();
604
605        let ctx = CompilationContext {
606            producer_ctx: &pc,
607            rt,
608            languages: &languages,
609            beans: &beans,
610            function_invoker: None,
611            component_ctx,
612            route_id: None,
613            staging_mode: &staging,
614            idempotent_repositories: &idempotent_repositories,
615            claim_check_repositories: &claim_check_repositories,
616            cache_repositories: &cache_repositories,
617            intercept: InterceptRules::default(),
618        };
619
620        // Filter with TWO child Processors → both get the same lifecycle handle.
621        let filter_step = BuilderStep::Filter {
622            predicate: FilterPredicate::new(|_| true),
623            steps: vec![
624                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
625                    Box::pin(async move { Ok(ex) })
626                }))),
627                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
628                    Box::pin(async move { Ok(ex) })
629                }))),
630            ],
631        };
632
633        let result = reg.compile_step(filter_step, 0, &ctx);
634        let compiled = result
635            .expect("compilation should succeed")
636            .expect("should match");
637
638        match compiled {
639            CompiledStep::Segment { lifecycle, .. } => {
640                let handles = lifecycle.expect("Segment should have lifecycle handles");
641                assert_eq!(
642                    handles.len(),
643                    2,
644                    "expected 2 lifecycle handles for 2 children"
645                );
646                for h in &handles {
647                    assert_eq!(h.name(), "multi", "all handles should be 'multi'");
648                }
649            }
650            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
651        }
652    }
653
654    /// Test B: Multi-branch accumulation across Choice when-clauses.
655    #[tokio::test]
656    async fn compile_children_segments_multi_branch_accumulation() {
657        use std::collections::HashMap;
658        use std::sync::Mutex;
659
660        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
661        use crate::lifecycle::application::route_definition::WhenStep;
662        use camel_api::{
663            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
664        };
665        use camel_bean::BeanRegistry;
666        use camel_component_api::{
667            ComponentContext, NoOpComponentContext, RuntimeObservability,
668            test_support::NoopRuntimeObservability,
669        };
670
671        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
672
673        let mut reg = StepCompilerRegistry::new();
674        reg.register(Box::new(LifecycleInjectorCompiler {
675            handle: handle.clone(),
676        }));
677        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
678
679        let pc = ProducerContext::default();
680        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
681        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
682        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
683        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
684        let staging = FunctionStagingMode::DirectAdd;
685        let idempotent_repositories = crate::IdempotentRegistry::new();
686        let claim_check_repositories = crate::ClaimCheckRegistry::new();
687        let cache_repositories = crate::CacheRegistry::new();
688
689        let ctx = CompilationContext {
690            producer_ctx: &pc,
691            rt,
692            languages: &languages,
693            beans: &beans,
694            function_invoker: None,
695            component_ctx,
696            route_id: None,
697            staging_mode: &staging,
698            idempotent_repositories: &idempotent_repositories,
699            claim_check_repositories: &claim_check_repositories,
700            cache_repositories: &cache_repositories,
701            intercept: InterceptRules::default(),
702        };
703
704        // Choice with 2 when branches, each containing 1 stateful child.
705        let choice_step = BuilderStep::Choice {
706            whens: vec![
707                WhenStep {
708                    predicate: FilterPredicate::new(|_| true),
709                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
710                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
711                    ))],
712                },
713                WhenStep {
714                    predicate: FilterPredicate::new(|_| false),
715                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
716                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
717                    ))],
718                },
719            ],
720            otherwise: None,
721        };
722
723        let result = reg.compile_step(choice_step, 0, &ctx);
724        let compiled = result
725            .expect("compilation should succeed")
726            .expect("should match");
727
728        match compiled {
729            CompiledStep::Segment { lifecycle, .. } => {
730                let handles = lifecycle.expect("Segment should have lifecycle handles");
731                assert_eq!(
732                    handles.len(),
733                    2,
734                    "expected 2 lifecycle handles from 2 branches"
735                );
736                for h in &handles {
737                    assert_eq!(h.name(), "branch", "all handles should be 'branch'");
738                }
739            }
740            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
741        }
742    }
743
744    /// Test C: Nested Segment-in-Segment flattening — outer Segment contains
745    /// innermost lifecycle handle from a grandchild Processor.
746    #[tokio::test]
747    async fn compile_children_segments_nested_segment_flattening() {
748        use std::collections::HashMap;
749        use std::sync::Mutex;
750
751        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
752        use camel_api::{
753            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
754        };
755        use camel_bean::BeanRegistry;
756        use camel_component_api::{
757            ComponentContext, NoOpComponentContext, RuntimeObservability,
758            test_support::NoopRuntimeObservability,
759        };
760
761        let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
762
763        let mut reg = StepCompilerRegistry::new();
764        reg.register(Box::new(LifecycleInjectorCompiler {
765            handle: inner_handle.clone(),
766        }));
767        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
768
769        let pc = ProducerContext::default();
770        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
771        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
772        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
773        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
774        let staging = FunctionStagingMode::DirectAdd;
775        let idempotent_repositories = crate::IdempotentRegistry::new();
776        let claim_check_repositories = crate::ClaimCheckRegistry::new();
777        let cache_repositories = crate::CacheRegistry::new();
778
779        let ctx = CompilationContext {
780            producer_ctx: &pc,
781            rt,
782            languages: &languages,
783            beans: &beans,
784            function_invoker: None,
785            component_ctx,
786            route_id: None,
787            staging_mode: &staging,
788            idempotent_repositories: &idempotent_repositories,
789            claim_check_repositories: &claim_check_repositories,
790            cache_repositories: &cache_repositories,
791            intercept: InterceptRules::default(),
792        };
793
794        // Outer Filter containing an inner Filter that has a stateful Processor.
795        // The outer Segment's lifecycle should contain the innermost handle
796        // (proves recursive flattening through compile_children_segments).
797        let inner_filter = BuilderStep::Filter {
798            predicate: FilterPredicate::new(|_| true),
799            steps: vec![BuilderStep::Processor(OpaqueProcessor(
800                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
801            ))],
802        };
803
804        let outer_filter = BuilderStep::Filter {
805            predicate: FilterPredicate::new(|_| true),
806            steps: vec![inner_filter],
807        };
808
809        let result = reg.compile_step(outer_filter, 0, &ctx);
810        let compiled = result
811            .expect("compilation should succeed")
812            .expect("should match");
813
814        match compiled {
815            CompiledStep::Segment { lifecycle, .. } => {
816                let handles = lifecycle.expect("outer Segment should have lifecycle handles");
817                assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
818                assert_eq!(
819                    handles[0].name(),
820                    "deep",
821                    "handle should be from innermost child"
822                );
823            }
824            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
825        }
826    }
827}
828
829#[cfg(test)]
830mod dispatch_tests {
831    use super::*;
832    use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
833    use camel_api::{BoxProcessor, BoxProcessorExt};
834    use camel_bean::BeanRegistry;
835    use camel_component_api::{
836        ComponentContext, NoOpComponentContext, RuntimeObservability,
837        test_support::NoopRuntimeObservability,
838    };
839    use std::collections::HashMap;
840    use std::sync::Mutex;
841
842    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Stop`.
843    struct ToStopCompiler;
844
845    impl StepCompiler for ToStopCompiler {
846        fn compile(
847            &self,
848            step: BuilderStep,
849            _step_index: usize,
850            _ctx: &CompilationContext,
851            _registry: &StepCompilerRegistry,
852        ) -> Result<CompileOutcome, CamelError> {
853            match step {
854                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
855                other => Ok(CompileOutcome::NotHandled(other)),
856            }
857        }
858    }
859
860    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Process`.
861    struct ToProcessCompiler;
862
863    impl StepCompiler for ToProcessCompiler {
864        fn compile(
865            &self,
866            step: BuilderStep,
867            _step_index: usize,
868            _ctx: &CompilationContext,
869            _registry: &StepCompilerRegistry,
870        ) -> Result<CompileOutcome, CamelError> {
871            match step {
872                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
873                    processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
874                    body_contract: None,
875                    lifecycle: None,
876                })),
877                other => Ok(CompileOutcome::NotHandled(other)),
878            }
879        }
880    }
881
882    /// Compiler that passes all steps through (never handles any variant).
883    /// Returns `NotHandled(step)` for every input, proving by-value pass-through.
884    struct PassThroughCompiler;
885
886    impl StepCompiler for PassThroughCompiler {
887        fn compile(
888            &self,
889            step: BuilderStep,
890            _step_index: usize,
891            _ctx: &CompilationContext,
892            _registry: &StepCompilerRegistry,
893        ) -> Result<CompileOutcome, CamelError> {
894            Ok(CompileOutcome::NotHandled(step))
895        }
896    }
897
898    /// Shared context builder — avoids repeating the 15-line setup in every test.
899    #[allow(clippy::too_many_arguments)]
900    fn ctx<'a>(
901        pc: &'a ProducerContext,
902        rt: Arc<dyn RuntimeObservability>,
903        languages: &'a SharedLanguageRegistry,
904        beans: &'a Arc<Mutex<BeanRegistry>>,
905        component_ctx: Arc<dyn ComponentContext>,
906        staging: &'a FunctionStagingMode,
907        idempotent_repositories: &'a crate::IdempotentRegistry,
908        claim_check_repositories: &'a crate::ClaimCheckRegistry,
909        cache_repositories: &'a crate::CacheRegistry,
910    ) -> CompilationContext<'a> {
911        CompilationContext {
912            producer_ctx: pc,
913            rt,
914            languages,
915            beans,
916            function_invoker: None,
917            component_ctx,
918            route_id: None,
919            staging_mode: staging,
920            idempotent_repositories,
921            claim_check_repositories,
922            cache_repositories,
923            intercept: InterceptRules::default(),
924        }
925    }
926
927    /// Test that the dispatcher respects registration order: the first matching
928    /// compiler wins even when a later compiler also matches.
929    #[test]
930    fn compile_step_preserves_registry_order() {
931        let pc = ProducerContext::default();
932        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
933        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
934        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
935        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
936        let staging = FunctionStagingMode::DirectAdd;
937        let idempotent_repositories = crate::IdempotentRegistry::new();
938        let claim_check_repositories = crate::ClaimCheckRegistry::new();
939        let cache_repositories = crate::CacheRegistry::new();
940
941        let context = ctx(
942            &pc,
943            rt,
944            &languages,
945            &beans,
946            component_ctx,
947            &staging,
948            &idempotent_repositories,
949            &claim_check_repositories,
950            &cache_repositories,
951        );
952
953        // Register ToStopCompiler FIRST, ToProcessCompiler SECOND.
954        // Both match BuilderStep::To. The first-registered should win.
955        let mut reg = StepCompilerRegistry::new();
956        reg.register(Box::new(ToStopCompiler));
957        reg.register(Box::new(ToProcessCompiler));
958
959        let result = reg
960            .compile_step(BuilderStep::To("test".into()), 0, &context)
961            .expect("compilation should succeed")
962            .expect("should match");
963
964        assert!(
965            matches!(result, CompiledStep::Stop),
966            "expected ToStopCompiler (first registered) to win, got {result:?}"
967        );
968    }
969
970    /// Test that when no compiler handles a variant, the dispatcher returns
971    /// `Ok(None)` rather than an error.
972    #[test]
973    fn compile_step_unhandled_returns_ok_none() {
974        let pc = ProducerContext::default();
975        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
976        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
977        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
978        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
979        let staging = FunctionStagingMode::DirectAdd;
980        let idempotent_repositories = crate::IdempotentRegistry::new();
981        let claim_check_repositories = crate::ClaimCheckRegistry::new();
982        let cache_repositories = crate::CacheRegistry::new();
983
984        let context = ctx(
985            &pc,
986            rt,
987            &languages,
988            &beans,
989            component_ctx,
990            &staging,
991            &idempotent_repositories,
992            &claim_check_repositories,
993            &cache_repositories,
994        );
995
996        // Register a compiler that only handles BuilderStep::To.
997        let mut reg = StepCompilerRegistry::new();
998        reg.register(Box::new(ToStopCompiler));
999
1000        // Send a BuilderStep::Log variant — ToStopCompiler does not match it.
1001        let result = reg.compile_step(
1002            BuilderStep::Log {
1003                level: camel_processor::LogLevel::Info,
1004                message: "unhandled".into(),
1005            },
1006            0,
1007            &context,
1008        );
1009
1010        assert!(
1011            matches!(result, Ok(None)),
1012            "expected Ok(None), got {result:?}"
1013        );
1014    }
1015
1016    /// Test that `NotHandled(step)` passes the step by-value to the next
1017    /// compiler. Compiler N returns `NotHandled(step)` for a variant that
1018    /// compiler N+1 handles — proving the step is not dropped or replaced.
1019    #[test]
1020    fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
1021        let pc = ProducerContext::default();
1022        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1023        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1024        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1025        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1026        let staging = FunctionStagingMode::DirectAdd;
1027        let idempotent_repositories = crate::IdempotentRegistry::new();
1028        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1029        let cache_repositories = crate::CacheRegistry::new();
1030
1031        let context = ctx(
1032            &pc,
1033            rt,
1034            &languages,
1035            &beans,
1036            component_ctx,
1037            &staging,
1038            &idempotent_repositories,
1039            &claim_check_repositories,
1040            &cache_repositories,
1041        );
1042
1043        // Register PassThroughCompiler FIRST (never handles anything),
1044        // ToStopCompiler SECOND (handles BuilderStep::To).
1045        let mut reg = StepCompilerRegistry::new();
1046        reg.register(Box::new(PassThroughCompiler));
1047        reg.register(Box::new(ToStopCompiler));
1048
1049        let result = reg
1050            .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1051            .expect("compilation should succeed")
1052            .expect("should match");
1053
1054        assert!(
1055            matches!(result, CompiledStep::Stop),
1056            "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1057        );
1058    }
1059}