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, SpanKindHint,
12    StepLifecycle,
13};
14use camel_component_api::{ComponentContext, RuntimeObservability};
15use camel_endpoint::parse_uri;
16
17use crate::intercept::InterceptRules;
18use crate::lifecycle::adapters::route_controller::SharedLanguageRegistry;
19use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
20use crate::lifecycle::application::route_definition::BuilderStep;
21use crate::{CacheRegistry, ClaimCheckRegistry, IdempotentRegistry};
22use camel_bean::BeanRegistry;
23
24mod control_flow;
25mod core;
26mod endpoints;
27mod error_handling;
28mod routing;
29mod splitting;
30mod transforms;
31
32/// A compiled pipeline step.
33///
34/// `Process` is the normal case: a boxed processor plus its optional body
35/// contract. `Stop` (added in Task 3b) is the Stop EIP marker — `run_steps`
36/// recognises it and produces `PipelineOutcome::Stopped` without invoking a
37/// Tower service. `Segment` (added in Task 3) wraps an `OutcomeSegment` for
38/// structural EIPs with outcome-aware sub-pipelines.
39///
40/// **Boundary:** `CompiledStep` is the compile-time representation. At runtime,
41/// `run_steps` consumes a `Vec<CompiledStep>` (Stop variants included) and
42/// produces a `PipelineOutcome`; the wrapping `Service<Exchange>` impl
43/// translates `PipelineOutcome` back to `Result<Exchange, CamelError>`. See
44/// ADR-0024.
45#[derive(Debug, Clone)]
46pub enum CompiledStep {
47    Process {
48        processor: BoxProcessor,
49        body_contract: Option<BodyType>,
50        /// Lifecycle handle for this processor, if it is stateful.
51        /// `None` for stateless processors (the common case).
52        lifecycle: Option<Arc<dyn StepLifecycle>>,
53        /// Stable span name (e.g. `to:direct`, `split`), stamped by
54        /// `StepCompilerRegistry::compile_step` from
55        /// `BuilderStep::span_label`. `None` for anonymous steps — spans
56        /// fall back to the positional `step-{index}` name. Read by
57        /// `compose_traced_pipeline` / `segment_span` in route_compiler.rs.
58        label: Option<Arc<str>>,
59        /// Span kind for this step's step span, stamped by
60        /// `StepCompilerRegistry::compile_step` from
61        /// `BuilderStep::span_kind_hint`. `Internal` for steps built
62        /// outside the registry. Read by `compose_traced_pipeline` in
63        /// route_compiler.rs.
64        kind_hint: SpanKindHint,
65        /// Declared send URI for `To` steps — the raw authored text,
66        /// stamped by `StepCompilerRegistry::compile_step` from
67        /// `BuilderStep::to_uri_metadata` BEFORE endpoint resolution and
68        /// interception (a `SkipTo` substitution or `DivertCopyTo`
69        /// diversion never rewrites it). `None` for every non-To step.
70        /// Stored as shared immutable text; read by the tracing
71        /// pipeline (steplatency task 2.1) in route_compiler.rs.
72        to_uri: Option<Arc<str>>,
73    },
74    /// Stop EIP marker. `run_steps` produces `PipelineOutcome::Stopped(ex)`
75    /// without invoking a Tower service. Replaces `StopService` (Task 7).
76    Stop,
77    /// Outcome-aware structural EIP segment. `run_steps` invokes
78    /// `segment.run(ex)` and matches on the returned `PipelineOutcome`.
79    /// See ADR-0025.
80    Segment {
81        segment: camel_api::OutcomeSegment,
82        body_contract: Option<BodyType>,
83        /// Lifecycle handles from children nested inside this segment.
84        /// `Option<Vec<...>>` (not `Option<Arc<...>>`) because multiple stateful
85        /// children (e.g. Idempotent+Resequencer inside Filter) each
86        /// register independently.
87        lifecycle: Option<Vec<Arc<dyn StepLifecycle>>>,
88        /// Stable span name, same contract as [`CompiledStep::Process`]'s
89        /// `label` field.
90        label: Option<Arc<str>>,
91    },
92}
93
94impl CompiledStep {
95    /// Overwrite the span label of a `Process`/`Segment` step; no-op on `Stop`
96    /// (the marker produces no step span, so it carries no label).
97    pub(crate) fn set_label(&mut self, label: Option<String>) {
98        let label = label.map(Arc::from);
99        match self {
100            CompiledStep::Process { label: slot, .. }
101            | CompiledStep::Segment { label: slot, .. } => {
102                *slot = label;
103            }
104            CompiledStep::Stop => {}
105        }
106    }
107
108    /// Overwrite the span kind hint of a `Process` step; no-op on `Stop` and
109    /// `Segment` (neither produces an individual step span, so neither
110    /// carries a kind hint).
111    pub(crate) fn set_kind_hint(&mut self, hint: SpanKindHint) {
112        match self {
113            CompiledStep::Process {
114                kind_hint: slot, ..
115            } => *slot = hint,
116            CompiledStep::Stop | CompiledStep::Segment { .. } => {}
117        }
118    }
119
120    /// Overwrite the declared send URI of a `Process` step; no-op on `Stop`
121    /// and `Segment` (neither is a send step, so neither carries declared
122    /// URI metadata).
123    pub(crate) fn set_to_uri(&mut self, to_uri: Option<Arc<str>>) {
124        match self {
125            CompiledStep::Process { to_uri: slot, .. } => *slot = to_uri,
126            CompiledStep::Stop | CompiledStep::Segment { .. } => {}
127        }
128    }
129}
130
131/// Result from a compiler: either it handled the step, or it did not recognize
132/// the variant and returns the step for the next compiler.
133///
134/// `Result` is intentionally outside this enum (returned by `StepCompiler::compile`)
135/// so that compiler arms can use the `?` operator for propagated errors instead of
136/// wrapping every `Err` in `Matched(Err(e))`.
137pub(crate) enum CompileOutcome {
138    Matched(CompiledStep),
139    NotHandled(BuilderStep),
140}
141
142/// A compiler that can handle one or more `BuilderStep` variants.
143///
144/// The `compile` method receives ownership of the step. If the compiler recognizes
145/// the variant it returns `Ok(CompileOutcome::Matched(...))`. Otherwise it returns
146/// `Ok(CompileOutcome::NotHandled(step))` to pass the step to the next compiler.
147/// Compilation errors are returned as `Err(CamelError)`.
148pub(crate) trait StepCompiler: Send + Sync {
149    fn compile(
150        &self,
151        step: BuilderStep,
152        step_index: usize,
153        ctx: &CompilationContext,
154        registry: &StepCompilerRegistry,
155    ) -> Result<CompileOutcome, CamelError>;
156}
157
158/// Shared context passed to every compiler invocation.
159pub(crate) struct CompilationContext<'a> {
160    pub producer_ctx: &'a ProducerContext,
161    pub rt: Arc<dyn RuntimeObservability>,
162    pub languages: &'a SharedLanguageRegistry,
163    pub beans: &'a Arc<std::sync::Mutex<BeanRegistry>>,
164    pub function_invoker: Option<Arc<dyn FunctionInvoker>>,
165    pub component_ctx: Arc<dyn ComponentContext>,
166    pub route_id: Option<&'a str>,
167    pub staging_mode: &'a FunctionStagingMode,
168    /// Idempotent repository registry. Used by the `IdempotentConsumer`
169    /// compiler arm to resolve repository names into `Arc<dyn IdempotentRepository>`.
170    pub idempotent_repositories: &'a IdempotentRegistry,
171    /// Claim check repository registry. Used by the `ClaimCheck` compiler arm
172    /// to resolve repository names into `Arc<dyn ClaimCheckRepository>`.
173    pub claim_check_repositories: &'a ClaimCheckRegistry,
174    /// Cache repository registry. Used by the `Cache`, `CacheInvalidate`, and
175    /// `CachePeekStale` compiler arms to resolve repository names into
176    /// `Arc<dyn CacheRepository>`.
177    pub cache_repositories: &'a CacheRegistry,
178    /// Route send-point interception rules captured at compile time.
179    /// Consulted by send-step compilation (Task 4/5).
180    pub intercept: InterceptRules,
181}
182
183impl<'a> CompilationContext<'a> {
184    /// Recursively compile child steps. Used by compilers that have sub-pipelines
185    /// (Filter, Choice, Split, Loop, etc.).
186    pub fn compile_children(
187        &self,
188        steps: Vec<BuilderStep>,
189        registry: &StepCompilerRegistry,
190    ) -> Result<Vec<CompiledStep>, CamelError> {
191        registry.compile_steps(steps, self)
192    }
193
194    /// Recursively compile child steps and map them into outcome-aware segments.
195    ///
196    /// Each `CompiledStep` variant is converted to a `Box<dyn OutcomePipeline>`:
197    /// - `Process` → `BoxProcessorSegment`, optionally wrapped in `BodyCoercingSegment`
198    /// - `Stop` → `StopSegment` (produces `PipelineOutcome::Stopped(ex)`)
199    /// - `Segment` → its inner `OutcomeSegment` (which now implements OutcomePipeline)
200    ///
201    /// This replaces the 22-line duplicated closure in Filter/DeclarativeFilter
202    /// (and will prevent 14+ more duplicates in T9–T16).
203    #[allow(clippy::type_complexity)]
204    pub fn compile_children_segments(
205        &self,
206        steps: Vec<BuilderStep>,
207        registry: &StepCompilerRegistry,
208    ) -> Result<
209        (
210            Vec<Box<dyn camel_api::OutcomePipeline>>,
211            Vec<Arc<dyn camel_api::StepLifecycle>>,
212        ),
213        CamelError,
214    > {
215        let pairs = self.compile_children(steps, registry)?;
216        let mut lifecycle_handles: Vec<Arc<dyn camel_api::StepLifecycle>> = Vec::new();
217        let segments: Vec<Box<dyn camel_api::OutcomePipeline>> = pairs
218            .into_iter()
219            .map(|c| match c {
220                CompiledStep::Process {
221                    processor,
222                    body_contract,
223                    lifecycle,
224                    label: _,
225                    kind_hint: _,
226                    to_uri: _,
227                } => {
228                    if let Some(lc) = lifecycle {
229                        lifecycle_handles.push(lc);
230                    }
231                    let inner: Box<dyn camel_api::OutcomePipeline> = Box::new(
232                        crate::lifecycle::adapters::route_compiler::BoxProcessorSegment::new(
233                            processor,
234                        ),
235                    );
236                    match body_contract {
237                        Some(contract) => Box::new(
238                            crate::lifecycle::adapters::route_compiler::BodyCoercingSegment::new(
239                                inner, contract,
240                            ),
241                        ),
242                        None => inner,
243                    }
244                }
245                CompiledStep::Stop => {
246                    Box::new(crate::lifecycle::adapters::route_compiler::StopSegment)
247                        as Box<dyn camel_api::OutcomePipeline>
248                }
249                CompiledStep::Segment {
250                    segment,
251                    body_contract: _,
252                    lifecycle,
253                    label: _,
254                } => {
255                    if let Some(lcs) = lifecycle {
256                        lifecycle_handles.extend(lcs);
257                    }
258                    Box::new(segment)
259                }
260            })
261            .collect();
262        Ok((segments, lifecycle_handles))
263    }
264}
265
266/// Registry of step compilers. Steps are dispatched to compilers in registration
267/// order. The first matching compiler handles the step.
268pub(crate) struct StepCompilerRegistry {
269    compilers: Vec<Box<dyn StepCompiler>>,
270}
271
272impl StepCompilerRegistry {
273    pub fn new() -> Self {
274        Self {
275            compilers: Vec::new(),
276        }
277    }
278
279    pub fn register(&mut self, compiler: Box<dyn StepCompiler>) {
280        self.compilers.push(compiler);
281    }
282
283    /// Try each compiler in order. The first to return `Matched` wins.
284    /// If all return `NotHandled`, returns `Ok(None)`. Compilation errors
285    /// short-circuit and return `Err(CamelError)`.
286    pub fn compile_step(
287        &self,
288        step: BuilderStep,
289        step_index: usize,
290        ctx: &CompilationContext,
291    ) -> Result<Option<CompiledStep>, CamelError> {
292        // Capture the span label, kind hint, and declared To URI BEFORE the
293        // dispatch loop moves the step. The URI is the authored text, so a
294        // `SkipTo`/`DivertCopyTo` interception inside a compiler never
295        // rewrites the retained metadata.
296        let label = step.span_label();
297        let kind_hint = step.span_kind_hint();
298        let to_uri = step.to_uri_metadata();
299        let mut step = step;
300        for compiler in &self.compilers {
301            match compiler.compile(step, step_index, ctx, self)? {
302                CompileOutcome::Matched(mut s) => {
303                    s.set_label(label);
304                    s.set_kind_hint(kind_hint);
305                    s.set_to_uri(to_uri);
306                    return Ok(Some(s));
307                }
308                CompileOutcome::NotHandled(s) => step = s,
309            }
310        }
311        Ok(None)
312    }
313
314    /// Compile all steps in a vector.
315    pub fn compile_steps(
316        &self,
317        steps: Vec<BuilderStep>,
318        ctx: &CompilationContext,
319    ) -> Result<Vec<CompiledStep>, CamelError> {
320        let mut out = Vec::with_capacity(steps.len());
321        for (i, step) in steps.into_iter().enumerate() {
322            match self.compile_step(step, i, ctx)? {
323                Some(c) => out.push(c),
324                None => {
325                    return Err(CamelError::RouteError(
326                        "no compiler registered for step variant".into(),
327                    ));
328                }
329            }
330        }
331        Ok(out)
332    }
333}
334
335/// A resolved send target: the pieces the un-intercepted `To` path produces
336/// for one URI.
337pub(super) struct ResolvedSend {
338    pub producer: BoxProcessor,
339    pub body_contract: Option<BodyType>,
340    pub lifecycle: Option<Arc<dyn StepLifecycle>>,
341}
342
343/// Resolve a send URI into producer/body-contract/lifecycle, exactly as the
344/// un-intercepted `To` path does. Canonical resolution path for send steps;
345/// [`resolve_producer_with_lifecycle`] and [`resolve_producer`] are thin
346/// wrappers over this.
347pub(super) fn resolve_send(
348    ctx: &CompilationContext,
349    uri: &str,
350) -> Result<ResolvedSend, CamelError> {
351    let parsed = parse_uri(uri)?;
352    let component = ctx
353        .component_ctx
354        .resolve_component(&parsed.scheme)
355        .ok_or_else(|| CamelError::ComponentNotFound(parsed.scheme.clone()))?;
356    let endpoint = component.create_endpoint(uri, ctx.component_ctx.as_ref())?;
357    let body_contract = endpoint.body_contract();
358    let producer = endpoint.create_producer(Arc::clone(&ctx.rt), ctx.producer_ctx)?;
359    // Capture the endpoint's lifecycle handle so the route controller can
360    // start/shut it down in route order (ADR-0022). Default is `None` for
361    // stateless endpoints — see `Endpoint::lifecycle` in camel-component-api.
362    let lifecycle: Option<Arc<dyn StepLifecycle>> = endpoint.lifecycle();
363    Ok(ResolvedSend {
364        producer,
365        body_contract,
366        lifecycle,
367    })
368}
369
370/// Parse a URI and create a producer, also returning the endpoint's
371/// [`StepLifecycle`] handle (if any). Thin wrapper over [`resolve_send`] that
372/// discards the body contract. Use directly when the lifecycle is needed
373/// (e.g. `WireTap` propagation).
374pub(crate) fn resolve_producer_with_lifecycle(
375    ctx: &CompilationContext,
376    uri: &str,
377) -> Result<(BoxProcessor, Option<Arc<dyn StepLifecycle>>), CamelError> {
378    let resolved = resolve_send(ctx, uri)?;
379    Ok((resolved.producer, resolved.lifecycle))
380}
381
382/// Parse a URI and create a producer, reusing `component_ctx`, `rt`, and `producer_ctx`
383/// from the compilation context.
384///
385/// Thin wrapper over [`resolve_producer_with_lifecycle`] that discards the
386/// endpoint's [`StepLifecycle`] handle.
387pub(crate) fn resolve_producer(
388    ctx: &CompilationContext,
389    uri: &str,
390) -> Result<BoxProcessor, CamelError> {
391    Ok(resolve_producer_with_lifecycle(ctx, uri)?.0)
392}
393
394/// Pack a lifecycle Vec into `None` when empty, `Some` when non-empty.
395/// Preserves the invariant that `Some` always implies ≥1 handle.
396pub(super) fn pack_lifecycles(
397    lifecycles: Vec<Arc<dyn StepLifecycle>>,
398) -> Option<Vec<Arc<dyn StepLifecycle>>> {
399    if lifecycles.is_empty() {
400        None
401    } else {
402        Some(lifecycles)
403    }
404}
405
406/// Build the full registry with all compiler groups.
407pub(crate) fn build_registry() -> StepCompilerRegistry {
408    let mut reg = StepCompilerRegistry::new();
409    reg.register(Box::new(core::CoreCompiler));
410    reg.register(Box::new(endpoints::EndpointsCompiler));
411    reg.register(Box::new(transforms::TransformsCompiler));
412    reg.register(Box::new(routing::RoutingCompiler));
413    reg.register(Box::new(control_flow::ControlFlowCompiler));
414    reg.register(Box::new(splitting::SplittingCompiler));
415    reg.register(Box::new(error_handling::ErrorHandlingCompiler));
416    reg
417}
418
419#[cfg(test)]
420mod segment_tests {
421    use super::*;
422    use camel_api::{Exchange, OutcomePipeline, PipelineOutcome};
423    use std::future::Future;
424    use std::pin::Pin;
425
426    #[derive(Clone)]
427    struct EchoSegment;
428
429    impl OutcomePipeline for EchoSegment {
430        fn clone_box(&self) -> Box<dyn OutcomePipeline> {
431            Box::new(EchoSegment)
432        }
433        fn run<'a>(
434            &'a mut self,
435            exchange: Exchange,
436        ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
437            Box::pin(async move { PipelineOutcome::Completed(exchange) })
438        }
439    }
440
441    #[test]
442    fn compiled_step_segment_clone_compiles() {
443        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
444        let step = CompiledStep::Segment {
445            segment: seg,
446            body_contract: None,
447            lifecycle: None,
448            label: None,
449        };
450        let _cloned = step.clone();
451        if let CompiledStep::Segment { .. } = _cloned {
452            // ok
453        } else {
454            panic!("clone should preserve variant");
455        }
456    }
457
458    #[test]
459    fn compiled_step_segment_debug_renders() {
460        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
461        let step = CompiledStep::Segment {
462            segment: seg,
463            body_contract: None,
464            lifecycle: None,
465            label: None,
466        };
467        let s = format!("{:?}", step);
468        assert!(
469            s.contains("Segment"),
470            "debug should mention Segment variant: {s}"
471        );
472    }
473
474    #[test]
475    fn outcome_segment_satisfies_clone_send_static() {
476        fn assert_traits<T: Clone + Send + 'static>() {}
477        assert_traits::<camel_api::OutcomeSegment>();
478    }
479
480    #[tokio::test]
481    #[allow(clippy::arc_with_non_send_sync)]
482    async fn outcome_segment_survives_arcswap_swap() {
483        use arc_swap::ArcSwap;
484        use camel_api::{Exchange, Message, OutcomePipeline, PipelineOutcome};
485        use std::sync::Arc;
486
487        #[derive(Clone)]
488        struct EchoSegment;
489        impl OutcomePipeline for EchoSegment {
490            fn clone_box(&self) -> Box<dyn OutcomePipeline> {
491                Box::new(EchoSegment)
492            }
493            fn run<'a>(
494                &'a mut self,
495                ex: Exchange,
496            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = PipelineOutcome> + Send + 'a>>
497            {
498                Box::pin(async move { PipelineOutcome::Completed(ex) })
499            }
500        }
501
502        let seg = camel_api::OutcomeSegment::new(Box::new(EchoSegment));
503        let slot: ArcSwap<Option<camel_api::OutcomeSegment>> = ArcSwap::from_pointee(None);
504        slot.store(Arc::new(Some(seg.clone())));
505        slot.store(Arc::new(Some(seg)));
506
507        let mut borrowed = slot.load().as_ref().clone().unwrap();
508        let outcome = borrowed.run(Exchange::new(Message::new("ping"))).await;
509        assert!(matches!(outcome, PipelineOutcome::Completed(_)));
510    }
511
512    /// Test lifecycle handle used by compile_children_segments_bubbles_child_lifecycle.
513    #[derive(Debug)]
514    struct TestLifecycle;
515
516    #[async_trait::async_trait]
517    impl camel_api::StepLifecycle for TestLifecycle {
518        fn name(&self) -> &'static str {
519            "test-lifecycle"
520        }
521        async fn shutdown(
522            &self,
523            _reason: camel_api::StepShutdownReason,
524        ) -> Result<(), camel_api::CamelError> {
525            Ok(())
526        }
527    }
528
529    /// Custom compiler that injects a lifecycle handle into every
530    /// `BuilderStep::Processor` it compiles.
531    struct LifecycleInjectorCompiler {
532        handle: Arc<dyn camel_api::StepLifecycle>,
533    }
534
535    impl StepCompiler for LifecycleInjectorCompiler {
536        fn compile(
537            &self,
538            step: BuilderStep,
539            _step_index: usize,
540            _ctx: &CompilationContext,
541            _registry: &StepCompilerRegistry,
542        ) -> Result<CompileOutcome, CamelError> {
543            match step {
544                BuilderStep::Processor(op) => Ok(CompileOutcome::Matched(CompiledStep::Process {
545                    processor: op.0,
546                    body_contract: None,
547                    lifecycle: Some(self.handle.clone()),
548                    label: None,
549                    kind_hint: SpanKindHint::Internal,
550                    to_uri: None,
551                })),
552                other => Ok(CompileOutcome::NotHandled(other)),
553            }
554        }
555    }
556
557    #[tokio::test]
558    async fn compile_children_segments_bubbles_child_lifecycle() {
559        use std::collections::HashMap;
560        use std::sync::Mutex;
561
562        use camel_api::{
563            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
564        };
565        use camel_bean::BeanRegistry;
566        use camel_component_api::{
567            ComponentContext, NoOpComponentContext, RuntimeObservability,
568            test_support::NoopRuntimeObservability,
569        };
570
571        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
572
573        let handle: Arc<dyn StepLifecycle> = Arc::new(TestLifecycle);
574
575        // Register lifecycle injector + real control-flow compiler so
576        // compile_children_segments runs through a structural EIP path.
577        let mut reg = StepCompilerRegistry::new();
578        reg.register(Box::new(LifecycleInjectorCompiler {
579            handle: handle.clone(),
580        }));
581        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
582
583        let pc = ProducerContext::default();
584        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
585        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
586        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
587        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
588        let staging = FunctionStagingMode::DirectAdd;
589        let idempotent_repositories = crate::IdempotentRegistry::new();
590        let claim_check_repositories = crate::ClaimCheckRegistry::new();
591        let cache_repositories = crate::CacheRegistry::new();
592
593        let ctx = CompilationContext {
594            producer_ctx: &pc,
595            rt,
596            languages: &languages,
597            beans: &beans,
598            function_invoker: None,
599            component_ctx,
600            route_id: None,
601            staging_mode: &staging,
602            idempotent_repositories: &idempotent_repositories,
603            claim_check_repositories: &claim_check_repositories,
604            cache_repositories: &cache_repositories,
605            intercept: InterceptRules::default(),
606        };
607
608        // Compile a Filter with a child Processor step.
609        let filter_step = BuilderStep::Filter {
610            predicate: FilterPredicate::new(|_| true),
611            steps: vec![BuilderStep::Processor(OpaqueProcessor(
612                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
613            ))],
614        };
615
616        let result = reg.compile_step(filter_step, 0, &ctx);
617        let compiled = result
618            .expect("compilation should succeed")
619            .expect("should match");
620
621        match compiled {
622            CompiledStep::Segment {
623                lifecycle,
624                body_contract,
625                ..
626            } => {
627                assert_eq!(body_contract, None, "body_contract should be None");
628                let handles = lifecycle.expect("Segment should have lifecycle handles");
629                assert_eq!(handles.len(), 1, "expected 1 lifecycle handle");
630                assert_eq!(handles[0].name(), "test-lifecycle", "handle name mismatch");
631            }
632            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
633        }
634    }
635
636    /// A lifecycle handle with a configurable name for multi-handle tests.
637    #[derive(Debug)]
638    struct NamedLifecycle(&'static str);
639
640    #[async_trait::async_trait]
641    impl camel_api::StepLifecycle for NamedLifecycle {
642        fn name(&self) -> &'static str {
643            self.0
644        }
645        async fn shutdown(
646            &self,
647            _reason: camel_api::StepShutdownReason,
648        ) -> Result<(), camel_api::CamelError> {
649            Ok(())
650        }
651    }
652
653    /// Test A: Multiple stateful children in one Segment → Vec length 2.
654    #[tokio::test]
655    async fn compile_children_segments_multiple_stateful_children() {
656        use std::collections::HashMap;
657        use std::sync::Mutex;
658
659        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
660        use camel_api::{
661            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
662        };
663        use camel_bean::BeanRegistry;
664        use camel_component_api::{
665            ComponentContext, NoOpComponentContext, RuntimeObservability,
666            test_support::NoopRuntimeObservability,
667        };
668
669        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("multi"));
670
671        let mut reg = StepCompilerRegistry::new();
672        reg.register(Box::new(LifecycleInjectorCompiler {
673            handle: handle.clone(),
674        }));
675        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
676
677        let pc = ProducerContext::default();
678        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
679        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
680        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
681        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
682        let staging = FunctionStagingMode::DirectAdd;
683        let idempotent_repositories = crate::IdempotentRegistry::new();
684        let claim_check_repositories = crate::ClaimCheckRegistry::new();
685        let cache_repositories = crate::CacheRegistry::new();
686
687        let ctx = CompilationContext {
688            producer_ctx: &pc,
689            rt,
690            languages: &languages,
691            beans: &beans,
692            function_invoker: None,
693            component_ctx,
694            route_id: None,
695            staging_mode: &staging,
696            idempotent_repositories: &idempotent_repositories,
697            claim_check_repositories: &claim_check_repositories,
698            cache_repositories: &cache_repositories,
699            intercept: InterceptRules::default(),
700        };
701
702        // Filter with TWO child Processors → both get the same lifecycle handle.
703        let filter_step = BuilderStep::Filter {
704            predicate: FilterPredicate::new(|_| true),
705            steps: vec![
706                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
707                    Box::pin(async move { Ok(ex) })
708                }))),
709                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
710                    Box::pin(async move { Ok(ex) })
711                }))),
712            ],
713        };
714
715        let result = reg.compile_step(filter_step, 0, &ctx);
716        let compiled = result
717            .expect("compilation should succeed")
718            .expect("should match");
719
720        match compiled {
721            CompiledStep::Segment { lifecycle, .. } => {
722                let handles = lifecycle.expect("Segment should have lifecycle handles");
723                assert_eq!(
724                    handles.len(),
725                    2,
726                    "expected 2 lifecycle handles for 2 children"
727                );
728                for h in &handles {
729                    assert_eq!(h.name(), "multi", "all handles should be 'multi'");
730                }
731            }
732            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
733        }
734    }
735
736    /// Test B: Multi-branch accumulation across Choice when-clauses.
737    #[tokio::test]
738    async fn compile_children_segments_multi_branch_accumulation() {
739        use std::collections::HashMap;
740        use std::sync::Mutex;
741
742        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
743        use crate::lifecycle::application::route_definition::WhenStep;
744        use camel_api::{
745            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
746        };
747        use camel_bean::BeanRegistry;
748        use camel_component_api::{
749            ComponentContext, NoOpComponentContext, RuntimeObservability,
750            test_support::NoopRuntimeObservability,
751        };
752
753        let handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("branch"));
754
755        let mut reg = StepCompilerRegistry::new();
756        reg.register(Box::new(LifecycleInjectorCompiler {
757            handle: handle.clone(),
758        }));
759        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
760
761        let pc = ProducerContext::default();
762        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
763        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
764        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
765        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
766        let staging = FunctionStagingMode::DirectAdd;
767        let idempotent_repositories = crate::IdempotentRegistry::new();
768        let claim_check_repositories = crate::ClaimCheckRegistry::new();
769        let cache_repositories = crate::CacheRegistry::new();
770
771        let ctx = CompilationContext {
772            producer_ctx: &pc,
773            rt,
774            languages: &languages,
775            beans: &beans,
776            function_invoker: None,
777            component_ctx,
778            route_id: None,
779            staging_mode: &staging,
780            idempotent_repositories: &idempotent_repositories,
781            claim_check_repositories: &claim_check_repositories,
782            cache_repositories: &cache_repositories,
783            intercept: InterceptRules::default(),
784        };
785
786        // Choice with 2 when branches, each containing 1 stateful child.
787        let choice_step = BuilderStep::Choice {
788            whens: vec![
789                WhenStep {
790                    predicate: FilterPredicate::new(|_| true),
791                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
792                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
793                    ))],
794                },
795                WhenStep {
796                    predicate: FilterPredicate::new(|_| false),
797                    steps: vec![BuilderStep::Processor(OpaqueProcessor(
798                        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
799                    ))],
800                },
801            ],
802            otherwise: None,
803        };
804
805        let result = reg.compile_step(choice_step, 0, &ctx);
806        let compiled = result
807            .expect("compilation should succeed")
808            .expect("should match");
809
810        match compiled {
811            CompiledStep::Segment { lifecycle, .. } => {
812                let handles = lifecycle.expect("Segment should have lifecycle handles");
813                assert_eq!(
814                    handles.len(),
815                    2,
816                    "expected 2 lifecycle handles from 2 branches"
817                );
818                for h in &handles {
819                    assert_eq!(h.name(), "branch", "all handles should be 'branch'");
820                }
821            }
822            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
823        }
824    }
825
826    /// Test C: Nested Segment-in-Segment flattening — outer Segment contains
827    /// innermost lifecycle handle from a grandchild Processor.
828    #[tokio::test]
829    async fn compile_children_segments_nested_segment_flattening() {
830        use std::collections::HashMap;
831        use std::sync::Mutex;
832
833        use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
834        use camel_api::{
835            BoxProcessor, BoxProcessorExt, FilterPredicate, OpaqueProcessor, StepLifecycle,
836        };
837        use camel_bean::BeanRegistry;
838        use camel_component_api::{
839            ComponentContext, NoOpComponentContext, RuntimeObservability,
840            test_support::NoopRuntimeObservability,
841        };
842
843        let inner_handle: Arc<dyn StepLifecycle> = Arc::new(NamedLifecycle("deep"));
844
845        let mut reg = StepCompilerRegistry::new();
846        reg.register(Box::new(LifecycleInjectorCompiler {
847            handle: inner_handle.clone(),
848        }));
849        reg.register(Box::new(super::control_flow::ControlFlowCompiler));
850
851        let pc = ProducerContext::default();
852        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
853        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
854        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
855        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
856        let staging = FunctionStagingMode::DirectAdd;
857        let idempotent_repositories = crate::IdempotentRegistry::new();
858        let claim_check_repositories = crate::ClaimCheckRegistry::new();
859        let cache_repositories = crate::CacheRegistry::new();
860
861        let ctx = CompilationContext {
862            producer_ctx: &pc,
863            rt,
864            languages: &languages,
865            beans: &beans,
866            function_invoker: None,
867            component_ctx,
868            route_id: None,
869            staging_mode: &staging,
870            idempotent_repositories: &idempotent_repositories,
871            claim_check_repositories: &claim_check_repositories,
872            cache_repositories: &cache_repositories,
873            intercept: InterceptRules::default(),
874        };
875
876        // Outer Filter containing an inner Filter that has a stateful Processor.
877        // The outer Segment's lifecycle should contain the innermost handle
878        // (proves recursive flattening through compile_children_segments).
879        let inner_filter = BuilderStep::Filter {
880            predicate: FilterPredicate::new(|_| true),
881            steps: vec![BuilderStep::Processor(OpaqueProcessor(
882                BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
883            ))],
884        };
885
886        let outer_filter = BuilderStep::Filter {
887            predicate: FilterPredicate::new(|_| true),
888            steps: vec![inner_filter],
889        };
890
891        let result = reg.compile_step(outer_filter, 0, &ctx);
892        let compiled = result
893            .expect("compilation should succeed")
894            .expect("should match");
895
896        match compiled {
897            CompiledStep::Segment { lifecycle, .. } => {
898                let handles = lifecycle.expect("outer Segment should have lifecycle handles");
899                assert_eq!(handles.len(), 1, "expected 1 innermost lifecycle handle");
900                assert_eq!(
901                    handles[0].name(),
902                    "deep",
903                    "handle should be from innermost child"
904                );
905            }
906            other => panic!("Expected CompiledStep::Segment, got {other:?}"),
907        }
908    }
909}
910
911#[cfg(test)]
912mod dispatch_tests {
913    use super::*;
914    use crate::lifecycle::adapters::step_resolution::FunctionStagingMode;
915    use camel_api::{BoxProcessor, BoxProcessorExt};
916    use camel_bean::BeanRegistry;
917    use camel_component_api::{
918        ComponentContext, NoOpComponentContext, RuntimeObservability,
919        test_support::NoopRuntimeObservability,
920    };
921    use std::collections::HashMap;
922    use std::sync::Mutex;
923
924    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Stop`.
925    struct ToStopCompiler;
926
927    impl StepCompiler for ToStopCompiler {
928        fn compile(
929            &self,
930            step: BuilderStep,
931            _step_index: usize,
932            _ctx: &CompilationContext,
933            _registry: &StepCompilerRegistry,
934        ) -> Result<CompileOutcome, CamelError> {
935            match step {
936                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Stop)),
937                other => Ok(CompileOutcome::NotHandled(other)),
938            }
939        }
940    }
941
942    /// Compiler that handles `BuilderStep::To` → `CompiledStep::Process`.
943    struct ToProcessCompiler;
944
945    impl StepCompiler for ToProcessCompiler {
946        fn compile(
947            &self,
948            step: BuilderStep,
949            _step_index: usize,
950            _ctx: &CompilationContext,
951            _registry: &StepCompilerRegistry,
952        ) -> Result<CompileOutcome, CamelError> {
953            match step {
954                BuilderStep::To(_) => Ok(CompileOutcome::Matched(CompiledStep::Process {
955                    processor: BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) })),
956                    body_contract: None,
957                    lifecycle: None,
958                    label: None,
959                    kind_hint: SpanKindHint::Internal,
960                    to_uri: None,
961                })),
962                other => Ok(CompileOutcome::NotHandled(other)),
963            }
964        }
965    }
966
967    /// Minimal outcome pipeline for building `Segment` fixtures in tests.
968    #[derive(Clone)]
969    struct NoopPipeline;
970
971    impl camel_api::OutcomePipeline for NoopPipeline {
972        fn clone_box(&self) -> Box<dyn camel_api::OutcomePipeline> {
973            Box::new(NoopPipeline)
974        }
975
976        fn run<'a>(
977            &'a mut self,
978            exchange: camel_api::Exchange,
979        ) -> std::pin::Pin<
980            Box<dyn std::future::Future<Output = camel_api::PipelineOutcome> + Send + 'a>,
981        > {
982            Box::pin(async move { camel_api::PipelineOutcome::Completed(exchange) })
983        }
984    }
985
986    /// Compiler that passes all steps through (never handles any variant).
987    /// Returns `NotHandled(step)` for every input, proving by-value pass-through.
988    struct PassThroughCompiler;
989
990    impl StepCompiler for PassThroughCompiler {
991        fn compile(
992            &self,
993            step: BuilderStep,
994            _step_index: usize,
995            _ctx: &CompilationContext,
996            _registry: &StepCompilerRegistry,
997        ) -> Result<CompileOutcome, CamelError> {
998            Ok(CompileOutcome::NotHandled(step))
999        }
1000    }
1001
1002    /// Shared context builder — avoids repeating the 15-line setup in every test.
1003    #[allow(clippy::too_many_arguments)]
1004    fn ctx<'a>(
1005        pc: &'a ProducerContext,
1006        rt: Arc<dyn RuntimeObservability>,
1007        languages: &'a SharedLanguageRegistry,
1008        beans: &'a Arc<Mutex<BeanRegistry>>,
1009        component_ctx: Arc<dyn ComponentContext>,
1010        staging: &'a FunctionStagingMode,
1011        idempotent_repositories: &'a crate::IdempotentRegistry,
1012        claim_check_repositories: &'a crate::ClaimCheckRegistry,
1013        cache_repositories: &'a crate::CacheRegistry,
1014    ) -> CompilationContext<'a> {
1015        CompilationContext {
1016            producer_ctx: pc,
1017            rt,
1018            languages,
1019            beans,
1020            function_invoker: None,
1021            component_ctx,
1022            route_id: None,
1023            staging_mode: staging,
1024            idempotent_repositories,
1025            claim_check_repositories,
1026            cache_repositories,
1027            intercept: InterceptRules::default(),
1028        }
1029    }
1030
1031    /// Test that the dispatcher respects registration order: the first matching
1032    /// compiler wins even when a later compiler also matches.
1033    #[test]
1034    fn compile_step_preserves_registry_order() {
1035        let pc = ProducerContext::default();
1036        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1037        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1038        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1039        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1040        let staging = FunctionStagingMode::DirectAdd;
1041        let idempotent_repositories = crate::IdempotentRegistry::new();
1042        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1043        let cache_repositories = crate::CacheRegistry::new();
1044
1045        let context = ctx(
1046            &pc,
1047            rt,
1048            &languages,
1049            &beans,
1050            component_ctx,
1051            &staging,
1052            &idempotent_repositories,
1053            &claim_check_repositories,
1054            &cache_repositories,
1055        );
1056
1057        // Register ToStopCompiler FIRST, ToProcessCompiler SECOND.
1058        // Both match BuilderStep::To. The first-registered should win.
1059        let mut reg = StepCompilerRegistry::new();
1060        reg.register(Box::new(ToStopCompiler));
1061        reg.register(Box::new(ToProcessCompiler));
1062
1063        let result = reg
1064            .compile_step(BuilderStep::To("test".into()), 0, &context)
1065            .expect("compilation should succeed")
1066            .expect("should match");
1067
1068        assert!(
1069            matches!(result, CompiledStep::Stop),
1070            "expected ToStopCompiler (first registered) to win, got {result:?}"
1071        );
1072    }
1073
1074    /// Task 1.2 (span-name-enrichment): `set_label` is a no-op on `Stop`.
1075    #[test]
1076    fn set_label_noop_on_stop() {
1077        let mut step = CompiledStep::Stop;
1078        step.set_label(Some("x".into()));
1079        assert!(
1080            matches!(step, CompiledStep::Stop),
1081            "set_label must not change a Stop step, got {step:?}"
1082        );
1083    }
1084
1085    /// Task 1.2 (span-name-enrichment): `compile_step` stamps the outcome with
1086    /// the step's `span_label` (here `to:direct` from `BuilderStep::To`).
1087    #[test]
1088    fn compile_step_stamps_label() {
1089        let pc = ProducerContext::default();
1090        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1091        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1092        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1093        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1094        let staging = FunctionStagingMode::DirectAdd;
1095        let idempotent_repositories = crate::IdempotentRegistry::new();
1096        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1097        let cache_repositories = crate::CacheRegistry::new();
1098
1099        let context = ctx(
1100            &pc,
1101            rt,
1102            &languages,
1103            &beans,
1104            component_ctx,
1105            &staging,
1106            &idempotent_repositories,
1107            &claim_check_repositories,
1108            &cache_repositories,
1109        );
1110
1111        let mut reg = StepCompilerRegistry::new();
1112        reg.register(Box::new(ToProcessCompiler));
1113
1114        let result = reg
1115            .compile_step(BuilderStep::To("direct:x".into()), 0, &context)
1116            .expect("compilation should succeed")
1117            .expect("should match");
1118
1119        match result {
1120            CompiledStep::Process { label, .. } => {
1121                assert_eq!(label.as_deref(), Some("to:direct"));
1122            }
1123            other => panic!("expected Process, got {other:?}"),
1124        }
1125    }
1126
1127    /// Task 1.2 (span-kind-hint): `set_kind_hint` is a no-op on `Stop` and
1128    /// `Segment` — only `Process` carries a kind hint, and a Segment's label
1129    /// must survive the call untouched.
1130    #[test]
1131    fn set_kind_hint_noop_on_stop_and_segment() {
1132        use camel_api::SpanKindHint;
1133
1134        // Stop: the marker produces no step span, so it carries no kind.
1135        let mut step = CompiledStep::Stop;
1136        step.set_kind_hint(SpanKindHint::Client);
1137        assert!(
1138            matches!(step, CompiledStep::Stop),
1139            "set_kind_hint must not change a Stop step, got {step:?}"
1140        );
1141
1142        // Segment: no kind_hint field; its label is unchanged.
1143        let mut step = CompiledStep::Segment {
1144            segment: camel_api::OutcomeSegment::new(Box::new(NoopPipeline)),
1145            body_contract: None,
1146            lifecycle: None,
1147            label: Some("seg".into()),
1148        };
1149        step.set_kind_hint(SpanKindHint::Client);
1150        match step {
1151            CompiledStep::Segment { label, .. } => {
1152                assert_eq!(label.as_deref(), Some("seg"));
1153            }
1154            other => panic!("expected Segment to stay a Segment, got {other:?}"),
1155        }
1156    }
1157
1158    /// Task 1.2 (span-kind-hint): `compile_step` stamps the outcome with the
1159    /// step's `span_kind_hint` (here `kafka` → Producer) alongside the label.
1160    #[test]
1161    fn compile_step_stamps_kind_hint() {
1162        use camel_api::SpanKindHint;
1163
1164        let pc = ProducerContext::default();
1165        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1166        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1167        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1168        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1169        let staging = FunctionStagingMode::DirectAdd;
1170        let idempotent_repositories = crate::IdempotentRegistry::new();
1171        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1172        let cache_repositories = crate::CacheRegistry::new();
1173
1174        let context = ctx(
1175            &pc,
1176            rt,
1177            &languages,
1178            &beans,
1179            component_ctx,
1180            &staging,
1181            &idempotent_repositories,
1182            &claim_check_repositories,
1183            &cache_repositories,
1184        );
1185
1186        let mut reg = StepCompilerRegistry::new();
1187        reg.register(Box::new(ToProcessCompiler));
1188
1189        let result = reg
1190            .compile_step(BuilderStep::To("kafka:orders".into()), 0, &context)
1191            .expect("compilation should succeed")
1192            .expect("should match");
1193
1194        match result {
1195            CompiledStep::Process {
1196                kind_hint, label, ..
1197            } => {
1198                assert_eq!(kind_hint, SpanKindHint::Producer);
1199                assert_eq!(label.as_deref(), Some("to:kafka"));
1200            }
1201            other => panic!("expected Process, got {other:?}"),
1202        }
1203    }
1204
1205    /// Task 1.1 (steplatency): `compile_step` retains the declared To URI —
1206    /// the raw authored text, captured before endpoint resolution and
1207    /// interception — as shared immutable string metadata on the compiled
1208    /// `Process` step.
1209    #[test]
1210    fn compiled_to_step_retains_declared_uri() {
1211        let pc = ProducerContext::default();
1212        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1213        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1214        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1215        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1216        let staging = FunctionStagingMode::DirectAdd;
1217        let idempotent_repositories = crate::IdempotentRegistry::new();
1218        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1219        let cache_repositories = crate::CacheRegistry::new();
1220
1221        let context = ctx(
1222            &pc,
1223            rt,
1224            &languages,
1225            &beans,
1226            component_ctx,
1227            &staging,
1228            &idempotent_repositories,
1229            &claim_check_repositories,
1230            &cache_repositories,
1231        );
1232
1233        let mut reg = StepCompilerRegistry::new();
1234        reg.register(Box::new(ToProcessCompiler));
1235
1236        let result = reg
1237            .compile_step(BuilderStep::To("direct:orders".into()), 0, &context)
1238            .expect("compilation should succeed")
1239            .expect("should match");
1240
1241        match result {
1242            CompiledStep::Process { to_uri, .. } => {
1243                assert_eq!(to_uri, Some(Arc::from("direct:orders")));
1244            }
1245            other => panic!("expected Process, got {other:?}"),
1246        }
1247    }
1248
1249    /// Task 1.1 (steplatency): a non-To process step carries no declared URI.
1250    #[test]
1251    fn compiled_processor_step_has_no_declared_uri() {
1252        use camel_api::OpaqueProcessor;
1253
1254        let pc = ProducerContext::default();
1255        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1256        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1257        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1258        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1259        let staging = FunctionStagingMode::DirectAdd;
1260        let idempotent_repositories = crate::IdempotentRegistry::new();
1261        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1262        let cache_repositories = crate::CacheRegistry::new();
1263
1264        let context = ctx(
1265            &pc,
1266            rt,
1267            &languages,
1268            &beans,
1269            component_ctx,
1270            &staging,
1271            &idempotent_repositories,
1272            &claim_check_repositories,
1273            &cache_repositories,
1274        );
1275
1276        let mut reg = StepCompilerRegistry::new();
1277        reg.register(Box::new(super::core::CoreCompiler));
1278
1279        let result = reg
1280            .compile_step(
1281                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(|ex| {
1282                    Box::pin(async move { Ok(ex) })
1283                }))),
1284                0,
1285                &context,
1286            )
1287            .expect("compilation should succeed")
1288            .expect("should match");
1289
1290        match result {
1291            CompiledStep::Process { to_uri, .. } => {
1292                assert_eq!(to_uri, None);
1293            }
1294            other => panic!("expected Process, got {other:?}"),
1295        }
1296    }
1297
1298    /// Test that when no compiler handles a variant, the dispatcher returns
1299    /// `Ok(None)` rather than an error.
1300    #[test]
1301    fn compile_step_unhandled_returns_ok_none() {
1302        let pc = ProducerContext::default();
1303        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1304        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1305        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1306        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1307        let staging = FunctionStagingMode::DirectAdd;
1308        let idempotent_repositories = crate::IdempotentRegistry::new();
1309        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1310        let cache_repositories = crate::CacheRegistry::new();
1311
1312        let context = ctx(
1313            &pc,
1314            rt,
1315            &languages,
1316            &beans,
1317            component_ctx,
1318            &staging,
1319            &idempotent_repositories,
1320            &claim_check_repositories,
1321            &cache_repositories,
1322        );
1323
1324        // Register a compiler that only handles BuilderStep::To.
1325        let mut reg = StepCompilerRegistry::new();
1326        reg.register(Box::new(ToStopCompiler));
1327
1328        // Send a BuilderStep::Log variant — ToStopCompiler does not match it.
1329        let result = reg.compile_step(
1330            BuilderStep::Log {
1331                level: camel_processor::LogLevel::Info,
1332                message: "unhandled".into(),
1333            },
1334            0,
1335            &context,
1336        );
1337
1338        assert!(
1339            matches!(result, Ok(None)),
1340            "expected Ok(None), got {result:?}"
1341        );
1342    }
1343
1344    /// Test that `NotHandled(step)` passes the step by-value to the next
1345    /// compiler. Compiler N returns `NotHandled(step)` for a variant that
1346    /// compiler N+1 handles — proving the step is not dropped or replaced.
1347    #[test]
1348    fn compile_step_nothandled_passes_step_intact_to_next_compiler() {
1349        let pc = ProducerContext::default();
1350        let rt: Arc<dyn RuntimeObservability> = Arc::new(NoopRuntimeObservability);
1351        let languages: SharedLanguageRegistry = Arc::new(Mutex::new(HashMap::new()));
1352        let beans: Arc<Mutex<BeanRegistry>> = Arc::new(Mutex::new(BeanRegistry::new()));
1353        let component_ctx: Arc<dyn ComponentContext> = Arc::new(NoOpComponentContext);
1354        let staging = FunctionStagingMode::DirectAdd;
1355        let idempotent_repositories = crate::IdempotentRegistry::new();
1356        let claim_check_repositories = crate::ClaimCheckRegistry::new();
1357        let cache_repositories = crate::CacheRegistry::new();
1358
1359        let context = ctx(
1360            &pc,
1361            rt,
1362            &languages,
1363            &beans,
1364            component_ctx,
1365            &staging,
1366            &idempotent_repositories,
1367            &claim_check_repositories,
1368            &cache_repositories,
1369        );
1370
1371        // Register PassThroughCompiler FIRST (never handles anything),
1372        // ToStopCompiler SECOND (handles BuilderStep::To).
1373        let mut reg = StepCompilerRegistry::new();
1374        reg.register(Box::new(PassThroughCompiler));
1375        reg.register(Box::new(ToStopCompiler));
1376
1377        let result = reg
1378            .compile_step(BuilderStep::To("passthrough".into()), 0, &context)
1379            .expect("compilation should succeed")
1380            .expect("should match");
1381
1382        assert!(
1383            matches!(result, CompiledStep::Stop),
1384            "expected ToStopCompiler (N+1) to win after pass-through, got {result:?}"
1385        );
1386    }
1387}