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