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    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        stop_on_exception: bool,
120        steps: Vec<BuilderStep>,
121    },
122    /// Declarative stream split using a streaming split expression, resolved at route-add time.
123    DeclarativeStreamSplit {
124        stream_config: camel_api::StreamSplitConfig,
125        aggregation: camel_api::splitter::AggregationStrategy,
126        stop_on_exception: bool,
127        steps: Vec<BuilderStep>,
128    },
129    DeclarativeDynamicRouter {
130        expression: LanguageExpressionDef,
131        uri_delimiter: String,
132        cache_size: i32,
133        ignore_invalid_endpoints: bool,
134        max_iterations: usize,
135    },
136    DeclarativeRoutingSlip {
137        expression: LanguageExpressionDef,
138        uri_delimiter: String,
139        cache_size: i32,
140        ignore_invalid_endpoints: bool,
141    },
142    /// A Splitter sub-pipeline: config + nested steps to execute per fragment.
143    Split {
144        config: SplitterConfig,
145        steps: Vec<BuilderStep>,
146    },
147    /// An Aggregator step: collects exchanges by correlation key, emits when complete.
148    Aggregate {
149        config: AggregatorConfig,
150    },
151    /// A Filter sub-pipeline: predicate + nested steps executed only when predicate is true.
152    Filter {
153        predicate: FilterPredicate,
154        steps: Vec<BuilderStep>,
155    },
156    /// A Choice step: evaluates when-clauses in order, routes to the first match.
157    /// If no when matches, the optional otherwise branch is used.
158    Choice {
159        whens: Vec<WhenStep>,
160        otherwise: Option<Vec<BuilderStep>>,
161    },
162    /// A WireTap step: sends a clone of the exchange to a tap endpoint (fire-and-forget).
163    WireTap {
164        uri: String,
165    },
166    /// A Multicast step: sends the same exchange to multiple destinations.
167    Multicast {
168        steps: Vec<BuilderStep>,
169        config: MulticastConfig,
170    },
171    /// Declarative log step with a language-evaluated message, resolved at route-add time.
172    DeclarativeLog {
173        level: camel_processor::LogLevel,
174        message: ValueSourceDef,
175    },
176    /// Bean invocation step — resolved at route-add time.
177    Bean {
178        name: String,
179        method: String,
180    },
181    /// Script step: executes a script that can mutate the exchange.
182    /// The script has access to `headers`, `properties`, and `body`.
183    Script {
184        language: String,
185        script: String,
186    },
187    /// Throttle step: rate limiting with configurable behavior when limit exceeded.
188    Throttle {
189        config: camel_api::ThrottlerConfig,
190        steps: Vec<BuilderStep>,
191    },
192    /// LoadBalance step: distributes exchanges across multiple endpoints using a strategy.
193    LoadBalance {
194        config: camel_api::LoadBalancerConfig,
195        steps: Vec<BuilderStep>,
196    },
197    /// DynamicRouter step: routes exchanges dynamically based on expression evaluation.
198    DynamicRouter {
199        config: camel_api::DynamicRouterConfig,
200    },
201    RoutingSlip {
202        config: camel_api::RoutingSlipConfig,
203    },
204    RecipientList {
205        config: camel_api::recipient_list::RecipientListConfig,
206    },
207    DeclarativeRecipientList {
208        expression: LanguageExpressionDef,
209        delimiter: String,
210        parallel: bool,
211        parallel_limit: Option<usize>,
212        stop_on_exception: bool,
213        aggregation: String,
214    },
215    Delay {
216        config: camel_api::DelayConfig,
217    },
218    /// Runtime loop with closure-based predicate (programmatic DSL).
219    Loop {
220        config: LoopConfig,
221        steps: Vec<BuilderStep>,
222    },
223    /// Declarative loop with optional language-based while predicate (YAML DSL).
224    DeclarativeLoop {
225        count: Option<usize>,
226        while_predicate: Option<LanguageExpressionDef>,
227        steps: Vec<BuilderStep>,
228        max_iterations: Option<usize>,
229    },
230    /// EIP-7 enrich: synchronous content enrichment via a resolved producer.
231    Enrich {
232        uri: String,
233        strategy: Option<String>,
234        timeout_ms: Option<u64>,
235    },
236    /// EIP-7 pollEnrich: blocking poll of a PollingConsumer with timeout.
237    PollEnrich {
238        uri: String,
239        strategy: Option<String>,
240        timeout_ms: Option<u64>,
241    },
242    /// Validate step: evaluates a language expression as predicate.
243    /// Exchange passes if predicate returns true; else CamelError::ValidationError.
244    Validate {
245        predicate: LanguageExpressionDef,
246    },
247    /// Claim Check step (EIP). Transforms the exchange body to/from a
248    /// `ClaimCheckRepository` by key. Process-mode, no child pipeline.
249    /// `filter` enables selective merge-back of body/headers during checkout.
250    ClaimCheck {
251        repository: String,
252        operation: String,
253        key: LanguageExpressionDef,
254        filter: Option<String>,
255    },
256    /// Sampling step (EIP). Passes 1 of every N exchanges (counter-based,
257    /// deterministic). Non-sampled exchanges get CamelStop=true (drop semantics).
258    /// Process-mode, stateless. No StepLifecycle — counter is route-scoped.
259    Sampling {
260        period: usize,
261    },
262    /// Sort step (EIP). Orders a body array by extracting a sort key
263    /// from each element via a language expression. Process-mode, stateless.
264    Sort {
265        expression: LanguageExpressionDef,
266        reverse: bool,
267    },
268    /// Idempotent Consumer step (EIP). Wraps a child sub-pipeline that runs
269    /// only when the message-id is NOT present in the named repository.
270    /// Compiled to a `IdempotentConsumerSegment` (OutcomePipeline, segment-mode).
271    IdempotentConsumer {
272        repository: String,
273        expression: LanguageExpressionDef,
274        steps: Vec<BuilderStep>,
275        eager: bool,
276        remove_on_failure: bool,
277    },
278    /// Cache step (EIP). Wraps a child sub-pipeline that runs only on cache
279    /// miss; on hit the stored body is reconstructed and the child is skipped.
280    /// Compiled to a `CacheService` (OutcomePipeline, segment-mode).
281    Cache {
282        repository: Option<String>,
283        key: LanguageExpressionDef,
284        ttl: Option<String>,
285        max_entry_bytes: Option<usize>,
286        on_miss: Vec<BuilderStep>,
287    },
288    /// Cache Invalidate step (EIP). Removes a single cache entry by key.
289    /// Compiled to a `CacheInvalidateService` (OutcomePipeline, segment-mode).
290    CacheInvalidate {
291        repository: Option<String>,
292        key: LanguageExpressionDef,
293    },
294    /// Cache Peek Stale step (EIP). Serves a stale (post-expiry) cache entry.
295    /// Compiled to a `CachePeekStaleService` (OutcomePipeline, segment-mode).
296    CachePeekStale {
297        repository: Option<String>,
298        key: LanguageExpressionDef,
299        on_miss: camel_processor::PeekStaleMissPolicy,
300    },
301    /// Declarative doTry/doCatch/doFinally, resolved at route-add time.
302    DeclarativeDoTry {
303        try_steps: Vec<BuilderStep>,
304        catch: Vec<DoTryCatchClauseBuilder>,
305        finally: Option<DoTryFinallyBuilder>,
306    },
307    /// Resequencer EIP: resequences exchanges by sequence number.
308    /// Must be a top-level step (not nested inside structural EIPs).
309    Resequence {
310        policy_config: ResequencePolicyConfig,
311    },
312}
313
314/// An unresolved route definition. "to" URIs have not been resolved to producers yet.
315pub struct RouteDefinition {
316    pub(crate) from_uri: String,
317    pub(crate) steps: Vec<BuilderStep>,
318    /// Optional per-route error handler config. Takes precedence over the global one.
319    pub(crate) error_handler: Option<ErrorHandlerConfig>,
320    /// Optional circuit breaker config. Applied between error handler and step pipeline.
321    pub(crate) circuit_breaker: Option<CircuitBreakerConfig>,
322    /// Circuit breaker fallback sub-pipeline, as UNRESOLVED steps.
323    ///
324    /// Sibling of [`RouteDefinition::circuit_breaker`]: the DSL layers thread
325    /// `Vec<BuilderStep>` here (same rule as `cache_peek_stale.on_miss`) and
326    /// camel-core compiles it via the `StepCompilerRegistry` when the route is
327    /// compiled. The resolved `CircuitBreakerConfig.fallback` (`BoxProcessor`)
328    /// stays `None` until that compile — the DSL never constructs processors.
329    pub(crate) circuit_breaker_fallback: Vec<BuilderStep>,
330    pub(crate) security_policy: Option<SecurityPolicyConfig>,
331    /// Optional token authenticator for validating JWT/OAuth tokens.
332    pub(crate) security_authenticator: Option<Arc<dyn TokenAuthenticator>>,
333    /// Optional Unit of Work config for in-flight tracking and completion hooks.
334    pub(crate) unit_of_work: Option<UnitOfWorkConfig>,
335    /// User override for the consumer's concurrency model. `None` means
336    /// "use whatever the consumer declares".
337    pub(crate) concurrency: Option<ConcurrencyModel>,
338    /// Unique identifier for this route. Required.
339    pub(crate) route_id: String,
340    /// Whether this route should start automatically when the context starts.
341    pub(crate) auto_startup: bool,
342    /// Order in which routes are started. Lower values start first.
343    pub(crate) startup_order: i32,
344    pub(crate) source_hash: Option<u64>,
345}
346
347impl RouteDefinition {
348    /// Create a new route definition with the required route ID.
349    pub fn new(from_uri: impl Into<String>, steps: Vec<BuilderStep>) -> Self {
350        Self {
351            from_uri: from_uri.into(),
352            steps,
353            error_handler: None,
354            circuit_breaker: None,
355            circuit_breaker_fallback: Vec::new(),
356            security_policy: None,
357            security_authenticator: None,
358            unit_of_work: None,
359            concurrency: None,
360            route_id: String::new(), // Will be set by with_route_id()
361            auto_startup: true,
362            startup_order: 1000,
363            source_hash: None,
364        }
365    }
366
367    /// The source endpoint URI.
368    pub fn from_uri(&self) -> &str {
369        &self.from_uri
370    }
371
372    /// The steps in this route definition.
373    pub fn steps(&self) -> &[BuilderStep] {
374        &self.steps
375    }
376
377    /// Transform the step list, consuming and returning self.
378    /// Used for post-parse instrumentation (e.g. benchmark timing injection
379    /// around `To` steps). The field stays private; callers rebuild via this
380    /// consuming method rather than holding a `&mut` alias.
381    pub fn map_steps(mut self, f: impl FnOnce(Vec<BuilderStep>) -> Vec<BuilderStep>) -> Self {
382        self.steps = f(self.steps);
383        self
384    }
385
386    /// Set a per-route error handler, overriding the global one.
387    pub fn with_error_handler(mut self, config: ErrorHandlerConfig) -> Self {
388        self.error_handler = Some(config);
389        self
390    }
391
392    /// Get the route-level error handler config, if set.
393    pub fn error_handler_config(&self) -> Option<&ErrorHandlerConfig> {
394        self.error_handler.as_ref()
395    }
396
397    /// Set a circuit breaker for this route.
398    pub fn with_circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
399        self.circuit_breaker = Some(config);
400        self
401    }
402
403    /// Set the circuit breaker fallback sub-pipeline (unresolved steps).
404    ///
405    /// Mirror of [`RouteDefinition::with_circuit_breaker`]: the steps are
406    /// compiled by camel-core at route-compile time (registry monopoly) and
407    /// attached to the resolved `CircuitBreakerConfig.fallback`. Empty when no
408    /// CB is configured or the CB has no fallback.
409    pub fn with_circuit_breaker_fallback(mut self, steps: Vec<BuilderStep>) -> Self {
410        self.circuit_breaker_fallback = steps;
411        self
412    }
413
414    /// Set a security policy for this route.
415    pub fn with_security_policy(mut self, config: SecurityPolicyConfig) -> Self {
416        self.security_policy = Some(config);
417        self
418    }
419
420    /// Set a token authenticator for this route.
421    pub fn with_security_authenticator(
422        mut self,
423        authenticator: Arc<dyn TokenAuthenticator>,
424    ) -> Self {
425        self.security_authenticator = Some(authenticator);
426        self
427    }
428
429    /// Set a unit of work config for this route.
430    pub fn with_unit_of_work(mut self, config: UnitOfWorkConfig) -> Self {
431        self.unit_of_work = Some(config);
432        self
433    }
434
435    /// Get the unit of work config, if set.
436    pub fn unit_of_work_config(&self) -> Option<&UnitOfWorkConfig> {
437        self.unit_of_work.as_ref()
438    }
439
440    /// Get the circuit breaker config, if set.
441    pub fn circuit_breaker_config(&self) -> Option<&CircuitBreakerConfig> {
442        self.circuit_breaker.as_ref()
443    }
444
445    pub fn security_policy_config(&self) -> Option<&SecurityPolicyConfig> {
446        self.security_policy.as_ref()
447    }
448
449    pub fn security_authenticator(&self) -> Option<&Arc<dyn TokenAuthenticator>> {
450        self.security_authenticator.as_ref()
451    }
452
453    /// User-specified concurrency override, if any.
454    pub fn concurrency_override(&self) -> Option<&ConcurrencyModel> {
455        self.concurrency.as_ref()
456    }
457
458    /// Override the consumer's concurrency model for this route.
459    pub fn with_concurrency(mut self, model: ConcurrencyModel) -> Self {
460        self.concurrency = Some(model);
461        self
462    }
463
464    /// Get the route ID.
465    pub fn route_id(&self) -> &str {
466        &self.route_id
467    }
468
469    /// Whether this route should start automatically when the context starts.
470    pub fn auto_startup(&self) -> bool {
471        self.auto_startup
472    }
473
474    /// Order in which routes are started. Lower values start first.
475    pub fn startup_order(&self) -> i32 {
476        self.startup_order
477    }
478
479    /// Set a unique identifier for this route.
480    pub fn with_route_id(mut self, id: impl Into<String>) -> Self {
481        self.route_id = id.into();
482        self
483    }
484
485    /// Set whether this route should start automatically.
486    pub fn with_auto_startup(mut self, auto: bool) -> Self {
487        self.auto_startup = auto;
488        self
489    }
490
491    /// Set the startup order. Lower values start first.
492    pub fn with_startup_order(mut self, order: i32) -> Self {
493        self.startup_order = order;
494        self
495    }
496
497    pub fn with_source_hash(mut self, hash: u64) -> Self {
498        self.source_hash = Some(hash);
499        self
500    }
501
502    pub fn source_hash(&self) -> Option<u64> {
503        self.source_hash
504    }
505
506    /// Extract the metadata fields needed for introspection.
507    /// This is used by RouteController to store route info without the non-Sync steps.
508    pub fn to_info(&self) -> RouteDefinitionInfo {
509        RouteDefinitionInfo {
510            route_id: self.route_id.clone(),
511            auto_startup: self.auto_startup,
512            startup_order: self.startup_order,
513            source_hash: self.source_hash,
514        }
515    }
516}
517
518/// Minimal route definition metadata for introspection.
519///
520/// This struct contains only the metadata fields from [`RouteDefinition`]
521/// that are needed for route lifecycle management, without the `steps` field
522/// (which contains non-Sync types and cannot be stored in a Sync struct).
523#[derive(Clone)]
524pub struct RouteDefinitionInfo {
525    route_id: String,
526    auto_startup: bool,
527    startup_order: i32,
528    pub(crate) source_hash: Option<u64>,
529}
530
531impl RouteDefinitionInfo {
532    /// Get the route ID.
533    pub fn route_id(&self) -> &str {
534        &self.route_id
535    }
536
537    /// Whether this route should start automatically when the context starts.
538    pub fn auto_startup(&self) -> bool {
539        self.auto_startup
540    }
541
542    /// Order in which routes are started. Lower values start first.
543    pub fn startup_order(&self) -> i32 {
544        self.startup_order
545    }
546
547    pub fn source_hash(&self) -> Option<u64> {
548        self.source_hash
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    /// Golden debug output for EVERY BuilderStep variant.
557    ///
558    /// Before refactoring the manual `Debug` impl to `#[derive(Debug)]`, this test
559    /// locks the exact output of every variant so the refactor can be verified.
560    #[test]
561    fn golden_debug_output_all_variants() {
562        use camel_api::declarative::LanguageExpressionDef;
563        use camel_api::loop_eip::LoopMode;
564        use camel_api::recipient_list::RecipientListConfig;
565        use camel_api::splitter::{AggregationStrategy, StreamSplitConfig, StreamSplitFormat};
566        use camel_api::{
567            BoxProcessor, DynamicRouterConfig, Exchange, FilterPredicate, FunctionDefinition,
568            FunctionId, IdentityProcessor, MulticastConfig, OpaqueProcessor, RoutingSlipConfig,
569            Value,
570        };
571        use std::sync::Arc;
572
573        let expr = LanguageExpressionDef {
574            language: "simple".into(),
575            source: "${body}".into(),
576        };
577
578        // -- group A: trivial / non-recursive ----------------------------------
579        // derive(Debug) emits the variant without the enum name for unit/tuple variants.
580        assert_eq!(format!("{:?}", BuilderStep::Stop), "Stop");
581        assert_eq!(
582            format!(
583                "{:?}",
584                BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(IdentityProcessor)))
585            ),
586            "Processor(BoxProcessor(...))"
587        );
588        assert_eq!(
589            format!("{:?}", BuilderStep::To("mock:out".into())),
590            "To(\"mock:out\")"
591        );
592
593        // -- group B: variant with named fields (no sub-steps) -----------------
594        // derive(Debug) emits ALL fields, not the redacted `..` form.
595        assert_eq!(
596            format!(
597                "{:?}",
598                BuilderStep::Log {
599                    level: camel_processor::LogLevel::Info,
600                    message: "hello".into(),
601                }
602            ),
603            "Log { level: Info, message: \"hello\" }"
604        );
605
606        assert_eq!(
607            format!(
608                "{:?}",
609                BuilderStep::DeclarativeSetHeader {
610                    key: "k".into(),
611                    value: ValueSourceDef::Literal(Value::String("v".into())),
612                }
613            ),
614            "DeclarativeSetHeader { key: \"k\", value: Literal(String(\"v\")) }"
615        );
616
617        assert_eq!(
618            format!(
619                "{:?}",
620                BuilderStep::DeclarativeSetHeaderIfAbsent {
621                    key: "k".into(),
622                    value: ValueSourceDef::Literal(Value::String("v".into())),
623                }
624            ),
625            "DeclarativeSetHeaderIfAbsent { key: \"k\", value: Literal(String(\"v\")) }"
626        );
627
628        assert_eq!(
629            format!(
630                "{:?}",
631                BuilderStep::DeclarativeSetBody {
632                    value: ValueSourceDef::Literal(Value::String("v".into())),
633                }
634            ),
635            "DeclarativeSetBody { value: Literal(String(\"v\")) }"
636        );
637
638        assert_eq!(
639            format!(
640                "{:?}",
641                BuilderStep::DeclarativeSetProperty {
642                    key: "prop".into(),
643                    value_source: ValueSourceDef::Literal(Value::String("v".into())),
644                }
645            ),
646            "DeclarativeSetProperty { key: \"prop\", value_source: Literal(String(\"v\")) }"
647        );
648
649        assert_eq!(
650            format!(
651                "{:?}",
652                BuilderStep::DeclarativeScript {
653                    expression: expr.clone(),
654                }
655            ),
656            "DeclarativeScript { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" } }"
657        );
658
659        // DeclarativeFunction needs a FunctionDefinition
660        let func_def = FunctionDefinition {
661            id: FunctionId("test-id".into()),
662            runtime: "my_runtime".into(),
663            source: "${body}".into(),
664            timeout_ms: 5000,
665            route_id: None,
666            step_index: None,
667        };
668        assert_eq!(
669            format!(
670                "{:?}",
671                BuilderStep::DeclarativeFunction {
672                    definition: func_def,
673                }
674            ),
675            "DeclarativeFunction { definition: FunctionDefinition { id: FunctionId(\"test-id\"), runtime: \"my_runtime\", source: \"${body}\", timeout_ms: 5000, route_id: None, step_index: None } }"
676        );
677
678        assert_eq!(
679            format!(
680                "{:?}",
681                BuilderStep::WireTap {
682                    uri: "mock:tap".into(),
683                }
684            ),
685            "WireTap { uri: \"mock:tap\" }"
686        );
687
688        assert_eq!(
689            format!(
690                "{:?}",
691                BuilderStep::DeclarativeLog {
692                    level: camel_processor::LogLevel::Info,
693                    message: ValueSourceDef::Expression(expr.clone()),
694                }
695            ),
696            "DeclarativeLog { level: Info, message: Expression(LanguageExpressionDef { language: \"simple\", source: \"${body}\" }) }"
697        );
698
699        assert_eq!(
700            format!(
701                "{:?}",
702                BuilderStep::Bean {
703                    name: "myBean".into(),
704                    method: "process".into(),
705                }
706            ),
707            "Bean { name: \"myBean\", method: \"process\" }"
708        );
709
710        assert_eq!(
711            format!(
712                "{:?}",
713                BuilderStep::Script {
714                    language: "js".into(),
715                    script: "body".into(),
716                }
717            ),
718            "Script { language: \"js\", script: \"body\" }"
719        );
720
721        assert_eq!(
722            format!(
723                "{:?}",
724                BuilderStep::Aggregate {
725                    config: camel_api::AggregatorConfig::correlate_by("id")
726                        .complete_when_size(1)
727                        .build()
728                        .unwrap(),
729                }
730            ),
731            "Aggregate { config: AggregatorConfig { header_name: \"id\", completion: Single(Size(1)), correlation: HeaderName(\"id\"), strategy: CollectAll, max_buckets: Some(10000), bucket_ttl: Some(300s), force_completion_on_stop: false, discard_on_timeout: false, max_timeout_tasks: 1024 } }"
732        );
733
734        assert_eq!(
735            format!(
736                "{:?}",
737                BuilderStep::DynamicRouter {
738                    config: DynamicRouterConfig::new(Arc::new(|_: &Exchange| Some(
739                        "mock:dr".into()
740                    ))),
741                }
742            ),
743            "DynamicRouter { config: DynamicRouterConfig { uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false, max_iterations: 1000, timeout: Some(60s) } }"
744        );
745
746        assert_eq!(
747            format!(
748                "{:?}",
749                BuilderStep::RoutingSlip {
750                    config: RoutingSlipConfig::new(Arc::new(|_: &Exchange| Some("mock:rs".into()))),
751                }
752            ),
753            "RoutingSlip { config: RoutingSlipConfig { uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false } }"
754        );
755
756        assert_eq!(
757            format!(
758                "{:?}",
759                BuilderStep::RecipientList {
760                    config: RecipientListConfig::new(Arc::new(|_: &Exchange| String::new())),
761                }
762            ),
763            "RecipientList { config: RecipientListConfig { delimiter: \",\", parallel: false, parallel_limit: None, stop_on_exception: false, max_recipients: 1000 } }"
764        );
765
766        assert_eq!(
767            format!(
768                "{:?}",
769                BuilderStep::Enrich {
770                    uri: "mock:enrich".into(),
771                    strategy: Some("agg".into()),
772                    timeout_ms: Some(1000),
773                }
774            ),
775            "Enrich { uri: \"mock:enrich\", strategy: Some(\"agg\"), timeout_ms: Some(1000) }"
776        );
777
778        assert_eq!(
779            format!(
780                "{:?}",
781                BuilderStep::PollEnrich {
782                    uri: "mock:poll".into(),
783                    strategy: None,
784                    timeout_ms: None,
785                }
786            ),
787            "PollEnrich { uri: \"mock:poll\", strategy: None, timeout_ms: None }"
788        );
789
790        assert_eq!(
791            format!(
792                "{:?}",
793                BuilderStep::Validate {
794                    predicate: expr.clone(),
795                }
796            ),
797            "Validate { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" } }"
798        );
799
800        assert_eq!(
801            format!("{:?}", BuilderStep::Sampling { period: 100 }),
802            "Sampling { period: 100 }"
803        );
804
805        assert_eq!(
806            format!(
807                "{:?}",
808                BuilderStep::Resequence {
809                    policy_config: Default::default(),
810                }
811            ),
812            "Resequence { policy_config: ResequencePolicyConfig { mode: Batch { correlation: \"header.id\", sort: \"header.id\", completion: SizeOrTimeout(100, 30000) } } }"
813        );
814
815        // -- group C: variants with named fields (sub-steps is Vec) ------------
816        assert_eq!(
817            format!(
818                "{:?}",
819                BuilderStep::DeclarativeFilter {
820                    predicate: expr.clone(),
821                    steps: vec![BuilderStep::Stop],
822                }
823            ),
824            "DeclarativeFilter { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [Stop] }"
825        );
826
827        assert_eq!(
828            format!(
829                "{:?}",
830                BuilderStep::DeclarativeSplit {
831                    expression: expr.clone(),
832                    aggregation: AggregationStrategy::Original,
833                    parallel: false,
834                    parallel_limit: Some(2),
835                    stop_on_exception: true,
836                    steps: vec![BuilderStep::Stop],
837                }
838            ),
839            "DeclarativeSplit { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, aggregation: Original, parallel: false, parallel_limit: Some(2), stop_on_exception: true, steps: [Stop] }"
840        );
841
842        assert_eq!(
843            format!(
844                "{:?}",
845                BuilderStep::Split {
846                    config: camel_api::splitter::SplitterConfig::new(
847                        camel_api::splitter::split_body_lines()
848                    ),
849                    steps: vec![BuilderStep::Stop],
850                }
851            ),
852            "Split { config: SplitterConfig { expression: \"<split-expression>\", aggregation: LastWins, parallel: false, parallel_limit: None, stop_on_exception: true, max_fragments: 100000 }, steps: [Stop] }"
853        );
854
855        assert_eq!(
856            format!(
857                "{:?}",
858                BuilderStep::Filter {
859                    predicate: FilterPredicate::new(|_: &Exchange| true),
860                    steps: vec![BuilderStep::Stop],
861                }
862            ),
863            "Filter { predicate: FilterPredicate(..), steps: [Stop] }"
864        );
865
866        assert_eq!(
867            format!(
868                "{:?}",
869                BuilderStep::Throttle {
870                    config: camel_api::ThrottlerConfig::new(
871                        10,
872                        std::time::Duration::from_millis(10)
873                    ),
874                    steps: vec![BuilderStep::Stop],
875                }
876            ),
877            "Throttle { config: ThrottlerConfig { max_requests: 10, period: 10ms, strategy: Delay }, steps: [Stop] }"
878        );
879
880        assert_eq!(
881            format!(
882                "{:?}",
883                BuilderStep::LoadBalance {
884                    config: camel_api::LoadBalancerConfig::round_robin(),
885                    steps: vec![BuilderStep::To("mock:l1".into())],
886                }
887            ),
888            "LoadBalance { config: LoadBalancerConfig { strategy: RoundRobin }, steps: [To(\"mock:l1\")] }"
889        );
890
891        assert_eq!(
892            format!(
893                "{:?}",
894                BuilderStep::Delay {
895                    config: camel_api::DelayConfig::new(500),
896                }
897            ),
898            "Delay { config: DelayConfig { delay_ms: 500, dynamic_header: None, max_delay_ms: 3600000 } }"
899        );
900
901        // -- group D: Choice / DeclarativeChoice --------------------------------
902        // derive(Debug) emits full field enumeration; WhenStep Debug is a
903        // full struct listing; nested BuilderStep::Stop stays as `Stop`.
904        assert_eq!(
905            format!(
906                "{:?}",
907                BuilderStep::Choice {
908                    whens: vec![WhenStep {
909                        predicate: FilterPredicate::new(|_: &Exchange| true),
910                        steps: vec![BuilderStep::To("mock:a".into())],
911                    }],
912                    otherwise: None,
913                }
914            ),
915            "Choice { whens: [WhenStep { predicate: FilterPredicate(..), steps: [To(\"mock:a\")] }], otherwise: None }"
916        );
917
918        assert_eq!(
919            format!(
920                "{:?}",
921                BuilderStep::DeclarativeChoice {
922                    whens: vec![DeclarativeWhenStep {
923                        predicate: expr.clone(),
924                        steps: vec![BuilderStep::Stop],
925                    }],
926                    otherwise: Some(vec![BuilderStep::Stop]),
927                }
928            ),
929            "DeclarativeChoice { whens: [DeclarativeWhenStep { predicate: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [Stop] }], otherwise: Some([Stop]) }"
930        );
931
932        // -- group E: Multicast -------------------------------------------------
933        assert_eq!(
934            format!(
935                "{:?}",
936                BuilderStep::Multicast {
937                    steps: vec![BuilderStep::To("direct:a".into())],
938                    config: MulticastConfig::new(),
939                }
940            ),
941            "Multicast { steps: [To(\"direct:a\")], config: MulticastConfig { parallel: false, parallel_limit: None, stop_on_exception: false, timeout: None, aggregation: LastWins } }"
942        );
943
944        // -- group F: DeclarativeDynamicRouter / DeclarativeRoutingSlip --------
945        assert_eq!(
946            format!(
947                "{:?}",
948                BuilderStep::DeclarativeDynamicRouter {
949                    expression: expr.clone(),
950                    uri_delimiter: ",".into(),
951                    cache_size: 1000,
952                    ignore_invalid_endpoints: false,
953                    max_iterations: 1000,
954                }
955            ),
956            "DeclarativeDynamicRouter { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false, max_iterations: 1000 }"
957        );
958
959        assert_eq!(
960            format!(
961                "{:?}",
962                BuilderStep::DeclarativeRoutingSlip {
963                    expression: expr.clone(),
964                    uri_delimiter: ",".into(),
965                    cache_size: 1000,
966                    ignore_invalid_endpoints: false,
967                }
968            ),
969            "DeclarativeRoutingSlip { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, uri_delimiter: \",\", cache_size: 1000, ignore_invalid_endpoints: false }"
970        );
971
972        // -- group G: DeclarativeRecipientList ---------------------------------
973        assert_eq!(
974            format!(
975                "{:?}",
976                BuilderStep::DeclarativeRecipientList {
977                    expression: expr.clone(),
978                    delimiter: ",".into(),
979                    parallel: false,
980                    parallel_limit: None,
981                    stop_on_exception: false,
982                    aggregation: "original".into(),
983                }
984            ),
985            "DeclarativeRecipientList { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, delimiter: \",\", parallel: false, parallel_limit: None, stop_on_exception: false, aggregation: \"original\" }"
986        );
987
988        // -- group H: Loop / DeclarativeLoop -----------------------------------
989        assert_eq!(
990            format!(
991                "{:?}",
992                BuilderStep::Loop {
993                    config: camel_api::loop_eip::LoopConfig::new(LoopMode::Count(3)),
994                    steps: vec![],
995                }
996            ),
997            "Loop { config: LoopConfig { mode: Count(3), max_iterations: 10000 }, steps: [] }"
998        );
999
1000        assert_eq!(
1001            format!(
1002                "{:?}",
1003                BuilderStep::DeclarativeLoop {
1004                    count: Some(5),
1005                    while_predicate: None,
1006                    steps: vec![],
1007                    max_iterations: Some(100),
1008                }
1009            ),
1010            "DeclarativeLoop { count: Some(5), while_predicate: None, steps: [], max_iterations: Some(100) }"
1011        );
1012
1013        // -- group I: ClaimCheck -----------------------------------------------
1014        assert_eq!(
1015            format!(
1016                "{:?}",
1017                BuilderStep::ClaimCheck {
1018                    repository: "myRepo".into(),
1019                    operation: "checkout".into(),
1020                    key: expr.clone(),
1021                    filter: None,
1022                }
1023            ),
1024            "ClaimCheck { repository: \"myRepo\", operation: \"checkout\", key: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, filter: None }"
1025        );
1026
1027        // -- group J: Sort -----------------------------------------------------
1028        assert_eq!(
1029            format!(
1030                "{:?}",
1031                BuilderStep::Sort {
1032                    expression: expr.clone(),
1033                    reverse: false,
1034                }
1035            ),
1036            "Sort { expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, reverse: false }"
1037        );
1038
1039        // -- group K: IdempotentConsumer ---------------------------------------
1040        assert_eq!(
1041            format!(
1042                "{:?}",
1043                BuilderStep::IdempotentConsumer {
1044                    repository: "myRepo".into(),
1045                    expression: expr.clone(),
1046                    steps: vec![],
1047                    eager: true,
1048                    remove_on_failure: false,
1049                }
1050            ),
1051            "IdempotentConsumer { repository: \"myRepo\", expression: LanguageExpressionDef { language: \"simple\", source: \"${body}\" }, steps: [], eager: true, remove_on_failure: false }"
1052        );
1053
1054        // -- group L: DeclarativeDoTry -----------------------------------------
1055        assert_eq!(
1056            format!(
1057                "{:?}",
1058                BuilderStep::DeclarativeDoTry {
1059                    try_steps: vec![BuilderStep::Stop],
1060                    catch: vec![],
1061                    finally: None,
1062                }
1063            ),
1064            "DeclarativeDoTry { try_steps: [Stop], catch: [], finally: None }"
1065        );
1066
1067        // -- group M: DeclarativeStreamSplit -----------------------------------
1068        assert_eq!(
1069            format!(
1070                "{:?}",
1071                BuilderStep::DeclarativeStreamSplit {
1072                    stream_config: StreamSplitConfig {
1073                        format: StreamSplitFormat::Ndjson,
1074                        max_record_bytes: 1024 * 1024,
1075                        batch_size: 1,
1076                        chunk_size: None,
1077                        include_origin: true,
1078                    },
1079                    aggregation: AggregationStrategy::Original,
1080                    stop_on_exception: true,
1081                    steps: vec![BuilderStep::Stop],
1082                }
1083            ),
1084            "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] }"
1085        );
1086    }
1087
1088    #[test]
1089    fn test_builder_step_multicast_variant() {
1090        use camel_api::MulticastConfig;
1091
1092        let step = BuilderStep::Multicast {
1093            steps: vec![BuilderStep::To("direct:a".into())],
1094            config: MulticastConfig::new(),
1095        };
1096
1097        assert!(matches!(step, BuilderStep::Multicast { .. }));
1098    }
1099
1100    #[test]
1101    fn test_route_definition_defaults() {
1102        let def = RouteDefinition::new("direct:test", vec![]).with_route_id("test-route");
1103        assert_eq!(def.route_id(), "test-route");
1104        assert!(def.auto_startup());
1105        assert_eq!(def.startup_order(), 1000);
1106    }
1107
1108    #[test]
1109    fn test_route_definition_builders() {
1110        let def = RouteDefinition::new("direct:test", vec![])
1111            .with_route_id("my-route")
1112            .with_auto_startup(false)
1113            .with_startup_order(50);
1114        assert_eq!(def.route_id(), "my-route");
1115        assert!(!def.auto_startup());
1116        assert_eq!(def.startup_order(), 50);
1117    }
1118
1119    #[test]
1120    fn test_route_definition_accessors_cover_core_fields() {
1121        let def = RouteDefinition::new("direct:in", vec![BuilderStep::To("mock:out".into())])
1122            .with_route_id("accessor-route");
1123
1124        assert_eq!(def.from_uri(), "direct:in");
1125        assert_eq!(def.steps().len(), 1);
1126        assert!(matches!(def.steps()[0], BuilderStep::To(_)));
1127    }
1128
1129    #[test]
1130    fn test_route_definition_error_handler_circuit_breaker_and_concurrency_accessors() {
1131        use camel_api::circuit_breaker::CircuitBreakerConfig;
1132        use camel_api::error_handler::ErrorHandlerConfig;
1133        use camel_component_api::ConcurrencyModel;
1134
1135        let def = RouteDefinition::new("direct:test", vec![])
1136            .with_route_id("eh-route")
1137            .with_error_handler(ErrorHandlerConfig::dead_letter_channel("log:dlc"))
1138            .with_circuit_breaker(CircuitBreakerConfig::new())
1139            .with_concurrency(ConcurrencyModel::Concurrent { max: Some(4) });
1140
1141        let eh = def
1142            .error_handler_config()
1143            .expect("error handler should be set");
1144        assert_eq!(eh.dlc_uri.as_deref(), Some("log:dlc"));
1145        assert!(def.circuit_breaker_config().is_some());
1146        assert!(matches!(
1147            def.concurrency_override(),
1148            Some(ConcurrencyModel::Concurrent { max: Some(4) })
1149        ));
1150    }
1151
1152    #[test]
1153    fn test_builder_step_debug_covers_many_variants() {
1154        use camel_api::splitter::{AggregationStrategy, SplitterConfig, split_body_lines};
1155        use camel_api::{
1156            BoxProcessor, DynamicRouterConfig, Exchange, FilterPredicate, IdentityProcessor,
1157            OpaqueProcessor, RoutingSlipConfig, Value,
1158        };
1159        use std::sync::Arc;
1160
1161        let expr = LanguageExpressionDef {
1162            language: "simple".into(),
1163            source: "${body}".into(),
1164        };
1165
1166        let steps = vec![
1167            BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(IdentityProcessor))),
1168            BuilderStep::To("mock:out".into()),
1169            BuilderStep::Stop,
1170            BuilderStep::Log {
1171                level: camel_processor::LogLevel::Info,
1172                message: "hello".into(),
1173            },
1174            BuilderStep::DeclarativeSetHeader {
1175                key: "k".into(),
1176                value: ValueSourceDef::Literal(Value::String("v".into())),
1177            },
1178            BuilderStep::DeclarativeSetBody {
1179                value: ValueSourceDef::Expression(expr.clone()),
1180            },
1181            BuilderStep::DeclarativeFilter {
1182                predicate: expr.clone(),
1183                steps: vec![BuilderStep::Stop],
1184            },
1185            BuilderStep::DeclarativeChoice {
1186                whens: vec![DeclarativeWhenStep {
1187                    predicate: expr.clone(),
1188                    steps: vec![BuilderStep::Stop],
1189                }],
1190                otherwise: Some(vec![BuilderStep::Stop]),
1191            },
1192            BuilderStep::DeclarativeScript {
1193                expression: expr.clone(),
1194            },
1195            BuilderStep::DeclarativeSplit {
1196                expression: expr.clone(),
1197                aggregation: AggregationStrategy::Original,
1198                parallel: false,
1199                parallel_limit: Some(2),
1200                stop_on_exception: true,
1201                steps: vec![BuilderStep::Stop],
1202            },
1203            BuilderStep::Split {
1204                config: SplitterConfig::new(split_body_lines()),
1205                steps: vec![BuilderStep::Stop],
1206            },
1207            BuilderStep::Aggregate {
1208                config: camel_api::AggregatorConfig::correlate_by("id")
1209                    .complete_when_size(1)
1210                    .build()
1211                    .unwrap(),
1212            },
1213            BuilderStep::Filter {
1214                predicate: FilterPredicate::new(|_: &Exchange| true),
1215                steps: vec![BuilderStep::Stop],
1216            },
1217            BuilderStep::WireTap {
1218                uri: "mock:tap".into(),
1219            },
1220            BuilderStep::DeclarativeLog {
1221                level: camel_processor::LogLevel::Info,
1222                message: ValueSourceDef::Expression(expr.clone()),
1223            },
1224            BuilderStep::Bean {
1225                name: "bean".into(),
1226                method: "call".into(),
1227            },
1228            BuilderStep::Script {
1229                language: "rhai".into(),
1230                script: "body".into(),
1231            },
1232            BuilderStep::Throttle {
1233                config: camel_api::ThrottlerConfig::new(10, std::time::Duration::from_millis(10)),
1234                steps: vec![BuilderStep::Stop],
1235            },
1236            BuilderStep::LoadBalance {
1237                config: camel_api::LoadBalancerConfig::round_robin(),
1238                steps: vec![BuilderStep::To("mock:l1".into())],
1239            },
1240            BuilderStep::DynamicRouter {
1241                config: DynamicRouterConfig::new(Arc::new(|_| Some("mock:dr".into()))),
1242            },
1243            BuilderStep::RoutingSlip {
1244                config: RoutingSlipConfig::new(Arc::new(|_| Some("mock:rs".into()))),
1245            },
1246        ];
1247
1248        for step in steps {
1249            let dbg = format!("{step:?}");
1250            assert!(!dbg.is_empty());
1251        }
1252    }
1253
1254    #[test]
1255    fn test_route_definition_to_info_preserves_metadata() {
1256        let info = RouteDefinition::new("direct:test", vec![])
1257            .with_route_id("meta-route")
1258            .with_auto_startup(false)
1259            .with_startup_order(7)
1260            .to_info();
1261
1262        assert_eq!(info.route_id(), "meta-route");
1263        assert!(!info.auto_startup());
1264        assert_eq!(info.startup_order(), 7);
1265    }
1266
1267    #[test]
1268    fn test_choice_builder_step_debug() {
1269        use camel_api::FilterPredicate;
1270
1271        fn always_true(_: &camel_api::Exchange) -> bool {
1272            true
1273        }
1274
1275        let step = BuilderStep::Choice {
1276            whens: vec![WhenStep {
1277                predicate: FilterPredicate::new(always_true),
1278                steps: vec![BuilderStep::To("mock:a".into())],
1279            }],
1280            otherwise: None,
1281        };
1282        let debug = format!("{step:?}");
1283        assert!(debug.contains("Choice"));
1284    }
1285
1286    #[test]
1287    fn test_route_definition_unit_of_work() {
1288        use camel_api::UnitOfWorkConfig;
1289        let config = UnitOfWorkConfig {
1290            on_complete: Some("log:complete".into()),
1291            on_failure: Some("log:failed".into()),
1292        };
1293        let def = RouteDefinition::new("direct:test", vec![])
1294            .with_route_id("uow-test")
1295            .with_unit_of_work(config.clone());
1296        assert_eq!(
1297            def.unit_of_work_config().unwrap().on_complete.as_deref(),
1298            Some("log:complete")
1299        );
1300        assert_eq!(
1301            def.unit_of_work_config().unwrap().on_failure.as_deref(),
1302            Some("log:failed")
1303        );
1304
1305        let def_no_uow = RouteDefinition::new("direct:test", vec![]).with_route_id("no-uow");
1306        assert!(def_no_uow.unit_of_work_config().is_none());
1307    }
1308
1309    #[test]
1310    fn test_route_definition_security_policy_accessor() {
1311        use async_trait::async_trait;
1312        use camel_api::CamelError;
1313        use camel_api::Exchange;
1314        use camel_api::security_policy::{
1315            AuthorizationDecision, Principal, SecurityPolicy, SecurityPolicyConfig,
1316        };
1317
1318        struct StubPolicy;
1319        #[async_trait]
1320        impl SecurityPolicy for StubPolicy {
1321            async fn evaluate(
1322                &self,
1323                _exchange: &mut Exchange,
1324            ) -> Result<AuthorizationDecision, CamelError> {
1325                Ok(AuthorizationDecision::Granted {
1326                    principal: Principal {
1327                        subject: "test".into(),
1328                        issuer: "test".into(),
1329                        audience: vec![],
1330                        scopes: vec![],
1331                        roles: vec![],
1332                        claims: serde_json::Value::Null,
1333                    },
1334                })
1335            }
1336        }
1337
1338        let def_no_sp = RouteDefinition::new("direct:test", vec![]).with_route_id("no-sp");
1339        assert!(def_no_sp.security_policy_config().is_none());
1340
1341        let def = RouteDefinition::new("direct:test", vec![])
1342            .with_route_id("sp-test")
1343            .with_security_policy(SecurityPolicyConfig::new(StubPolicy));
1344        assert!(def.security_policy_config().is_some());
1345    }
1346
1347    #[test]
1348    fn test_route_definition_security_authenticator_accessor() {
1349        use camel_api::security_policy::Principal;
1350
1351        struct TestAuth;
1352        #[async_trait::async_trait]
1353        impl TokenAuthenticator for TestAuth {
1354            async fn authenticate_bearer(
1355                &self,
1356                _token: &str,
1357            ) -> Result<Principal, camel_api::CamelError> {
1358                Ok(Principal {
1359                    subject: "test".into(),
1360                    issuer: "test".into(),
1361                    audience: vec![],
1362                    scopes: vec![],
1363                    roles: vec![],
1364                    claims: serde_json::Value::Null,
1365                })
1366            }
1367        }
1368
1369        let def_no_auth = RouteDefinition::new("direct:test".to_string(), vec![]);
1370        assert!(def_no_auth.security_authenticator().is_none());
1371
1372        let auth = Arc::new(TestAuth);
1373        let def = RouteDefinition::new("direct:test".to_string(), vec![])
1374            .with_security_authenticator(auth);
1375        assert!(def.security_authenticator().is_some());
1376    }
1377
1378    #[test]
1379    fn test_map_steps_swaps_steps_and_preserves_other_fields() {
1380        let original = RouteDefinition::new(
1381            "direct:test".to_string(),
1382            vec![BuilderStep::To("mock:a".into()), BuilderStep::Stop],
1383        )
1384        .with_route_id("my-route");
1385
1386        let mapped = original.map_steps(|steps| {
1387            let mut out = Vec::with_capacity(steps.len() + 1);
1388            out.push(BuilderStep::To("mock:prefix".into()));
1389            out.extend(steps);
1390            out
1391        });
1392
1393        // Steps were transformed.
1394        assert_eq!(mapped.steps().len(), 3);
1395        assert!(matches!(mapped.steps()[0], BuilderStep::To(ref s) if s == "mock:prefix"));
1396        assert!(matches!(mapped.steps()[1], BuilderStep::To(ref s) if s == "mock:a"));
1397        // Other fields preserved.
1398        assert_eq!(mapped.route_id(), "my-route");
1399    }
1400}