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