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