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