Skip to main content

camel_core/lifecycle/application/
route_definition.rs

1// lifecycle/application/route_definition.rs
2// Route definition and builder-step types. Route (compiled artifact) lives in adapters.
3
4use std::sync::Arc;
5
6use camel_api::UnitOfWorkConfig;
7use camel_api::circuit_breaker::CircuitBreakerConfig;
8use camel_api::error_handler::ErrorHandlerConfig;
9use camel_api::loop_eip::LoopConfig;
10use camel_api::security_policy::SecurityPolicyConfig;
11use camel_api::{
12    AggregatorConfig, FilterPredicate, MulticastConfig, OpaqueProcessor, ResequencePolicyConfig,
13    SpanKindHint, SplitterConfig,
14};
15use camel_auth::TokenAuthenticator;
16use camel_component_api::ConcurrencyModel;
17
18/// An unresolved when-clause: predicate + nested steps for the sub-pipeline.
19#[derive(Clone)]
20pub struct WhenStep {
21    pub predicate: FilterPredicate,
22    pub steps: Vec<BuilderStep>,
23}
24
25impl std::fmt::Debug for WhenStep {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("WhenStep")
28            .field("predicate", &self.predicate)
29            .field("steps", &self.steps)
30            .finish()
31    }
32}
33
34pub use camel_api::declarative::{LanguageExpressionDef, ValueSourceDef};
35
36/// Declarative `when` clause resolved later by the runtime.
37#[derive(Debug, Clone)]
38pub struct DeclarativeWhenStep {
39    pub predicate: LanguageExpressionDef,
40    pub steps: Vec<BuilderStep>,
41}
42
43/// Builder struct for a single `doCatch` clause in the declarative pipeline.
44#[derive(Debug, Clone)]
45pub struct DoTryCatchClauseBuilder {
46    pub exception: Option<Vec<String>>,
47    pub when: Option<LanguageExpressionDef>,
48    pub on_when: Option<LanguageExpressionDef>,
49    pub disposition: camel_api::error_handler::ExceptionDisposition,
50    pub steps: Vec<BuilderStep>,
51}
52
53/// Builder struct for the `doFinally` block in the declarative pipeline.
54#[derive(Debug, Clone)]
55pub struct DoTryFinallyBuilder {
56    pub on_when: Option<LanguageExpressionDef>,
57    pub steps: Vec<BuilderStep>,
58}
59
60/// A single `doCatch` clause in the programmatic (builder) pipeline.
61#[derive(Clone)]
62pub struct DoTryCatchClause {
63    pub matcher: camel_processor::CatchMatcher,
64    pub on_when: Option<camel_api::PredicateSource>,
65    pub disposition: camel_api::error_handler::ExceptionDisposition,
66    pub steps: Vec<BuilderStep>,
67}
68
69impl std::fmt::Debug for DoTryCatchClause {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("DoTryCatchClause")
72            .field("matcher", &"<compiled>")
73            .field("on_when", &self.on_when.is_some())
74            .field("disposition", &self.disposition)
75            .field("steps", &self.steps)
76            .finish()
77    }
78}
79
80/// The `doFinally` block in the programmatic (builder) pipeline.
81#[derive(Clone)]
82pub struct DoTryFinallyClause {
83    pub on_when: Option<camel_api::PredicateSource>,
84    pub steps: Vec<BuilderStep>,
85}
86
87impl std::fmt::Debug for DoTryFinallyClause {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("DoTryFinallyClause")
90            .field("on_when", &self.on_when.is_some())
91            .field("steps", &self.steps)
92            .finish()
93    }
94}
95
96/// Opaque wrapper for a programmatic streaming-split expression.
97///
98/// `camel_api::StreamingSplitExpression` is a bare `Arc<dyn Fn …>` and has no
99/// `Debug` implementation; wrapping it keeps `BuilderStep`'s derived `Debug`
100/// while rendering the closure as opaque, mirroring `OpaqueProcessor`.
101#[derive(Clone)]
102pub struct StreamSplitExpression(pub camel_api::StreamingSplitExpression);
103
104impl std::fmt::Debug for StreamSplitExpression {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.write_str("StreamingSplitExpression(...)")
107    }
108}
109
110impl From<camel_api::StreamingSplitExpression> for StreamSplitExpression {
111    fn from(expression: camel_api::StreamingSplitExpression) -> Self {
112        Self(expression)
113    }
114}
115
116/// A step in an unresolved route definition.
117#[derive(Debug, Clone)]
118pub enum BuilderStep {
119    /// A pre-built Tower processor service.
120    Processor(OpaqueProcessor),
121    /// A destination URI — resolved at start time by CamelContext.
122    To(String),
123    /// A stop step that halts processing immediately.
124    Stop,
125    /// A static log step.
126    Log {
127        level: camel_processor::LogLevel,
128        message: String,
129    },
130    /// Declarative set_header (literal or language-based value), resolved at route-add time.
131    DeclarativeSetHeader {
132        key: String,
133        value: ValueSourceDef,
134    },
135    /// Declarative set_header_if_absent (internal-only), resolved at route-add time.
136    DeclarativeSetHeaderIfAbsent {
137        key: String,
138        value: ValueSourceDef,
139    },
140    /// Declarative remove_header, resolved at route-add time.
141    DeclarativeRemoveHeader {
142        key: String,
143    },
144    DeclarativeSetProperty {
145        key: String,
146        value_source: ValueSourceDef,
147    },
148    /// Declarative set_body (literal or language-based value), resolved at route-add time.
149    DeclarativeSetBody {
150        value: ValueSourceDef,
151    },
152    /// Declarative filter using a language predicate, resolved at route-add time.
153    DeclarativeFilter {
154        predicate: LanguageExpressionDef,
155        steps: Vec<BuilderStep>,
156    },
157    /// Declarative choice/when/otherwise using language predicates, resolved at route-add time.
158    DeclarativeChoice {
159        whens: Vec<DeclarativeWhenStep>,
160        otherwise: Option<Vec<BuilderStep>>,
161    },
162    /// Declarative script step. The language must support mutating expressions,
163    /// and the step writes the Exchange back.
164    DeclarativeScript {
165        expression: LanguageExpressionDef,
166    },
167    DeclarativeFunction {
168        definition: camel_api::FunctionDefinition,
169    },
170    /// Declarative split using a language expression, resolved at route-add time.
171    DeclarativeSplit {
172        expression: LanguageExpressionDef,
173        aggregation: camel_api::splitter::AggregationStrategy,
174        parallel: bool,
175        parallel_limit: Option<usize>,
176        /// Threshold above which split fragments start new traces (None =
177        /// `DEFAULT_TRACE_ITEM_THRESHOLD` applies at compile time).
178        trace_item_threshold: Option<usize>,
179        stop_on_exception: bool,
180        steps: Vec<BuilderStep>,
181    },
182    /// Declarative stream split using a streaming split expression, resolved at route-add time.
183    DeclarativeStreamSplit {
184        stream_config: camel_api::StreamSplitConfig,
185        aggregation: camel_api::splitter::AggregationStrategy,
186        stop_on_exception: bool,
187        steps: Vec<BuilderStep>,
188    },
189    /// Programmatic stream split: the caller supplies the fragment expression directly
190    /// (e.g. built from `camel_processor::stream_codec` or `zip_splitter`). The expression
191    /// encapsulates the stream configuration; the compiled segment applies the same
192    /// aggregation and stop-on-exception semantics as the declarative form.
193    StreamSplit {
194        expression: StreamSplitExpression,
195        aggregation: camel_api::splitter::AggregationStrategy,
196        stop_on_exception: bool,
197        steps: Vec<BuilderStep>,
198    },
199    DeclarativeDynamicRouter {
200        expression: LanguageExpressionDef,
201        uri_delimiter: String,
202        cache_size: i32,
203        ignore_invalid_endpoints: bool,
204        max_iterations: usize,
205    },
206    DeclarativeRoutingSlip {
207        expression: LanguageExpressionDef,
208        uri_delimiter: String,
209        cache_size: i32,
210        ignore_invalid_endpoints: bool,
211    },
212    /// A Splitter sub-pipeline: config + nested steps to execute per fragment.
213    Split {
214        config: SplitterConfig,
215        steps: Vec<BuilderStep>,
216    },
217    /// An Aggregator step: collects exchanges by correlation key, emits when complete.
218    Aggregate {
219        config: AggregatorConfig,
220    },
221    /// A Filter sub-pipeline: predicate + nested steps executed only when predicate is true.
222    Filter {
223        predicate: FilterPredicate,
224        steps: Vec<BuilderStep>,
225    },
226    /// A Choice step: evaluates when-clauses in order, routes to the first match.
227    /// If no when matches, the optional otherwise branch is used.
228    Choice {
229        whens: Vec<WhenStep>,
230        otherwise: Option<Vec<BuilderStep>>,
231    },
232    /// A WireTap step: sends a clone of the exchange to a tap endpoint (fire-and-forget).
233    WireTap {
234        uri: String,
235    },
236    /// A Multicast step: sends the same exchange to multiple destinations.
237    Multicast {
238        steps: Vec<BuilderStep>,
239        config: MulticastConfig,
240    },
241    /// Declarative log step with a language-evaluated message, resolved at route-add time.
242    DeclarativeLog {
243        level: camel_processor::LogLevel,
244        message: ValueSourceDef,
245    },
246    /// Bean invocation step — resolved at route-add time.
247    Bean {
248        name: String,
249        method: String,
250    },
251    /// Script step: executes a script that can mutate the exchange.
252    /// The script has access to `headers`, `properties`, and `body`.
253    Script {
254        language: String,
255        script: String,
256    },
257    /// Throttle step: rate limiting with configurable behavior when limit exceeded.
258    Throttle {
259        config: camel_api::ThrottlerConfig,
260        steps: Vec<BuilderStep>,
261    },
262    /// LoadBalance step: distributes exchanges across multiple endpoints using a strategy.
263    LoadBalance {
264        config: camel_api::LoadBalancerConfig,
265        steps: Vec<BuilderStep>,
266    },
267    /// DynamicRouter step: routes exchanges dynamically based on expression evaluation.
268    DynamicRouter {
269        config: camel_api::DynamicRouterConfig,
270    },
271    RoutingSlip {
272        config: camel_api::RoutingSlipConfig,
273    },
274    RecipientList {
275        config: camel_api::recipient_list::RecipientListConfig,
276    },
277    DeclarativeRecipientList {
278        expression: LanguageExpressionDef,
279        delimiter: String,
280        parallel: bool,
281        parallel_limit: Option<usize>,
282        stop_on_exception: bool,
283        aggregation: String,
284    },
285    Delay {
286        config: camel_api::DelayConfig,
287    },
288    /// Runtime loop with closure-based predicate (programmatic DSL).
289    Loop {
290        config: LoopConfig,
291        steps: Vec<BuilderStep>,
292    },
293    /// Declarative loop with optional language-based while predicate (YAML DSL).
294    DeclarativeLoop {
295        count: Option<usize>,
296        while_predicate: Option<LanguageExpressionDef>,
297        steps: Vec<BuilderStep>,
298        max_iterations: Option<usize>,
299    },
300    /// EIP-7 enrich: synchronous content enrichment via a resolved producer.
301    Enrich {
302        uri: String,
303        strategy: Option<String>,
304        timeout_ms: Option<u64>,
305    },
306    /// EIP-7 pollEnrich: blocking poll of a PollingConsumer with timeout.
307    PollEnrich {
308        uri: String,
309        strategy: Option<String>,
310        timeout_ms: Option<u64>,
311    },
312    /// Validate step: evaluates a language expression as predicate.
313    /// Exchange passes if predicate returns true; else CamelError::ValidationError.
314    Validate {
315        predicate: LanguageExpressionDef,
316    },
317    /// Claim Check step (EIP). Transforms the exchange body to/from a
318    /// `ClaimCheckRepository` by key. Process-mode, no child pipeline.
319    /// `filter` enables selective merge-back of body/headers during checkout.
320    ClaimCheck {
321        repository: String,
322        operation: String,
323        key: LanguageExpressionDef,
324        filter: Option<String>,
325    },
326    /// Sampling step (EIP). Passes 1 of every N exchanges (counter-based,
327    /// deterministic). Non-sampled exchanges get CamelStop=true (drop semantics).
328    /// Process-mode, stateless. No StepLifecycle — counter is route-scoped.
329    Sampling {
330        period: usize,
331    },
332    /// Sort step (EIP). Orders a body array by extracting a sort key
333    /// from each element via a language expression. Process-mode, stateless.
334    Sort {
335        expression: LanguageExpressionDef,
336        reverse: bool,
337    },
338    /// Idempotent Consumer step (EIP). Wraps a child sub-pipeline that runs
339    /// only when the message-id is NOT present in the named repository.
340    /// Compiled to a `IdempotentConsumerSegment` (OutcomePipeline, segment-mode).
341    IdempotentConsumer {
342        repository: String,
343        expression: LanguageExpressionDef,
344        steps: Vec<BuilderStep>,
345        eager: bool,
346        remove_on_failure: bool,
347    },
348    /// Cache step (EIP). Wraps a child sub-pipeline that runs only on cache
349    /// miss; on hit the stored body is reconstructed and the child is skipped.
350    /// Compiled to a `CacheService` (OutcomePipeline, segment-mode).
351    Cache {
352        repository: Option<String>,
353        key: LanguageExpressionDef,
354        ttl: Option<String>,
355        max_entry_bytes: Option<usize>,
356        /// Coalesce concurrent misses on the same key into one `on_miss` run.
357        coalesce_misses: bool,
358        on_miss: Vec<BuilderStep>,
359    },
360    /// Cache Invalidate step (EIP). Removes a single cache entry by exact key
361    /// (`key`) or every entry under a namespace prefix (`key_prefix`) — exactly
362    /// one of the two. Compiled to a `CacheInvalidateService` (OutcomePipeline,
363    /// segment-mode).
364    CacheInvalidate {
365        repository: Option<String>,
366        key: Option<LanguageExpressionDef>,
367        key_prefix: Option<LanguageExpressionDef>,
368    },
369    /// Cache Clear step (EIP). Removes all entries from the repository.
370    /// Compiled to a `CacheClearService` (OutcomePipeline, segment-mode).
371    CacheClear {
372        repository: Option<String>,
373    },
374    /// Cache Stats step (EIP). Emits the repository stats as a JSON body.
375    /// Compiled to a `CacheStatsService` (OutcomePipeline, segment-mode).
376    CacheStats {
377        repository: Option<String>,
378    },
379    /// Cache Peek Stale step (EIP). Serves a stale (post-expiry) cache entry.
380    /// Compiled to a `CachePeekStaleService` (OutcomePipeline, segment-mode).
381    CachePeekStale {
382        repository: Option<String>,
383        key: LanguageExpressionDef,
384        on_miss: camel_processor::PeekStaleMissPolicy,
385    },
386    /// Declarative doTry/doCatch/doFinally, resolved at route-add time.
387    DeclarativeDoTry {
388        try_steps: Vec<BuilderStep>,
389        catch: Vec<DoTryCatchClauseBuilder>,
390        finally: Option<DoTryFinallyBuilder>,
391    },
392    /// Programmatic doTry/doCatch/doFinally EIP, compiled to a `DoTrySegment`
393    /// (outcome-aware).
394    DoTry {
395        try_steps: Vec<BuilderStep>,
396        catches: Vec<DoTryCatchClause>,
397        finally: Option<DoTryFinallyClause>,
398    },
399    /// Resequencer EIP: resequences exchanges by sequence number.
400    /// Must be a top-level step (not nested inside structural EIPs).
401    Resequence {
402        policy_config: ResequencePolicyConfig,
403    },
404}
405
406impl BuilderStep {
407    /// Stable span name for this step (span-name-enrichment, task 1.2).
408    ///
409    /// `To` yields `to:{scheme}` where scheme is the AUTHORED URI before the
410    /// first `:` — a pure string split, no endpoint resolution, so a `SkipTo`
411    /// interception never rewrites the label. EIP variants yield their
412    /// kebab-case EIP name; declarative twins share the label of their
413    /// programmatic sibling (`DeclarativeSplit` and `Split` are both
414    /// `"split"`). Genuinely anonymous variants (opaque processors, the Stop
415    /// marker) yield `None` — spans fall back to the positional
416    /// `step-{index}` name.
417    ///
418    /// The match is exhaustive by design (no catch-all) so a future variant
419    /// forces a label decision here.
420    pub(crate) fn span_label(&self) -> Option<String> {
421        match self {
422            // Anonymous.
423            Self::Processor(_) | Self::Stop => None,
424
425            Self::To(uri) => uri.contains(':').then(|| {
426                let scheme = uri.split(':').next().unwrap_or_default();
427                format!("to:{scheme}")
428            }),
429
430            Self::Log { .. } | Self::DeclarativeLog { .. } => Some("log".into()),
431
432            Self::DeclarativeSetHeader { .. } => Some("set-header".into()),
433            Self::DeclarativeSetHeaderIfAbsent { .. } => Some("set-header-if-absent".into()),
434            Self::DeclarativeRemoveHeader { .. } => Some("remove-header".into()),
435            Self::DeclarativeSetProperty { .. } => Some("set-property".into()),
436            Self::DeclarativeSetBody { .. } => Some("set-body".into()),
437
438            Self::DeclarativeFilter { .. } | Self::Filter { .. } => Some("filter".into()),
439            Self::DeclarativeChoice { .. } | Self::Choice { .. } => Some("choice".into()),
440            Self::DeclarativeScript { .. } | Self::Script { .. } => Some("script".into()),
441            Self::DeclarativeFunction { .. } => Some("function".into()),
442
443            Self::DeclarativeSplit { .. }
444            | Self::DeclarativeStreamSplit { .. }
445            | Self::StreamSplit { .. }
446            | Self::Split { .. } => Some("split".into()),
447
448            Self::DeclarativeDynamicRouter { .. } | Self::DynamicRouter { .. } => {
449                Some("dynamic-router".into())
450            }
451            Self::DeclarativeRoutingSlip { .. } | Self::RoutingSlip { .. } => {
452                Some("routing-slip".into())
453            }
454            Self::DeclarativeRecipientList { .. } | Self::RecipientList { .. } => {
455                Some("recipient-list".into())
456            }
457
458            Self::Aggregate { .. } => Some("aggregate".into()),
459            Self::WireTap { .. } => Some("wire-tap".into()),
460            Self::Multicast { .. } => Some("multicast".into()),
461            Self::Bean { .. } => Some("bean".into()),
462            Self::Throttle { .. } => Some("throttle".into()),
463            Self::LoadBalance { .. } => Some("load-balance".into()),
464            Self::Delay { .. } => Some("delay".into()),
465            Self::Loop { .. } | Self::DeclarativeLoop { .. } => Some("loop".into()),
466            Self::Enrich { .. } => Some("enrich".into()),
467            Self::PollEnrich { .. } => Some("poll-enrich".into()),
468            Self::Validate { .. } => Some("validate".into()),
469            Self::ClaimCheck { .. } => Some("claim-check".into()),
470            Self::Sampling { .. } => Some("sampling".into()),
471            Self::Sort { .. } => Some("sort".into()),
472            Self::IdempotentConsumer { .. } => Some("idempotent-consumer".into()),
473            Self::Cache { .. } => Some("cache".into()),
474            Self::CacheInvalidate { .. } => Some("cache-invalidate".into()),
475            Self::CacheClear { .. } => Some("cache-clear".into()),
476            Self::CacheStats { .. } => Some("cache-stats".into()),
477            Self::CachePeekStale { .. } => Some("cache-peek-stale".into()),
478            Self::DeclarativeDoTry { .. } | Self::DoTry { .. } => Some("do-try".into()),
479            Self::Resequence { .. } => Some("resequence".into()),
480        }
481    }
482
483    /// Declared send URI retained on the compiled step for step-latency
484    /// attribution (steplatency task 1.1). Only `To` carries one: the RAW
485    /// AUTHORED URI, extracted as a pure string copy with no endpoint
486    /// resolution, so a `SkipTo` interception never rewrites it — the
487    /// compiled step keeps the URI the route author wrote, not the
488    /// intercept target. Stored as shared immutable text (`Arc<str>`).
489    /// Every other variant yields `None`.
490    ///
491    /// The match is exhaustive by design (no catch-all) so a future
492    /// variant forces a declared-URI decision here.
493    pub(crate) fn to_uri_metadata(&self) -> Option<Arc<str>> {
494        match self {
495            Self::To(uri) => Some(Arc::from(uri.as_str())),
496
497            // rc-4mz7: every remaining variant is named explicitly — no
498            // wildcard. A new BuilderStep variant fails to compile until it
499            // is classified here.
500            Self::Processor(..)
501            | Self::Stop
502            | Self::Log { .. }
503            | Self::DeclarativeSetHeader { .. }
504            | Self::DeclarativeSetHeaderIfAbsent { .. }
505            | Self::DeclarativeRemoveHeader { .. }
506            | Self::DeclarativeSetProperty { .. }
507            | Self::DeclarativeSetBody { .. }
508            | Self::DeclarativeFilter { .. }
509            | Self::DeclarativeChoice { .. }
510            | Self::DeclarativeScript { .. }
511            | Self::DeclarativeFunction { .. }
512            | Self::DeclarativeSplit { .. }
513            | Self::DeclarativeStreamSplit { .. }
514            | Self::StreamSplit { .. }
515            | Self::DeclarativeDynamicRouter { .. }
516            | Self::DeclarativeRoutingSlip { .. }
517            | Self::Split { .. }
518            | Self::Aggregate { .. }
519            | Self::Filter { .. }
520            | Self::Choice { .. }
521            | Self::Multicast { .. }
522            | Self::DeclarativeLog { .. }
523            | Self::Bean { .. }
524            | Self::Script { .. }
525            | Self::Throttle { .. }
526            | Self::LoadBalance { .. }
527            | Self::DynamicRouter { .. }
528            | Self::RoutingSlip { .. }
529            | Self::RecipientList { .. }
530            | Self::DeclarativeRecipientList { .. }
531            | Self::Delay { .. }
532            | Self::Loop { .. }
533            | Self::DeclarativeLoop { .. }
534            | Self::Enrich { .. }
535            | Self::PollEnrich { .. }
536            | Self::WireTap { .. }
537            | Self::Validate { .. }
538            | Self::ClaimCheck { .. }
539            | Self::Sampling { .. }
540            | Self::Sort { .. }
541            | Self::IdempotentConsumer { .. }
542            | Self::Cache { .. }
543            | Self::CacheInvalidate { .. }
544            | Self::CacheClear { .. }
545            | Self::CacheStats { .. }
546            | Self::CachePeekStale { .. }
547            | Self::DeclarativeDoTry { .. }
548            | Self::DoTry { .. }
549            | Self::Resequence { .. } => None,
550        }
551    }
552
553    /// Span kind hint for this step's step span (span-kind-hint, task 1.2).
554    ///
555    /// Endpoint-bearing variants (`To`, `Enrich`, `PollEnrich`, `WireTap`)
556    /// classify the AUTHORED URI scheme — the text before the first `:`,
557    /// compared with `eq_ignore_ascii_case`, no endpoint resolution — so a
558    /// `SkipTo` interception never rewrites the kind. Messaging broker
559    /// schemes map to `Producer` (async, one-way send) and synchronous
560    /// outbound protocols to `Client`; every other scheme and scheme-less
561    /// URIs stay `Internal` route processing.
562    ///
563    /// Every non-endpoint variant is internal route processing. The match is
564    /// exhaustive by design (no `_` wildcard): adding a `BuilderStep` variant
565    /// without an explicit span-kind decision here is a compile error
566    /// (rc-4mz7). `BuilderStep` is defined in this crate and carries no
567    /// `#[non_exhaustive]`, so same-crate exhaustiveness holds.
568    pub(crate) fn span_kind_hint(&self) -> SpanKindHint {
569        match self {
570            // Endpoint-bearing variants: classify by the authored scheme.
571            Self::To(uri)
572            | Self::Enrich { uri, .. }
573            | Self::PollEnrich { uri, .. }
574            | Self::WireTap { uri, .. } => uri_span_kind(uri),
575
576            // rc-4mz7: every remaining variant is named explicitly — no
577            // wildcard. A new BuilderStep variant fails to compile until it
578            // is classified here.
579            Self::Processor(..)
580            | Self::Stop
581            | Self::Log { .. }
582            | Self::DeclarativeSetHeader { .. }
583            | Self::DeclarativeSetHeaderIfAbsent { .. }
584            | Self::DeclarativeRemoveHeader { .. }
585            | Self::DeclarativeSetProperty { .. }
586            | Self::DeclarativeSetBody { .. }
587            | Self::DeclarativeFilter { .. }
588            | Self::DeclarativeChoice { .. }
589            | Self::DeclarativeScript { .. }
590            | Self::DeclarativeFunction { .. }
591            | Self::DeclarativeSplit { .. }
592            | Self::DeclarativeStreamSplit { .. }
593            | Self::StreamSplit { .. }
594            | Self::DeclarativeDynamicRouter { .. }
595            | Self::DeclarativeRoutingSlip { .. }
596            | Self::Split { .. }
597            | Self::Aggregate { .. }
598            | Self::Filter { .. }
599            | Self::Choice { .. }
600            | Self::Multicast { .. }
601            | Self::DeclarativeLog { .. }
602            | Self::Bean { .. }
603            | Self::Script { .. }
604            | Self::Throttle { .. }
605            | Self::LoadBalance { .. }
606            | Self::DynamicRouter { .. }
607            | Self::RoutingSlip { .. }
608            | Self::RecipientList { .. }
609            | Self::DeclarativeRecipientList { .. }
610            | Self::Delay { .. }
611            | Self::Loop { .. }
612            | Self::DeclarativeLoop { .. }
613            | Self::Validate { .. }
614            | Self::ClaimCheck { .. }
615            | Self::Sampling { .. }
616            | Self::Sort { .. }
617            | Self::IdempotentConsumer { .. }
618            | Self::Cache { .. }
619            | Self::CacheInvalidate { .. }
620            | Self::CacheClear { .. }
621            | Self::CacheStats { .. }
622            | Self::CachePeekStale { .. }
623            | Self::DeclarativeDoTry { .. }
624            | Self::DoTry { .. }
625            | Self::Resequence { .. } => SpanKindHint::Internal,
626        }
627    }
628}
629
630/// Classify an AUTHORED endpoint URI by its scheme — the text before the
631/// first `:`, compared with `eq_ignore_ascii_case`, no endpoint resolution.
632///
633/// Messaging broker schemes map to `Producer` (async, one-way send) and
634/// synchronous outbound protocols to `Client`; every other scheme and
635/// scheme-less URIs stay `Internal` — the kind is never guessed.
636fn uri_span_kind(uri: &str) -> SpanKindHint {
637    /// Broker-style destinations: an async, one-way send.
638    const PRODUCER_SCHEMES: [&str; 5] = ["kafka", "jms", "activemq", "artemis", "mqtt"];
639    /// Synchronous outbound request/response protocols.
640    const CLIENT_SCHEMES: [&str; 12] = [
641        "http",
642        "https",
643        "grpc",
644        "grpcs",
645        "ws",
646        "redis",
647        "opensearch",
648        "sql",
649        "surrealdb",
650        "cxf",
651        "llm",
652        "mcp",
653    ];
654
655    let scheme = uri.split(':').next().unwrap_or_default();
656    if PRODUCER_SCHEMES
657        .iter()
658        .any(|s| s.eq_ignore_ascii_case(scheme))
659    {
660        SpanKindHint::Producer
661    } else if CLIENT_SCHEMES
662        .iter()
663        .any(|s| s.eq_ignore_ascii_case(scheme))
664    {
665        SpanKindHint::Client
666    } else {
667        SpanKindHint::Internal
668    }
669}
670
671/// An unresolved route definition. "to" URIs have not been resolved to producers yet.
672pub struct RouteDefinition {
673    pub(crate) from_uri: String,
674    pub(crate) steps: Vec<BuilderStep>,
675    /// Optional per-route error handler config. Takes precedence over the global one.
676    pub(crate) error_handler: Option<ErrorHandlerConfig>,
677    /// Optional circuit breaker config. Applied between error handler and step pipeline.
678    pub(crate) circuit_breaker: Option<CircuitBreakerConfig>,
679    /// Circuit breaker fallback sub-pipeline, as UNRESOLVED steps.
680    ///
681    /// Sibling of [`RouteDefinition::circuit_breaker`]: the DSL layers thread
682    /// `Vec<BuilderStep>` here (same rule as `cache_peek_stale.on_miss`) and
683    /// camel-core compiles it via the `StepCompilerRegistry` when the route is
684    /// compiled. The resolved `CircuitBreakerConfig.fallback` (`BoxProcessor`)
685    /// stays `None` until that compile — the DSL never constructs processors.
686    pub(crate) circuit_breaker_fallback: Vec<BuilderStep>,
687    pub(crate) security_policy: Option<SecurityPolicyConfig>,
688    /// Optional token authenticator for validating JWT/OAuth tokens.
689    pub(crate) security_authenticator: Option<Arc<dyn TokenAuthenticator>>,
690    /// Named authentication providers for this route, injected into the
691    /// consumer `SecurityContext` so Phase-2 transports can resolve them.
692    pub(crate) provider_registry: Option<Arc<camel_auth::ProviderRegistry>>,
693    /// Declared provider name for plan compilation (`security_policy.provider`
694    /// in the DSL). Resolved against the provider registry at staging time;
695    /// a missing name fails compilation instead of downgrading to `Public`.
696    pub(crate) security_provider: Option<String>,
697    /// Route-level audience override for plan compilation. When present the
698    /// compiled `RouteSecurityPlan.audience_binding` uses these audiences
699    /// (issuers still come from the provider); when absent the resolved
700    /// provider's binding is copied verbatim.
701    pub(crate) security_audiences: Option<Vec<String>>,
702    /// Optional Unit of Work config for in-flight tracking and completion hooks.
703    pub(crate) unit_of_work: Option<UnitOfWorkConfig>,
704    /// User override for the consumer's concurrency model. `None` means
705    /// "use whatever the consumer declares".
706    pub(crate) concurrency: Option<ConcurrencyModel>,
707    /// Unique identifier for this route. Required.
708    pub(crate) route_id: String,
709    /// Whether this route should start automatically when the context starts.
710    pub(crate) auto_startup: bool,
711    /// Order in which routes are started. Lower values start first.
712    pub(crate) startup_order: i32,
713    pub(crate) source_hash: Option<u64>,
714}
715
716impl RouteDefinition {
717    /// Create a new route definition with the required route ID.
718    pub fn new(from_uri: impl Into<String>, steps: Vec<BuilderStep>) -> Self {
719        Self {
720            from_uri: from_uri.into(),
721            steps,
722            error_handler: None,
723            circuit_breaker: None,
724            circuit_breaker_fallback: Vec::new(),
725            security_policy: None,
726            security_authenticator: None,
727            provider_registry: None,
728            security_provider: None,
729            security_audiences: None,
730            unit_of_work: None,
731            concurrency: None,
732            route_id: String::new(), // Will be set by with_route_id()
733            auto_startup: true,
734            startup_order: 1000,
735            source_hash: None,
736        }
737    }
738
739    /// The source endpoint URI.
740    pub fn from_uri(&self) -> &str {
741        &self.from_uri
742    }
743
744    /// The steps in this route definition.
745    pub fn steps(&self) -> &[BuilderStep] {
746        &self.steps
747    }
748
749    pub fn circuit_breaker_fallback(&self) -> &[BuilderStep] {
750        &self.circuit_breaker_fallback
751    }
752
753    /// Transform the step list, consuming and returning self.
754    /// Used for post-parse instrumentation (e.g. benchmark timing injection
755    /// around `To` steps). The field stays private; callers rebuild via this
756    /// consuming method rather than holding a `&mut` alias.
757    pub fn map_steps(mut self, f: impl FnOnce(Vec<BuilderStep>) -> Vec<BuilderStep>) -> Self {
758        self.steps = f(self.steps);
759        self
760    }
761
762    /// Set a per-route error handler, overriding the global one.
763    pub fn with_error_handler(mut self, config: ErrorHandlerConfig) -> Self {
764        self.error_handler = Some(config);
765        self
766    }
767
768    /// Get the route-level error handler config, if set.
769    pub fn error_handler_config(&self) -> Option<&ErrorHandlerConfig> {
770        self.error_handler.as_ref()
771    }
772
773    /// Set a circuit breaker for this route.
774    pub fn with_circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
775        self.circuit_breaker = Some(config);
776        self
777    }
778
779    /// Set the circuit breaker fallback sub-pipeline (unresolved steps).
780    ///
781    /// Mirror of [`RouteDefinition::with_circuit_breaker`]: the steps are
782    /// compiled by camel-core at route-compile time (registry monopoly) and
783    /// attached to the resolved `CircuitBreakerConfig.fallback`. Empty when no
784    /// CB is configured or the CB has no fallback.
785    pub fn with_circuit_breaker_fallback(mut self, steps: Vec<BuilderStep>) -> Self {
786        self.circuit_breaker_fallback = steps;
787        self
788    }
789
790    /// Set a security policy for this route.
791    pub fn with_security_policy(mut self, config: SecurityPolicyConfig) -> Self {
792        self.security_policy = Some(config);
793        self
794    }
795
796    /// Set a token authenticator for this route.
797    pub fn with_security_authenticator(
798        mut self,
799        authenticator: Arc<dyn TokenAuthenticator>,
800    ) -> Self {
801        self.security_authenticator = Some(authenticator);
802        self
803    }
804
805    /// Set the named authentication providers for this route.
806    ///
807    /// The registry is injected into the consumer `SecurityContext` so
808    /// Phase-2 transports can resolve providers without holding their own
809    /// authenticator.
810    pub fn with_provider_registry(mut self, registry: Arc<camel_auth::ProviderRegistry>) -> Self {
811        self.provider_registry = Some(registry);
812        self
813    }
814
815    /// Set the declared provider name used by plan compilation.
816    pub fn with_security_provider(mut self, name: impl Into<String>) -> Self {
817        self.security_provider = Some(name.into());
818        self
819    }
820
821    /// Set the route-level audience override used by plan compilation.
822    pub fn with_security_audiences(mut self, audiences: Vec<String>) -> Self {
823        self.security_audiences = Some(audiences);
824        self
825    }
826
827    /// The declared provider name, if any.
828    pub fn security_provider(&self) -> Option<&str> {
829        self.security_provider.as_deref()
830    }
831
832    /// The route-level audience override, if any.
833    pub fn security_audiences(&self) -> Option<&[String]> {
834        self.security_audiences.as_deref()
835    }
836
837    /// Set a unit of work config for this route.
838    pub fn with_unit_of_work(mut self, config: UnitOfWorkConfig) -> Self {
839        self.unit_of_work = Some(config);
840        self
841    }
842
843    /// Get the unit of work config, if set.
844    pub fn unit_of_work_config(&self) -> Option<&UnitOfWorkConfig> {
845        self.unit_of_work.as_ref()
846    }
847
848    /// Get the circuit breaker config, if set.
849    pub fn circuit_breaker_config(&self) -> Option<&CircuitBreakerConfig> {
850        self.circuit_breaker.as_ref()
851    }
852
853    pub fn security_policy_config(&self) -> Option<&SecurityPolicyConfig> {
854        self.security_policy.as_ref()
855    }
856
857    pub fn security_authenticator(&self) -> Option<&Arc<dyn TokenAuthenticator>> {
858        self.security_authenticator.as_ref()
859    }
860
861    /// User-specified concurrency override, if any.
862    pub fn concurrency_override(&self) -> Option<&ConcurrencyModel> {
863        self.concurrency.as_ref()
864    }
865
866    /// Override the consumer's concurrency model for this route.
867    pub fn with_concurrency(mut self, model: ConcurrencyModel) -> Self {
868        self.concurrency = Some(model);
869        self
870    }
871
872    /// Get the route ID.
873    pub fn route_id(&self) -> &str {
874        &self.route_id
875    }
876
877    /// Whether this route should start automatically when the context starts.
878    pub fn auto_startup(&self) -> bool {
879        self.auto_startup
880    }
881
882    /// Order in which routes are started. Lower values start first.
883    pub fn startup_order(&self) -> i32 {
884        self.startup_order
885    }
886
887    /// Set a unique identifier for this route.
888    pub fn with_route_id(mut self, id: impl Into<String>) -> Self {
889        self.route_id = id.into();
890        self
891    }
892
893    /// Set whether this route should start automatically.
894    pub fn with_auto_startup(mut self, auto: bool) -> Self {
895        self.auto_startup = auto;
896        self
897    }
898
899    /// Set the startup order. Lower values start first.
900    pub fn with_startup_order(mut self, order: i32) -> Self {
901        self.startup_order = order;
902        self
903    }
904
905    pub fn with_source_hash(mut self, hash: u64) -> Self {
906        self.source_hash = Some(hash);
907        self
908    }
909
910    pub fn source_hash(&self) -> Option<u64> {
911        self.source_hash
912    }
913
914    /// Extract the metadata fields needed for introspection.
915    /// This is used by RouteController to store route info without the non-Sync steps.
916    pub fn to_info(&self) -> RouteDefinitionInfo {
917        RouteDefinitionInfo {
918            route_id: self.route_id.clone(),
919            auto_startup: self.auto_startup,
920            startup_order: self.startup_order,
921            source_hash: self.source_hash,
922        }
923    }
924}
925
926/// Minimal route definition metadata for introspection.
927///
928/// This struct contains only the metadata fields from [`RouteDefinition`]
929/// that are needed for route lifecycle management, without the `steps` field
930/// (which contains non-Sync types and cannot be stored in a Sync struct).
931#[derive(Clone)]
932pub struct RouteDefinitionInfo {
933    route_id: String,
934    auto_startup: bool,
935    startup_order: i32,
936    pub(crate) source_hash: Option<u64>,
937}
938
939impl RouteDefinitionInfo {
940    /// Get the route ID.
941    pub fn route_id(&self) -> &str {
942        &self.route_id
943    }
944
945    /// Whether this route should start automatically when the context starts.
946    pub fn auto_startup(&self) -> bool {
947        self.auto_startup
948    }
949
950    /// Order in which routes are started. Lower values start first.
951    pub fn startup_order(&self) -> i32 {
952        self.startup_order
953    }
954
955    pub fn source_hash(&self) -> Option<u64> {
956        self.source_hash
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963    use camel_api::{RecipientSource, TargetSource};
964
965    /// Task 1.2 (span-name-enrichment): `BuilderStep::span_label` mapping.
966    ///
967    /// `To` labels carry the authored URI scheme (before the first `:`, no
968    /// interception resolution); EIP variants map to kebab-case EIP names
969    /// (declarative twins share the label of their programmatic sibling);
970    /// anonymous variants (opaque processors) carry no label.
971    #[test]
972    fn builder_step_span_label_mapping() {
973        use camel_api::declarative::LanguageExpressionDef;
974        use camel_api::splitter::AggregationStrategy;
975        use camel_api::{BoxProcessor, IdentityProcessor, OpaqueProcessor};
976
977        let expr = LanguageExpressionDef {
978            language: "simple".into(),
979            source: "${body}".into(),
980        };
981
982        // To: scheme is the authored URI before the first ':'.
983        assert_eq!(
984            BuilderStep::To("direct:tree-sub".into())
985                .span_label()
986                .as_deref(),
987            Some("to:direct")
988        );
989        assert_eq!(
990            BuilderStep::To("http://api.example/x".into())
991                .span_label()
992                .as_deref(),
993            Some("to:http")
994        );
995        // Scheme-less URI: no label — never leak the full text.
996        assert_eq!(BuilderStep::To("garbage".into()).span_label(), None);
997
998        // EIP variants → kebab-case EIP name.
999        assert_eq!(
1000            BuilderStep::Log {
1001                level: camel_processor::LogLevel::Info,
1002                message: "m".into(),
1003            }
1004            .span_label()
1005            .as_deref(),
1006            Some("log")
1007        );
1008        assert_eq!(
1009            BuilderStep::Split {
1010                config: camel_api::splitter::SplitterConfig::new(
1011                    camel_api::splitter::split_body_lines()
1012                ),
1013                steps: vec![BuilderStep::Stop],
1014            }
1015            .span_label()
1016            .as_deref(),
1017            Some("split")
1018        );
1019        assert_eq!(
1020            BuilderStep::DeclarativeSplit {
1021                expression: expr.clone(),
1022                aggregation: AggregationStrategy::Original,
1023                parallel: false,
1024                parallel_limit: None,
1025                trace_item_threshold: None,
1026                stop_on_exception: true,
1027                steps: vec![BuilderStep::Stop],
1028            }
1029            .span_label()
1030            .as_deref(),
1031            Some("split")
1032        );
1033        assert_eq!(
1034            BuilderStep::DeclarativeStreamSplit {
1035                stream_config: camel_api::StreamSplitConfig::default(),
1036                aggregation: AggregationStrategy::Original,
1037                stop_on_exception: true,
1038                steps: vec![BuilderStep::Stop],
1039            }
1040            .span_label()
1041            .as_deref(),
1042            Some("split")
1043        );
1044        assert_eq!(BuilderStep::Stop.span_label(), None);
1045
1046        // Anonymous variants → None.
1047        assert_eq!(
1048            BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(IdentityProcessor)))
1049                .span_label(),
1050            None
1051        );
1052    }
1053
1054    /// Task 1.2 (span-kind-hint): `BuilderStep::span_kind_hint` mapping.
1055    ///
1056    /// Endpoint-bearing variants (`To`, `Enrich`, `PollEnrich`, `WireTap`)
1057    /// classify the AUTHORED URI scheme (text before the first `:`,
1058    /// compared case-insensitively, no endpoint resolution): messaging
1059    /// brokers map to `Producer`, synchronous outbound protocols to
1060    /// `Client`. Every other scheme, scheme-less URIs, and every
1061    /// non-endpoint variant stay `Internal` route processing. The
1062    /// exhaustive match (no wildcard) in `span_kind_hint` is the rc-4mz7
1063    /// contract: a new `BuilderStep` variant cannot compile without a
1064    /// classification here.
1065    #[test]
1066    fn builder_step_span_kind_hint_mapping() {
1067        use camel_api::splitter::split_body_lines;
1068        use camel_api::{Exchange, FilterPredicate, SpanKindHint};
1069
1070        let kind = |uri: &str| BuilderStep::To(uri.into()).span_kind_hint();
1071
1072        // Messaging brokers → Producer.
1073        assert_eq!(kind("kafka:orders"), SpanKindHint::Producer);
1074        assert_eq!(kind("jms:q"), SpanKindHint::Producer);
1075        assert_eq!(kind("activemq:q"), SpanKindHint::Producer);
1076        assert_eq!(kind("artemis:q"), SpanKindHint::Producer);
1077        assert_eq!(kind("mqtt:t"), SpanKindHint::Producer);
1078        // Scheme comparison is case-insensitive.
1079        assert_eq!(kind("KAFKA:orders"), SpanKindHint::Producer);
1080
1081        // Synchronous outbound protocols → Client.
1082        assert_eq!(kind("http://x"), SpanKindHint::Client);
1083        assert_eq!(kind("https://x"), SpanKindHint::Client);
1084        assert_eq!(kind("grpc://x"), SpanKindHint::Client);
1085        assert_eq!(kind("grpcs://x"), SpanKindHint::Client);
1086        assert_eq!(kind("ws://x"), SpanKindHint::Client);
1087        assert_eq!(kind("redis://x"), SpanKindHint::Client);
1088        assert_eq!(kind("opensearch://x"), SpanKindHint::Client);
1089        assert_eq!(kind("sql:db"), SpanKindHint::Client);
1090        assert_eq!(kind("surrealdb://x"), SpanKindHint::Client);
1091        assert_eq!(kind("cxf://x"), SpanKindHint::Client);
1092        assert_eq!(kind("llm://x"), SpanKindHint::Client);
1093        assert_eq!(kind("mcp://x"), SpanKindHint::Client);
1094
1095        // In-process and unknown schemes → Internal.
1096        assert_eq!(kind("direct:y"), SpanKindHint::Internal);
1097        assert_eq!(kind("timer:z"), SpanKindHint::Internal);
1098        // Scheme-less URI: the kind is never guessed.
1099        assert_eq!(kind("garbage"), SpanKindHint::Internal);
1100
1101        // Non-`To` variants are internal route processing.
1102        assert_eq!(
1103            BuilderStep::Log {
1104                level: camel_processor::LogLevel::Info,
1105                message: "m".into(),
1106            }
1107            .span_kind_hint(),
1108            SpanKindHint::Internal
1109        );
1110        assert_eq!(
1111            BuilderStep::Filter {
1112                predicate: FilterPredicate::new(|_: &Exchange| true),
1113                steps: vec![BuilderStep::Stop],
1114            }
1115            .span_kind_hint(),
1116            SpanKindHint::Internal
1117        );
1118        assert_eq!(
1119            BuilderStep::Split {
1120                config: camel_api::splitter::SplitterConfig::new(split_body_lines()),
1121                steps: vec![BuilderStep::Stop],
1122            }
1123            .span_kind_hint(),
1124            SpanKindHint::Internal
1125        );
1126
1127        // rc-2heu: endpoint-bearing EIP steps share the `To` scheme
1128        // classification. Each covers a Client scheme, a Producer scheme,
1129        // and an Internal (in-process) scheme.
1130        let enrich = |uri: &str| BuilderStep::Enrich {
1131            uri: uri.into(),
1132            strategy: None,
1133            timeout_ms: None,
1134        };
1135        let poll_enrich = |uri: &str| BuilderStep::PollEnrich {
1136            uri: uri.into(),
1137            strategy: None,
1138            timeout_ms: None,
1139        };
1140        let wire_tap = |uri: &str| BuilderStep::WireTap { uri: uri.into() };
1141
1142        // Client scheme.
1143        assert_eq!(enrich("http://x").span_kind_hint(), SpanKindHint::Client);
1144        assert_eq!(
1145            poll_enrich("http://x").span_kind_hint(),
1146            SpanKindHint::Client
1147        );
1148        assert_eq!(wire_tap("http://x").span_kind_hint(), SpanKindHint::Client);
1149        // Producer scheme.
1150        assert_eq!(
1151            enrich("kafka:orders").span_kind_hint(),
1152            SpanKindHint::Producer
1153        );
1154        assert_eq!(
1155            poll_enrich("kafka:orders").span_kind_hint(),
1156            SpanKindHint::Producer
1157        );
1158        assert_eq!(
1159            wire_tap("kafka:orders").span_kind_hint(),
1160            SpanKindHint::Producer
1161        );
1162        // Internal (in-process) scheme.
1163        assert_eq!(enrich("direct:y").span_kind_hint(), SpanKindHint::Internal);
1164        assert_eq!(
1165            poll_enrich("seda:q").span_kind_hint(),
1166            SpanKindHint::Internal
1167        );
1168        assert_eq!(
1169            wire_tap("direct:y").span_kind_hint(),
1170            SpanKindHint::Internal
1171        );
1172    }
1173
1174    /// Golden debug output for EVERY BuilderStep variant.
1175    ///
1176    /// Before refactoring the manual `Debug` impl to `#[derive(Debug)]`, this test
1177    /// locks the exact output of every variant so the refactor can be verified.
1178    #[test]
1179    fn golden_debug_output_all_variants() {
1180        use camel_api::declarative::LanguageExpressionDef;
1181        use camel_api::loop_eip::LoopMode;
1182        use camel_api::recipient_list::RecipientListConfig;
1183        use camel_api::splitter::{AggregationStrategy, StreamSplitConfig, StreamSplitFormat};
1184        use camel_api::{
1185            BoxProcessor, DynamicRouterConfig, Exchange, FilterPredicate, FunctionDefinition,
1186            FunctionId, IdentityProcessor, MulticastConfig, OpaqueProcessor, RoutingSlipConfig,
1187            Value,
1188        };
1189        use std::sync::Arc;
1190
1191        let expr = LanguageExpressionDef {
1192            language: "simple".into(),
1193            source: "${body}".into(),
1194        };
1195
1196        // -- group A: trivial / non-recursive ----------------------------------
1197        // derive(Debug) emits the variant without the enum name for unit/tuple variants.
1198        assert_eq!(format!("{:?}", BuilderStep::Stop), "Stop");
1199        assert_eq!(
1200            format!(
1201                "{:?}",
1202                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(IdentityProcessor)))
1203            ),
1204            "Processor(BoxProcessor(...))"
1205        );
1206        assert_eq!(
1207            format!("{:?}", BuilderStep::To("mock:out".into())),
1208            "To(\"mock:out\")"
1209        );
1210
1211        // -- group B: variant with named fields (no sub-steps) -----------------
1212        // derive(Debug) emits ALL fields, not the redacted `..` form.
1213        assert_eq!(
1214            format!(
1215                "{:?}",
1216                BuilderStep::Log {
1217                    level: camel_processor::LogLevel::Info,
1218                    message: "hello".into(),
1219                }
1220            ),
1221            "Log { level: Info, message: \"hello\" }"
1222        );
1223
1224        assert_eq!(
1225            format!(
1226                "{:?}",
1227                BuilderStep::DeclarativeSetHeader {
1228                    key: "k".into(),
1229                    value: ValueSourceDef::Literal(Value::String("v".into())),
1230                }
1231            ),
1232            "DeclarativeSetHeader { key: \"k\", value: Literal(String(\"v\")) }"
1233        );
1234
1235        assert_eq!(
1236            format!(
1237                "{:?}",
1238                BuilderStep::DeclarativeSetHeaderIfAbsent {
1239                    key: "k".into(),
1240                    value: ValueSourceDef::Literal(Value::String("v".into())),
1241                }
1242            ),
1243            "DeclarativeSetHeaderIfAbsent { key: \"k\", value: Literal(String(\"v\")) }"
1244        );
1245
1246        assert_eq!(
1247            format!(
1248                "{:?}",
1249                BuilderStep::DeclarativeSetBody {
1250                    value: ValueSourceDef::Literal(Value::String("v".into())),
1251                }
1252            ),
1253            "DeclarativeSetBody { value: Literal(String(\"v\")) }"
1254        );
1255
1256        assert_eq!(
1257            format!(
1258                "{:?}",
1259                BuilderStep::DeclarativeSetProperty {
1260                    key: "prop".into(),
1261                    value_source: ValueSourceDef::Literal(Value::String("v".into())),
1262                }
1263            ),
1264            "DeclarativeSetProperty { key: \"prop\", value_source: Literal(String(\"v\")) }"
1265        );
1266
1267        assert_eq!(
1268            format!(
1269                "{:?}",
1270                BuilderStep::DeclarativeScript {
1271                    expression: expr.clone(),
1272                }
1273            ),
1274            "DeclarativeScript { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" } }"
1275        );
1276
1277        // DeclarativeFunction needs a FunctionDefinition
1278        let func_def = FunctionDefinition {
1279            id: FunctionId("test-id".into()),
1280            runtime: "my_runtime".into(),
1281            source: "${body}".into(),
1282            timeout_ms: 5000,
1283            route_id: None,
1284            step_index: None,
1285        };
1286        assert_eq!(
1287            format!(
1288                "{:?}",
1289                BuilderStep::DeclarativeFunction {
1290                    definition: func_def,
1291                }
1292            ),
1293            "DeclarativeFunction { definition: FunctionDefinition { id: FunctionId(\"test-id\"), runtime: \"my_runtime\", source: \"${body}\", timeout_ms: 5000, route_id: None, step_index: None } }"
1294        );
1295
1296        assert_eq!(
1297            format!(
1298                "{:?}",
1299                BuilderStep::WireTap {
1300                    uri: "mock:tap".into(),
1301                }
1302            ),
1303            "WireTap { uri: \"mock:tap\" }"
1304        );
1305
1306        assert_eq!(
1307            format!(
1308                "{:?}",
1309                BuilderStep::DeclarativeLog {
1310                    level: camel_processor::LogLevel::Info,
1311                    message: ValueSourceDef::Expression(expr.clone()),
1312                }
1313            ),
1314            "DeclarativeLog { level: Info, message: Expression(LanguageExpressionDef { language: \"simple\", source: \"${body}\" }) }"
1315        );
1316
1317        assert_eq!(
1318            format!(
1319                "{:?}",
1320                BuilderStep::Bean {
1321                    name: "myBean".into(),
1322                    method: "process".into(),
1323                }
1324            ),
1325            "Bean { name: \"myBean\", method: \"process\" }"
1326        );
1327
1328        assert_eq!(
1329            format!(
1330                "{:?}",
1331                BuilderStep::Script {
1332                    language: "js".into(),
1333                    script: "body".into(),
1334                }
1335            ),
1336            "Script { language: \"js\", script: \"body\" }"
1337        );
1338
1339        assert_eq!(
1340            format!(
1341                "{:?}",
1342                BuilderStep::Aggregate {
1343                    config: camel_api::AggregatorConfig::correlate_by("id")
1344                        .complete_when_size(1)
1345                        .build()
1346                        .unwrap(),
1347                }
1348            ),
1349            "Aggregate { config: AggregatorConfig { header_name: \"id\", completion: Single(Size(1)), correlation: HeaderName(\"id\"), strategy: CollectAll, max_buckets: Some(10000), max_bucket_size: Some(10000), bucket_ttl: Some(300s), force_completion_on_stop: false, discard_on_timeout: false, max_timeout_tasks: 1024 } }"
1350        );
1351
1352        assert_eq!(
1353            format!(
1354                "{:?}",
1355                BuilderStep::DynamicRouter {
1356                    config: DynamicRouterConfig::new(TargetSource::Sync(Arc::new(
1357                        |_: &Exchange| Some("mock:dr".into())
1358                    ))),
1359                }
1360            ),
1361            "DynamicRouter { config: DynamicRouterConfig { uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false, max_iterations: 1000, timeout: Some(60s) } }"
1362        );
1363
1364        assert_eq!(
1365            format!(
1366                "{:?}",
1367                BuilderStep::RoutingSlip {
1368                    config: RoutingSlipConfig::new(TargetSource::Sync(Arc::new(|_: &Exchange| {
1369                        Some("mock:rs".into())
1370                    }))),
1371                }
1372            ),
1373            "RoutingSlip { config: RoutingSlipConfig { uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false } }"
1374        );
1375
1376        assert_eq!(
1377            format!(
1378                "{:?}",
1379                BuilderStep::RecipientList {
1380                    config: RecipientListConfig::new(RecipientSource::Sync(Arc::new(
1381                        |_: &Exchange| String::new()
1382                    ))),
1383                }
1384            ),
1385            "RecipientList { config: RecipientListConfig { delimiter: \",\", parallel: false, parallel_limit: None, stop_on_exception: false, max_recipients: 1000 } }"
1386        );
1387
1388        assert_eq!(
1389            format!(
1390                "{:?}",
1391                BuilderStep::Enrich {
1392                    uri: "mock:enrich".into(),
1393                    strategy: Some("agg".into()),
1394                    timeout_ms: Some(1000),
1395                }
1396            ),
1397            "Enrich { uri: \"mock:enrich\", strategy: Some(\"agg\"), timeout_ms: Some(1000) }"
1398        );
1399
1400        assert_eq!(
1401            format!(
1402                "{:?}",
1403                BuilderStep::PollEnrich {
1404                    uri: "mock:poll".into(),
1405                    strategy: None,
1406                    timeout_ms: None,
1407                }
1408            ),
1409            "PollEnrich { uri: \"mock:poll\", strategy: None, timeout_ms: None }"
1410        );
1411
1412        assert_eq!(
1413            format!(
1414                "{:?}",
1415                BuilderStep::Validate {
1416                    predicate: expr.clone(),
1417                }
1418            ),
1419            "Validate { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" } }"
1420        );
1421
1422        assert_eq!(
1423            format!("{:?}", BuilderStep::Sampling { period: 100 }),
1424            "Sampling { period: 100 }"
1425        );
1426
1427        assert_eq!(
1428            format!(
1429                "{:?}",
1430                BuilderStep::Resequence {
1431                    policy_config: Default::default(),
1432                }
1433            ),
1434            "Resequence { policy_config: ResequencePolicyConfig { mode: Batch { correlation: \"header.id\", sort: \"header.id\", completion: SizeOrTimeout(100, 30000) } } }"
1435        );
1436
1437        // -- group C: variants with named fields (sub-steps is Vec) ------------
1438        assert_eq!(
1439            format!(
1440                "{:?}",
1441                BuilderStep::DeclarativeFilter {
1442                    predicate: expr.clone(),
1443                    steps: vec![BuilderStep::Stop],
1444                }
1445            ),
1446            "DeclarativeFilter { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [Stop] }"
1447        );
1448
1449        assert_eq!(
1450            format!(
1451                "{:?}",
1452                BuilderStep::DeclarativeSplit {
1453                    expression: expr.clone(),
1454                    aggregation: AggregationStrategy::Original,
1455                    parallel: false,
1456                    parallel_limit: Some(2),
1457                    trace_item_threshold: None,
1458                    stop_on_exception: true,
1459                    steps: vec![BuilderStep::Stop],
1460                }
1461            ),
1462            "DeclarativeSplit { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, aggregation: Original, parallel: false, parallel_limit: Some(2), trace_item_threshold: None, stop_on_exception: true, steps: [Stop] }"
1463        );
1464
1465        assert_eq!(
1466            format!(
1467                "{:?}",
1468                BuilderStep::Split {
1469                    config: camel_api::splitter::SplitterConfig::new(
1470                        camel_api::splitter::split_body_lines()
1471                    ),
1472                    steps: vec![BuilderStep::Stop],
1473                }
1474            ),
1475            "Split { config: SplitterConfig { expression: \"<split-expression>\", aggregation: LastWins, parallel: false, parallel_limit: None, stop_on_exception: true, max_fragments: 100000, trace_item_threshold: 100 }, steps: [Stop] }"
1476        );
1477
1478        assert_eq!(
1479            format!(
1480                "{:?}",
1481                BuilderStep::Filter {
1482                    predicate: FilterPredicate::new(|_: &Exchange| true),
1483                    steps: vec![BuilderStep::Stop],
1484                }
1485            ),
1486            "Filter { predicate: FilterPredicate(..), steps: [Stop] }"
1487        );
1488
1489        assert_eq!(
1490            format!(
1491                "{:?}",
1492                BuilderStep::Throttle {
1493                    config: camel_api::ThrottlerConfig::new(
1494                        10,
1495                        std::time::Duration::from_millis(10)
1496                    ),
1497                    steps: vec![BuilderStep::Stop],
1498                }
1499            ),
1500            "Throttle { config: ThrottlerConfig { max_requests: 10, period: 10ms, strategy: Delay }, steps: [Stop] }"
1501        );
1502
1503        assert_eq!(
1504            format!(
1505                "{:?}",
1506                BuilderStep::LoadBalance {
1507                    config: camel_api::LoadBalancerConfig::round_robin(),
1508                    steps: vec![BuilderStep::To("mock:l1".into())],
1509                }
1510            ),
1511            "LoadBalance { config: LoadBalancerConfig { strategy: RoundRobin }, steps: [To(\"mock:l1\")] }"
1512        );
1513
1514        assert_eq!(
1515            format!(
1516                "{:?}",
1517                BuilderStep::Delay {
1518                    config: camel_api::DelayConfig::new(500),
1519                }
1520            ),
1521            "Delay { config: DelayConfig { delay_ms: 500, dynamic_header: None, max_delay_ms: 3600000 } }"
1522        );
1523
1524        // -- group D: Choice / DeclarativeChoice --------------------------------
1525        // derive(Debug) emits full field enumeration; WhenStep Debug is a
1526        // full struct listing; nested BuilderStep::Stop stays as `Stop`.
1527        assert_eq!(
1528            format!(
1529                "{:?}",
1530                BuilderStep::Choice {
1531                    whens: vec![WhenStep {
1532                        predicate: FilterPredicate::new(|_: &Exchange| true),
1533                        steps: vec![BuilderStep::To("mock:a".into())],
1534                    }],
1535                    otherwise: None,
1536                }
1537            ),
1538            "Choice { whens: [WhenStep { predicate: FilterPredicate(..), steps: [To(\"mock:a\")] }], otherwise: None }"
1539        );
1540
1541        assert_eq!(
1542            format!(
1543                "{:?}",
1544                BuilderStep::DeclarativeChoice {
1545                    whens: vec![DeclarativeWhenStep {
1546                        predicate: expr.clone(),
1547                        steps: vec![BuilderStep::Stop],
1548                    }],
1549                    otherwise: Some(vec![BuilderStep::Stop]),
1550                }
1551            ),
1552            "DeclarativeChoice { whens: [DeclarativeWhenStep { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [Stop] }], otherwise: Some([Stop]) }"
1553        );
1554
1555        // -- group E: Multicast -------------------------------------------------
1556        assert_eq!(
1557            format!(
1558                "{:?}",
1559                BuilderStep::Multicast {
1560                    steps: vec![BuilderStep::To("direct:a".into())],
1561                    config: MulticastConfig::new(),
1562                }
1563            ),
1564            "Multicast { steps: [To(\"direct:a\")], config: MulticastConfig { parallel: false, parallel_limit: None, stop_on_exception: false, timeout: None, aggregation: LastWins } }"
1565        );
1566
1567        // -- group F: DeclarativeDynamicRouter / DeclarativeRoutingSlip --------
1568        assert_eq!(
1569            format!(
1570                "{:?}",
1571                BuilderStep::DeclarativeDynamicRouter {
1572                    expression: expr.clone(),
1573                    uri_delimiter: ",".into(),
1574                    cache_size: 1000,
1575                    ignore_invalid_endpoints: false,
1576                    max_iterations: 1000,
1577                }
1578            ),
1579            "DeclarativeDynamicRouter { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false, max_iterations: 1000 }"
1580        );
1581
1582        assert_eq!(
1583            format!(
1584                "{:?}",
1585                BuilderStep::DeclarativeRoutingSlip {
1586                    expression: expr.clone(),
1587                    uri_delimiter: ",".into(),
1588                    cache_size: 1000,
1589                    ignore_invalid_endpoints: false,
1590                }
1591            ),
1592            "DeclarativeRoutingSlip { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false }"
1593        );
1594
1595        // -- group G: DeclarativeRecipientList ---------------------------------
1596        assert_eq!(
1597            format!(
1598                "{:?}",
1599                BuilderStep::DeclarativeRecipientList {
1600                    expression: expr.clone(),
1601                    delimiter: ",".into(),
1602                    parallel: false,
1603                    parallel_limit: None,
1604                    stop_on_exception: false,
1605                    aggregation: "original".into(),
1606                }
1607            ),
1608            "DeclarativeRecipientList { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, delimiter: \",\", parallel: false, parallel_limit: None, stop_on_exception: false, aggregation: \"original\" }"
1609        );
1610
1611        // -- group H: Loop / DeclarativeLoop -----------------------------------
1612        assert_eq!(
1613            format!(
1614                "{:?}",
1615                BuilderStep::Loop {
1616                    config: camel_api::loop_eip::LoopConfig::new(LoopMode::Count(3)),
1617                    steps: vec![],
1618                }
1619            ),
1620            "Loop { config: LoopConfig { mode: Count(3), max_iterations: 10000 }, steps: [] }"
1621        );
1622
1623        assert_eq!(
1624            format!(
1625                "{:?}",
1626                BuilderStep::DeclarativeLoop {
1627                    count: Some(5),
1628                    while_predicate: None,
1629                    steps: vec![],
1630                    max_iterations: Some(100),
1631                }
1632            ),
1633            "DeclarativeLoop { count: Some(5), while_predicate: None, steps: [], max_iterations: Some(100) }"
1634        );
1635
1636        // -- group I: ClaimCheck -----------------------------------------------
1637        assert_eq!(
1638            format!(
1639                "{:?}",
1640                BuilderStep::ClaimCheck {
1641                    repository: "myRepo".into(),
1642                    operation: "checkout".into(),
1643                    key: expr.clone(),
1644                    filter: None,
1645                }
1646            ),
1647            "ClaimCheck { repository: \"myRepo\", operation: \"checkout\", key: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, filter: None }"
1648        );
1649
1650        // -- group J: Sort -----------------------------------------------------
1651        assert_eq!(
1652            format!(
1653                "{:?}",
1654                BuilderStep::Sort {
1655                    expression: expr.clone(),
1656                    reverse: false,
1657                }
1658            ),
1659            "Sort { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, reverse: false }"
1660        );
1661
1662        // -- group K: IdempotentConsumer ---------------------------------------
1663        assert_eq!(
1664            format!(
1665                "{:?}",
1666                BuilderStep::IdempotentConsumer {
1667                    repository: "myRepo".into(),
1668                    expression: expr.clone(),
1669                    steps: vec![],
1670                    eager: true,
1671                    remove_on_failure: false,
1672                }
1673            ),
1674            "IdempotentConsumer { repository: \"myRepo\", expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [], eager: true, remove_on_failure: false }"
1675        );
1676
1677        // -- group L: DeclarativeDoTry -----------------------------------------
1678        assert_eq!(
1679            format!(
1680                "{:?}",
1681                BuilderStep::DeclarativeDoTry {
1682                    try_steps: vec![BuilderStep::Stop],
1683                    catch: vec![],
1684                    finally: None,
1685                }
1686            ),
1687            "DeclarativeDoTry { try_steps: [Stop], catch: [], finally: None }"
1688        );
1689
1690        // -- group M: DeclarativeStreamSplit -----------------------------------
1691        assert_eq!(
1692            format!(
1693                "{:?}",
1694                BuilderStep::DeclarativeStreamSplit {
1695                    stream_config: StreamSplitConfig {
1696                        format: StreamSplitFormat::Ndjson,
1697                        max_record_bytes: 1024 * 1024,
1698                        batch_size: 1,
1699                        chunk_size: None,
1700                        include_origin: true,
1701                    },
1702                    aggregation: AggregationStrategy::Original,
1703                    stop_on_exception: true,
1704                    steps: vec![BuilderStep::Stop],
1705                }
1706            ),
1707            "DeclarativeStreamSplit { stream_config: StreamSplitConfig { format: Ndjson, max_record_bytes: 1048576, batch_size: 1, chunk_size: None, include_origin: true }, aggregation: Original, stop_on_exception: true, steps: [Stop] }"
1708        );
1709    }
1710
1711    #[test]
1712    fn test_builder_step_multicast_variant() {
1713        use camel_api::MulticastConfig;
1714
1715        let step = BuilderStep::Multicast {
1716            steps: vec![BuilderStep::To("direct:a".into())],
1717            config: MulticastConfig::new(),
1718        };
1719
1720        assert!(matches!(step, BuilderStep::Multicast { .. }));
1721    }
1722
1723    #[test]
1724    fn test_route_definition_defaults() {
1725        let def = RouteDefinition::new("direct:test", vec![]).with_route_id("test-route");
1726        assert_eq!(def.route_id(), "test-route");
1727        assert!(def.auto_startup());
1728        assert_eq!(def.startup_order(), 1000);
1729    }
1730
1731    #[test]
1732    fn test_route_definition_builders() {
1733        let def = RouteDefinition::new("direct:test", vec![])
1734            .with_route_id("my-route")
1735            .with_auto_startup(false)
1736            .with_startup_order(50);
1737        assert_eq!(def.route_id(), "my-route");
1738        assert!(!def.auto_startup());
1739        assert_eq!(def.startup_order(), 50);
1740    }
1741
1742    #[test]
1743    fn test_route_definition_accessors_cover_core_fields() {
1744        let def = RouteDefinition::new("direct:in", vec![BuilderStep::To("mock:out".into())])
1745            .with_route_id("accessor-route");
1746
1747        assert_eq!(def.from_uri(), "direct:in");
1748        assert_eq!(def.steps().len(), 1);
1749        assert!(matches!(def.steps()[0], BuilderStep::To(_)));
1750    }
1751
1752    #[test]
1753    fn test_route_definition_error_handler_circuit_breaker_and_concurrency_accessors() {
1754        use camel_api::circuit_breaker::CircuitBreakerConfig;
1755        use camel_api::error_handler::ErrorHandlerConfig;
1756        use camel_component_api::ConcurrencyModel;
1757
1758        let def = RouteDefinition::new("direct:test", vec![])
1759            .with_route_id("eh-route")
1760            .with_error_handler(ErrorHandlerConfig::dead_letter_channel("log:dlc"))
1761            .with_circuit_breaker(CircuitBreakerConfig::new())
1762            .with_concurrency(ConcurrencyModel::Concurrent { max: Some(4) });
1763
1764        let eh = def
1765            .error_handler_config()
1766            .expect("error handler should be set");
1767        assert_eq!(eh.dlc_uri.as_deref(), Some("log:dlc"));
1768        assert!(def.circuit_breaker_config().is_some());
1769        assert!(matches!(
1770            def.concurrency_override(),
1771            Some(ConcurrencyModel::Concurrent { max: Some(4) })
1772        ));
1773    }
1774
1775    #[test]
1776    fn test_builder_step_debug_covers_many_variants() {
1777        use camel_api::splitter::{AggregationStrategy, SplitterConfig, split_body_lines};
1778        use camel_api::{
1779            BoxProcessor, DynamicRouterConfig, Exchange, FilterPredicate, IdentityProcessor,
1780            OpaqueProcessor, RoutingSlipConfig, Value,
1781        };
1782        use std::sync::Arc;
1783
1784        let expr = LanguageExpressionDef {
1785            language: "simple".into(),
1786            source: "${body}".into(),
1787        };
1788
1789        let steps = vec![
1790            BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(IdentityProcessor))),
1791            BuilderStep::To("mock:out".into()),
1792            BuilderStep::Stop,
1793            BuilderStep::Log {
1794                level: camel_processor::LogLevel::Info,
1795                message: "hello".into(),
1796            },
1797            BuilderStep::DeclarativeSetHeader {
1798                key: "k".into(),
1799                value: ValueSourceDef::Literal(Value::String("v".into())),
1800            },
1801            BuilderStep::DeclarativeSetBody {
1802                value: ValueSourceDef::Expression(expr.clone()),
1803            },
1804            BuilderStep::DeclarativeFilter {
1805                predicate: expr.clone(),
1806                steps: vec![BuilderStep::Stop],
1807            },
1808            BuilderStep::DeclarativeChoice {
1809                whens: vec![DeclarativeWhenStep {
1810                    predicate: expr.clone(),
1811                    steps: vec![BuilderStep::Stop],
1812                }],
1813                otherwise: Some(vec![BuilderStep::Stop]),
1814            },
1815            BuilderStep::DeclarativeScript {
1816                expression: expr.clone(),
1817            },
1818            BuilderStep::DeclarativeSplit {
1819                expression: expr.clone(),
1820                aggregation: AggregationStrategy::Original,
1821                parallel: false,
1822                parallel_limit: Some(2),
1823                trace_item_threshold: None,
1824                stop_on_exception: true,
1825                steps: vec![BuilderStep::Stop],
1826            },
1827            BuilderStep::Split {
1828                config: SplitterConfig::new(split_body_lines()),
1829                steps: vec![BuilderStep::Stop],
1830            },
1831            BuilderStep::Aggregate {
1832                config: camel_api::AggregatorConfig::correlate_by("id")
1833                    .complete_when_size(1)
1834                    .build()
1835                    .unwrap(),
1836            },
1837            BuilderStep::Filter {
1838                predicate: FilterPredicate::new(|_: &Exchange| true),
1839                steps: vec![BuilderStep::Stop],
1840            },
1841            BuilderStep::WireTap {
1842                uri: "mock:tap".into(),
1843            },
1844            BuilderStep::DeclarativeLog {
1845                level: camel_processor::LogLevel::Info,
1846                message: ValueSourceDef::Expression(expr.clone()),
1847            },
1848            BuilderStep::Bean {
1849                name: "bean".into(),
1850                method: "call".into(),
1851            },
1852            BuilderStep::Script {
1853                language: "rhai".into(),
1854                script: "body".into(),
1855            },
1856            BuilderStep::Throttle {
1857                config: camel_api::ThrottlerConfig::new(10, std::time::Duration::from_millis(10)),
1858                steps: vec![BuilderStep::Stop],
1859            },
1860            BuilderStep::LoadBalance {
1861                config: camel_api::LoadBalancerConfig::round_robin(),
1862                steps: vec![BuilderStep::To("mock:l1".into())],
1863            },
1864            BuilderStep::DynamicRouter {
1865                config: DynamicRouterConfig::new(TargetSource::Sync(Arc::new(|_| {
1866                    Some("mock:dr".into())
1867                }))),
1868            },
1869            BuilderStep::RoutingSlip {
1870                config: RoutingSlipConfig::new(TargetSource::Sync(Arc::new(|_| {
1871                    Some("mock:rs".into())
1872                }))),
1873            },
1874        ];
1875
1876        for step in steps {
1877            let dbg = format!("{step:?}");
1878            assert!(!dbg.is_empty());
1879        }
1880    }
1881
1882    #[test]
1883    fn test_route_definition_to_info_preserves_metadata() {
1884        let info = RouteDefinition::new("direct:test", vec![])
1885            .with_route_id("meta-route")
1886            .with_auto_startup(false)
1887            .with_startup_order(7)
1888            .to_info();
1889
1890        assert_eq!(info.route_id(), "meta-route");
1891        assert!(!info.auto_startup());
1892        assert_eq!(info.startup_order(), 7);
1893    }
1894
1895    #[test]
1896    fn test_choice_builder_step_debug() {
1897        use camel_api::FilterPredicate;
1898
1899        fn always_true(_: &camel_api::Exchange) -> bool {
1900            true
1901        }
1902
1903        let step = BuilderStep::Choice {
1904            whens: vec![WhenStep {
1905                predicate: FilterPredicate::new(always_true),
1906                steps: vec![BuilderStep::To("mock:a".into())],
1907            }],
1908            otherwise: None,
1909        };
1910        let debug = format!("{step:?}");
1911        assert!(debug.contains("Choice"));
1912    }
1913
1914    #[test]
1915    fn test_route_definition_unit_of_work() {
1916        use camel_api::UnitOfWorkConfig;
1917        let config = UnitOfWorkConfig {
1918            on_complete: Some("log:complete".into()),
1919            on_failure: Some("log:failed".into()),
1920        };
1921        let def = RouteDefinition::new("direct:test", vec![])
1922            .with_route_id("uow-test")
1923            .with_unit_of_work(config.clone());
1924        assert_eq!(
1925            def.unit_of_work_config().unwrap().on_complete.as_deref(),
1926            Some("log:complete")
1927        );
1928        assert_eq!(
1929            def.unit_of_work_config().unwrap().on_failure.as_deref(),
1930            Some("log:failed")
1931        );
1932
1933        let def_no_uow = RouteDefinition::new("direct:test", vec![]).with_route_id("no-uow");
1934        assert!(def_no_uow.unit_of_work_config().is_none());
1935    }
1936
1937    #[test]
1938    fn test_route_definition_security_policy_accessor() {
1939        use async_trait::async_trait;
1940        use camel_api::CamelError;
1941        use camel_api::Exchange;
1942        use camel_api::security_policy::{
1943            AuthContext, AuthorizationDecision, Principal, SecurityPolicy, SecurityPolicyConfig,
1944        };
1945
1946        struct StubPolicy;
1947        #[async_trait]
1948        impl SecurityPolicy for StubPolicy {
1949            async fn evaluate(
1950                &self,
1951                _exchange: &mut Exchange,
1952                _auth: &AuthContext<'_>,
1953            ) -> Result<AuthorizationDecision, CamelError> {
1954                Ok(AuthorizationDecision::Granted {
1955                    principal: Principal {
1956                        subject: "test".into(),
1957                        issuer: "test".into(),
1958                        audience: vec![],
1959                        scopes: vec![],
1960                        roles: vec![],
1961                        claims: serde_json::Value::Null,
1962                    },
1963                })
1964            }
1965        }
1966
1967        let def_no_sp = RouteDefinition::new("direct:test", vec![]).with_route_id("no-sp");
1968        assert!(def_no_sp.security_policy_config().is_none());
1969
1970        let def = RouteDefinition::new("direct:test", vec![])
1971            .with_route_id("sp-test")
1972            .with_security_policy(SecurityPolicyConfig::new(StubPolicy));
1973        assert!(def.security_policy_config().is_some());
1974    }
1975
1976    #[test]
1977    fn test_route_definition_security_authenticator_accessor() {
1978        use camel_api::security_policy::Principal;
1979
1980        struct TestAuth;
1981        #[async_trait::async_trait]
1982        impl TokenAuthenticator for TestAuth {
1983            async fn authenticate_bearer(
1984                &self,
1985                _token: &str,
1986            ) -> Result<Principal, camel_api::CamelError> {
1987                Ok(Principal {
1988                    subject: "test".into(),
1989                    issuer: "test".into(),
1990                    audience: vec![],
1991                    scopes: vec![],
1992                    roles: vec![],
1993                    claims: serde_json::Value::Null,
1994                })
1995            }
1996        }
1997
1998        let def_no_auth = RouteDefinition::new("direct:test".to_string(), vec![]);
1999        assert!(def_no_auth.security_authenticator().is_none());
2000
2001        let auth = Arc::new(TestAuth);
2002        let def = RouteDefinition::new("direct:test".to_string(), vec![])
2003            .with_security_authenticator(auth);
2004        assert!(def.security_authenticator().is_some());
2005    }
2006
2007    #[test]
2008    fn test_map_steps_swaps_steps_and_preserves_other_fields() {
2009        let original = RouteDefinition::new(
2010            "direct:test".to_string(),
2011            vec![BuilderStep::To("mock:a".into()), BuilderStep::Stop],
2012        )
2013        .with_route_id("my-route");
2014
2015        let mapped = original.map_steps(|steps| {
2016            let mut out = Vec::with_capacity(steps.len() + 1);
2017            out.push(BuilderStep::To("mock:prefix".into()));
2018            out.extend(steps);
2019            out
2020        });
2021
2022        // Steps were transformed.
2023        assert_eq!(mapped.steps().len(), 3);
2024        assert!(matches!(mapped.steps()[0], BuilderStep::To(ref s) if s == "mock:prefix"));
2025        assert!(matches!(mapped.steps()[1], BuilderStep::To(ref s) if s == "mock:a"));
2026        // Other fields preserved.
2027        assert_eq!(mapped.route_id(), "my-route");
2028    }
2029
2030    #[test]
2031    fn circuit_breaker_fallback_accessor_returns_steps() {
2032        let def = RouteDefinition::new("direct:start", vec![])
2033            .with_circuit_breaker_fallback(vec![BuilderStep::To("mock:out".into())]);
2034        assert_eq!(def.circuit_breaker_fallback().len(), 1);
2035    }
2036}