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