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, BoxProcessor, FilterPredicate, MulticastConfig, 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.
19pub struct WhenStep {
20    pub predicate: FilterPredicate,
21    pub steps: Vec<BuilderStep>,
22}
23
24pub use camel_api::declarative::{LanguageExpressionDef, ValueSourceDef};
25
26/// Declarative `when` clause resolved later by the runtime.
27#[derive(Debug)]
28pub struct DeclarativeWhenStep {
29    pub predicate: LanguageExpressionDef,
30    pub steps: Vec<BuilderStep>,
31}
32
33/// Builder struct for a single `doCatch` clause in the declarative pipeline.
34#[derive(Debug)]
35pub struct DoTryCatchClauseBuilder {
36    pub exception: Option<Vec<String>>,
37    pub when: Option<LanguageExpressionDef>,
38    pub on_when: Option<LanguageExpressionDef>,
39    pub disposition: camel_api::error_handler::ExceptionDisposition,
40    pub steps: Vec<BuilderStep>,
41}
42
43/// Builder struct for the `doFinally` block in the declarative pipeline.
44#[derive(Debug)]
45pub struct DoTryFinallyBuilder {
46    pub on_when: Option<LanguageExpressionDef>,
47    pub steps: Vec<BuilderStep>,
48}
49
50/// A step in an unresolved route definition.
51pub enum BuilderStep {
52    /// A pre-built Tower processor service.
53    Processor(BoxProcessor),
54    /// A destination URI — resolved at start time by CamelContext.
55    To(String),
56    /// A stop step that halts processing immediately.
57    Stop,
58    /// A static log step.
59    Log {
60        level: camel_processor::LogLevel,
61        message: String,
62    },
63    /// Declarative set_header (literal or language-based value), resolved at route-add time.
64    DeclarativeSetHeader {
65        key: String,
66        value: ValueSourceDef,
67    },
68    DeclarativeSetProperty {
69        key: String,
70        value_source: ValueSourceDef,
71    },
72    /// Declarative set_body (literal or language-based value), resolved at route-add time.
73    DeclarativeSetBody {
74        value: ValueSourceDef,
75    },
76    /// Declarative filter using a language predicate, resolved at route-add time.
77    DeclarativeFilter {
78        predicate: LanguageExpressionDef,
79        steps: Vec<BuilderStep>,
80    },
81    /// Declarative choice/when/otherwise using language predicates, resolved at route-add time.
82    DeclarativeChoice {
83        whens: Vec<DeclarativeWhenStep>,
84        otherwise: Option<Vec<BuilderStep>>,
85    },
86    /// Declarative script step evaluated by language and written to body.
87    DeclarativeScript {
88        expression: LanguageExpressionDef,
89    },
90    DeclarativeFunction {
91        definition: camel_api::FunctionDefinition,
92    },
93    /// Declarative split using a language expression, resolved at route-add time.
94    DeclarativeSplit {
95        expression: LanguageExpressionDef,
96        aggregation: camel_api::splitter::AggregationStrategy,
97        parallel: bool,
98        parallel_limit: Option<usize>,
99        stop_on_exception: bool,
100        steps: Vec<BuilderStep>,
101    },
102    /// Declarative stream split using a streaming split expression, resolved at route-add time.
103    DeclarativeStreamSplit {
104        stream_config: camel_api::StreamSplitConfig,
105        aggregation: camel_api::splitter::AggregationStrategy,
106        stop_on_exception: bool,
107        steps: Vec<BuilderStep>,
108    },
109    DeclarativeDynamicRouter {
110        expression: LanguageExpressionDef,
111        uri_delimiter: String,
112        cache_size: i32,
113        ignore_invalid_endpoints: bool,
114        max_iterations: usize,
115    },
116    DeclarativeRoutingSlip {
117        expression: LanguageExpressionDef,
118        uri_delimiter: String,
119        cache_size: i32,
120        ignore_invalid_endpoints: bool,
121    },
122    /// A Splitter sub-pipeline: config + nested steps to execute per fragment.
123    Split {
124        config: SplitterConfig,
125        steps: Vec<BuilderStep>,
126    },
127    /// An Aggregator step: collects exchanges by correlation key, emits when complete.
128    Aggregate {
129        config: AggregatorConfig,
130    },
131    /// A Filter sub-pipeline: predicate + nested steps executed only when predicate is true.
132    Filter {
133        predicate: FilterPredicate,
134        steps: Vec<BuilderStep>,
135    },
136    /// A Choice step: evaluates when-clauses in order, routes to the first match.
137    /// If no when matches, the optional otherwise branch is used.
138    Choice {
139        whens: Vec<WhenStep>,
140        otherwise: Option<Vec<BuilderStep>>,
141    },
142    /// A WireTap step: sends a clone of the exchange to a tap endpoint (fire-and-forget).
143    WireTap {
144        uri: String,
145    },
146    /// A Multicast step: sends the same exchange to multiple destinations.
147    Multicast {
148        steps: Vec<BuilderStep>,
149        config: MulticastConfig,
150    },
151    /// Declarative log step with a language-evaluated message, resolved at route-add time.
152    DeclarativeLog {
153        level: camel_processor::LogLevel,
154        message: ValueSourceDef,
155    },
156    /// Bean invocation step — resolved at route-add time.
157    Bean {
158        name: String,
159        method: String,
160    },
161    /// Script step: executes a script that can mutate the exchange.
162    /// The script has access to `headers`, `properties`, and `body`.
163    Script {
164        language: String,
165        script: String,
166    },
167    /// Throttle step: rate limiting with configurable behavior when limit exceeded.
168    Throttle {
169        config: camel_api::ThrottlerConfig,
170        steps: Vec<BuilderStep>,
171    },
172    /// LoadBalance step: distributes exchanges across multiple endpoints using a strategy.
173    LoadBalance {
174        config: camel_api::LoadBalancerConfig,
175        steps: Vec<BuilderStep>,
176    },
177    /// DynamicRouter step: routes exchanges dynamically based on expression evaluation.
178    DynamicRouter {
179        config: camel_api::DynamicRouterConfig,
180    },
181    RoutingSlip {
182        config: camel_api::RoutingSlipConfig,
183    },
184    RecipientList {
185        config: camel_api::recipient_list::RecipientListConfig,
186    },
187    DeclarativeRecipientList {
188        expression: LanguageExpressionDef,
189        delimiter: String,
190        parallel: bool,
191        parallel_limit: Option<usize>,
192        stop_on_exception: bool,
193        aggregation: String,
194    },
195    Delay {
196        config: camel_api::DelayConfig,
197    },
198    /// Runtime loop with closure-based predicate (programmatic DSL).
199    Loop {
200        config: LoopConfig,
201        steps: Vec<BuilderStep>,
202    },
203    /// Declarative loop with optional language-based while predicate (YAML DSL).
204    DeclarativeLoop {
205        count: Option<usize>,
206        while_predicate: Option<LanguageExpressionDef>,
207        steps: Vec<BuilderStep>,
208    },
209    /// EIP-7 enrich: synchronous content enrichment via a resolved producer.
210    Enrich {
211        uri: String,
212        strategy: Option<String>,
213        timeout_ms: Option<u64>,
214    },
215    /// EIP-7 pollEnrich: blocking poll of a PollingConsumer with timeout.
216    PollEnrich {
217        uri: String,
218        strategy: Option<String>,
219        timeout_ms: Option<u64>,
220    },
221    /// Validate step: evaluates a language expression as predicate.
222    /// Exchange passes if predicate returns true; else CamelError::ValidationError.
223    Validate {
224        predicate: LanguageExpressionDef,
225    },
226    /// Claim Check step (EIP). Transforms the exchange body to/from a
227    /// `ClaimCheckRepository` by key. Process-mode, no child pipeline.
228    ClaimCheck {
229        repository: String,
230        operation: String,
231        key: LanguageExpressionDef,
232    },
233    /// Sampling step (EIP). Passes 1 of every N exchanges (counter-based,
234    /// deterministic). Non-sampled exchanges get CamelStop=true (drop semantics).
235    /// Process-mode, stateless. No StepLifecycle — counter is route-scoped.
236    Sampling {
237        period: usize,
238    },
239    /// Sort step (EIP). Orders a body array by extracting a sort key
240    /// from each element via a language expression. Process-mode, stateless.
241    Sort {
242        expression: LanguageExpressionDef,
243        reverse: bool,
244    },
245    /// Idempotent Consumer step (EIP). Wraps a child sub-pipeline that runs
246    /// only when the message-id is NOT present in the named repository.
247    /// Compiled to a `IdempotentConsumerSegment` (OutcomePipeline, segment-mode).
248    IdempotentConsumer {
249        repository: String,
250        expression: LanguageExpressionDef,
251        steps: Vec<BuilderStep>,
252        eager: bool,
253        remove_on_failure: bool,
254    },
255    /// Declarative doTry/doCatch/doFinally, resolved at route-add time.
256    DeclarativeDoTry {
257        try_steps: Vec<BuilderStep>,
258        catch: Vec<DoTryCatchClauseBuilder>,
259        finally: Option<DoTryFinallyBuilder>,
260    },
261    /// Resequencer EIP: resequences exchanges by sequence number.
262    /// Must be a top-level step (not nested inside structural EIPs).
263    Resequence {
264        policy_config: ResequencePolicyConfig,
265    },
266}
267
268impl std::fmt::Debug for BuilderStep {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        match self {
271            BuilderStep::Processor(_) => write!(f, "BuilderStep::Processor(...)"),
272            BuilderStep::To(uri) => write!(f, "BuilderStep::To({uri:?})"),
273            BuilderStep::Stop => write!(f, "BuilderStep::Stop"),
274            BuilderStep::Log { level, message } => write!(
275                f,
276                "BuilderStep::Log {{ level: {level:?}, message: {message:?} }}"
277            ),
278            BuilderStep::DeclarativeSetHeader { key, .. } => {
279                write!(
280                    f,
281                    "BuilderStep::DeclarativeSetHeader {{ key: {key:?}, .. }}"
282                )
283            }
284            BuilderStep::DeclarativeSetBody { .. } => {
285                write!(f, "BuilderStep::DeclarativeSetBody {{ .. }}")
286            }
287            BuilderStep::DeclarativeSetProperty { key, .. } => {
288                write!(
289                    f,
290                    "BuilderStep::DeclarativeSetProperty {{ key: {key:?}, .. }}"
291                )
292            }
293            BuilderStep::DeclarativeFilter { steps, .. } => {
294                write!(
295                    f,
296                    "BuilderStep::DeclarativeFilter {{ steps: {steps:?}, .. }}"
297                )
298            }
299            BuilderStep::DeclarativeChoice { whens, otherwise } => {
300                write!(
301                    f,
302                    "BuilderStep::DeclarativeChoice {{ whens: {} clause(s), otherwise: {} }}",
303                    whens.len(),
304                    if otherwise.is_some() { "Some" } else { "None" }
305                )
306            }
307            BuilderStep::DeclarativeScript { expression } => write!(
308                f,
309                "BuilderStep::DeclarativeScript {{ language: {:?}, .. }}",
310                expression.language
311            ),
312            BuilderStep::DeclarativeFunction { definition } => write!(
313                f,
314                "BuilderStep::DeclarativeFunction {{ id: {:?}, runtime: {:?}, .. }}",
315                definition.id, definition.runtime
316            ),
317            BuilderStep::DeclarativeSplit { steps, .. } => {
318                write!(
319                    f,
320                    "BuilderStep::DeclarativeSplit {{ steps: {steps:?}, .. }}"
321                )
322            }
323            BuilderStep::DeclarativeStreamSplit {
324                steps,
325                stream_config,
326                ..
327            } => {
328                write!(
329                    f,
330                    "BuilderStep::DeclarativeStreamSplit {{ format: {:?}, steps: {} step(s) }}",
331                    stream_config.format,
332                    steps.len()
333                )
334            }
335            BuilderStep::DeclarativeDynamicRouter { expression, .. } => write!(
336                f,
337                "BuilderStep::DeclarativeDynamicRouter {{ language: {:?}, .. }}",
338                expression.language
339            ),
340            BuilderStep::DeclarativeRoutingSlip { expression, .. } => write!(
341                f,
342                "BuilderStep::DeclarativeRoutingSlip {{ language: {:?}, .. }}",
343                expression.language
344            ),
345            BuilderStep::Split { steps, .. } => {
346                write!(f, "BuilderStep::Split {{ steps: {steps:?}, .. }}")
347            }
348            BuilderStep::Aggregate { .. } => write!(f, "BuilderStep::Aggregate {{ .. }}"),
349            BuilderStep::Filter { steps, .. } => {
350                write!(f, "BuilderStep::Filter {{ steps: {steps:?}, .. }}")
351            }
352            BuilderStep::Choice { whens, otherwise } => {
353                write!(
354                    f,
355                    "BuilderStep::Choice {{ whens: {} clause(s), otherwise: {} }}",
356                    whens.len(),
357                    if otherwise.is_some() { "Some" } else { "None" }
358                )
359            }
360            BuilderStep::WireTap { uri } => write!(f, "BuilderStep::WireTap {{ uri: {uri:?} }}"),
361            BuilderStep::Multicast { steps, .. } => {
362                write!(f, "BuilderStep::Multicast {{ steps: {steps:?}, .. }}")
363            }
364            BuilderStep::DeclarativeLog { level, .. } => {
365                write!(f, "BuilderStep::DeclarativeLog {{ level: {level:?}, .. }}")
366            }
367            BuilderStep::Bean { name, method } => {
368                write!(
369                    f,
370                    "BuilderStep::Bean {{ name: {name:?}, method: {method:?} }}"
371                )
372            }
373            BuilderStep::Script { language, .. } => {
374                write!(f, "BuilderStep::Script {{ language: {language:?}, .. }}")
375            }
376            BuilderStep::Throttle { steps, .. } => {
377                write!(f, "BuilderStep::Throttle {{ steps: {steps:?}, .. }}")
378            }
379            BuilderStep::LoadBalance { steps, .. } => {
380                write!(f, "BuilderStep::LoadBalance {{ steps: {steps:?}, .. }}")
381            }
382            BuilderStep::DynamicRouter { .. } => {
383                write!(f, "BuilderStep::DynamicRouter {{ .. }}")
384            }
385            BuilderStep::RoutingSlip { .. } => {
386                write!(f, "BuilderStep::RoutingSlip {{ .. }}")
387            }
388            BuilderStep::RecipientList { .. } => {
389                write!(f, "BuilderStep::RecipientList {{ .. }}")
390            }
391            BuilderStep::DeclarativeRecipientList {
392                expression,
393                aggregation,
394                ..
395            } => write!(
396                f,
397                "BuilderStep::DeclarativeRecipientList {{ language: {:?}, aggregation: {:?}, .. }}",
398                expression.language, aggregation
399            ),
400            BuilderStep::Delay { config } => {
401                write!(f, "BuilderStep::Delay {{ config: {:?} }}", config)
402            }
403            BuilderStep::Loop { config, steps } => {
404                write!(
405                    f,
406                    "BuilderStep::Loop {{ config: {:?}, steps: {} }}",
407                    config.mode_name(),
408                    steps.len()
409                )
410            }
411            BuilderStep::DeclarativeLoop {
412                count,
413                while_predicate,
414                steps,
415            } => {
416                write!(
417                    f,
418                    "BuilderStep::DeclarativeLoop {{ count: {:?}, while: {}, steps: {} }}",
419                    count,
420                    while_predicate.is_some(),
421                    steps.len()
422                )
423            }
424            BuilderStep::Validate { predicate } => {
425                write!(
426                    f,
427                    "BuilderStep::Validate {{ language: {:?}, source: {:?} }}",
428                    predicate.language, predicate.source
429                )
430            }
431            BuilderStep::IdempotentConsumer {
432                repository,
433                expression,
434                steps,
435                eager,
436                remove_on_failure,
437            } => {
438                write!(
439                    f,
440                    "BuilderStep::IdempotentConsumer {{ repository: {repository:?}, language: {:?}, steps: {}, eager: {eager}, remove_on_failure: {remove_on_failure} }}",
441                    expression.language,
442                    steps.len()
443                )
444            }
445            BuilderStep::Enrich {
446                uri,
447                strategy,
448                timeout_ms,
449            } => {
450                write!(
451                    f,
452                    "BuilderStep::Enrich {{ uri: {uri:?}, strategy: {strategy:?}, timeout_ms: {timeout_ms:?} }}"
453                )
454            }
455            BuilderStep::PollEnrich {
456                uri,
457                strategy,
458                timeout_ms,
459            } => {
460                write!(
461                    f,
462                    "BuilderStep::PollEnrich {{ uri: {uri:?}, strategy: {strategy:?}, timeout_ms: {timeout_ms:?} }}"
463                )
464            }
465            BuilderStep::ClaimCheck {
466                repository,
467                operation,
468                key,
469            } => {
470                write!(
471                    f,
472                    "BuilderStep::ClaimCheck {{ repository: {repository:?}, operation: {operation:?}, \
473                     key: {{ language: {:?}, source: {:?} }} }}",
474                    key.language, key.source
475                )
476            }
477            BuilderStep::Sampling { period } => {
478                write!(f, "BuilderStep::Sampling {{ period: {period} }}")
479            }
480            BuilderStep::Sort {
481                expression,
482                reverse,
483            } => {
484                write!(
485                    f,
486                    "BuilderStep::Sort {{ language: {:?}, source: {:?}, reverse: {reverse} }}",
487                    expression.language, expression.source
488                )
489            }
490            BuilderStep::DeclarativeDoTry {
491                try_steps,
492                catch,
493                finally,
494            } => {
495                write!(
496                    f,
497                    "BuilderStep::DeclarativeDoTry {{ try_steps: {} step(s), catch: {} clause(s), finally: {} }}",
498                    try_steps.len(),
499                    catch.len(),
500                    if finally.is_some() { "Some" } else { "None" }
501                )
502            }
503            BuilderStep::Resequence { .. } => write!(f, "BuilderStep::Resequence {{ .. }}"),
504        }
505    }
506}
507
508/// An unresolved route definition. "to" URIs have not been resolved to producers yet.
509pub struct RouteDefinition {
510    pub(crate) from_uri: String,
511    pub(crate) steps: Vec<BuilderStep>,
512    /// Optional per-route error handler config. Takes precedence over the global one.
513    pub(crate) error_handler: Option<ErrorHandlerConfig>,
514    /// Optional circuit breaker config. Applied between error handler and step pipeline.
515    pub(crate) circuit_breaker: Option<CircuitBreakerConfig>,
516    pub(crate) security_policy: Option<SecurityPolicyConfig>,
517    /// Optional token authenticator for validating JWT/OAuth tokens.
518    pub(crate) security_authenticator: Option<Arc<dyn TokenAuthenticator>>,
519    /// Optional Unit of Work config for in-flight tracking and completion hooks.
520    pub(crate) unit_of_work: Option<UnitOfWorkConfig>,
521    /// User override for the consumer's concurrency model. `None` means
522    /// "use whatever the consumer declares".
523    pub(crate) concurrency: Option<ConcurrencyModel>,
524    /// Unique identifier for this route. Required.
525    pub(crate) route_id: String,
526    /// Whether this route should start automatically when the context starts.
527    pub(crate) auto_startup: bool,
528    /// Order in which routes are started. Lower values start first.
529    pub(crate) startup_order: i32,
530    pub(crate) source_hash: Option<u64>,
531}
532
533impl RouteDefinition {
534    /// Create a new route definition with the required route ID.
535    pub fn new(from_uri: impl Into<String>, steps: Vec<BuilderStep>) -> Self {
536        Self {
537            from_uri: from_uri.into(),
538            steps,
539            error_handler: None,
540            circuit_breaker: None,
541            security_policy: None,
542            security_authenticator: None,
543            unit_of_work: None,
544            concurrency: None,
545            route_id: String::new(), // Will be set by with_route_id()
546            auto_startup: true,
547            startup_order: 1000,
548            source_hash: None,
549        }
550    }
551
552    /// The source endpoint URI.
553    pub fn from_uri(&self) -> &str {
554        &self.from_uri
555    }
556
557    /// The steps in this route definition.
558    pub fn steps(&self) -> &[BuilderStep] {
559        &self.steps
560    }
561
562    /// Set a per-route error handler, overriding the global one.
563    pub fn with_error_handler(mut self, config: ErrorHandlerConfig) -> Self {
564        self.error_handler = Some(config);
565        self
566    }
567
568    /// Get the route-level error handler config, if set.
569    pub fn error_handler_config(&self) -> Option<&ErrorHandlerConfig> {
570        self.error_handler.as_ref()
571    }
572
573    /// Set a circuit breaker for this route.
574    pub fn with_circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
575        self.circuit_breaker = Some(config);
576        self
577    }
578
579    /// Set a security policy for this route.
580    pub fn with_security_policy(mut self, config: SecurityPolicyConfig) -> Self {
581        self.security_policy = Some(config);
582        self
583    }
584
585    /// Set a token authenticator for this route.
586    pub fn with_security_authenticator(
587        mut self,
588        authenticator: Arc<dyn TokenAuthenticator>,
589    ) -> Self {
590        self.security_authenticator = Some(authenticator);
591        self
592    }
593
594    /// Set a unit of work config for this route.
595    pub fn with_unit_of_work(mut self, config: UnitOfWorkConfig) -> Self {
596        self.unit_of_work = Some(config);
597        self
598    }
599
600    /// Get the unit of work config, if set.
601    pub fn unit_of_work_config(&self) -> Option<&UnitOfWorkConfig> {
602        self.unit_of_work.as_ref()
603    }
604
605    /// Get the circuit breaker config, if set.
606    pub fn circuit_breaker_config(&self) -> Option<&CircuitBreakerConfig> {
607        self.circuit_breaker.as_ref()
608    }
609
610    pub fn security_policy_config(&self) -> Option<&SecurityPolicyConfig> {
611        self.security_policy.as_ref()
612    }
613
614    pub fn security_authenticator(&self) -> Option<&Arc<dyn TokenAuthenticator>> {
615        self.security_authenticator.as_ref()
616    }
617
618    /// User-specified concurrency override, if any.
619    pub fn concurrency_override(&self) -> Option<&ConcurrencyModel> {
620        self.concurrency.as_ref()
621    }
622
623    /// Override the consumer's concurrency model for this route.
624    pub fn with_concurrency(mut self, model: ConcurrencyModel) -> Self {
625        self.concurrency = Some(model);
626        self
627    }
628
629    /// Get the route ID.
630    pub fn route_id(&self) -> &str {
631        &self.route_id
632    }
633
634    /// Whether this route should start automatically when the context starts.
635    pub fn auto_startup(&self) -> bool {
636        self.auto_startup
637    }
638
639    /// Order in which routes are started. Lower values start first.
640    pub fn startup_order(&self) -> i32 {
641        self.startup_order
642    }
643
644    /// Set a unique identifier for this route.
645    pub fn with_route_id(mut self, id: impl Into<String>) -> Self {
646        self.route_id = id.into();
647        self
648    }
649
650    /// Set whether this route should start automatically.
651    pub fn with_auto_startup(mut self, auto: bool) -> Self {
652        self.auto_startup = auto;
653        self
654    }
655
656    /// Set the startup order. Lower values start first.
657    pub fn with_startup_order(mut self, order: i32) -> Self {
658        self.startup_order = order;
659        self
660    }
661
662    pub fn with_source_hash(mut self, hash: u64) -> Self {
663        self.source_hash = Some(hash);
664        self
665    }
666
667    pub fn source_hash(&self) -> Option<u64> {
668        self.source_hash
669    }
670
671    /// Extract the metadata fields needed for introspection.
672    /// This is used by RouteController to store route info without the non-Sync steps.
673    pub fn to_info(&self) -> RouteDefinitionInfo {
674        RouteDefinitionInfo {
675            route_id: self.route_id.clone(),
676            auto_startup: self.auto_startup,
677            startup_order: self.startup_order,
678            source_hash: self.source_hash,
679        }
680    }
681}
682
683/// Minimal route definition metadata for introspection.
684///
685/// This struct contains only the metadata fields from [`RouteDefinition`]
686/// that are needed for route lifecycle management, without the `steps` field
687/// (which contains non-Sync types and cannot be stored in a Sync struct).
688#[derive(Clone)]
689pub struct RouteDefinitionInfo {
690    route_id: String,
691    auto_startup: bool,
692    startup_order: i32,
693    pub(crate) source_hash: Option<u64>,
694}
695
696impl RouteDefinitionInfo {
697    /// Get the route ID.
698    pub fn route_id(&self) -> &str {
699        &self.route_id
700    }
701
702    /// Whether this route should start automatically when the context starts.
703    pub fn auto_startup(&self) -> bool {
704        self.auto_startup
705    }
706
707    /// Order in which routes are started. Lower values start first.
708    pub fn startup_order(&self) -> i32 {
709        self.startup_order
710    }
711
712    pub fn source_hash(&self) -> Option<u64> {
713        self.source_hash
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720
721    #[test]
722    fn test_builder_step_multicast_variant() {
723        use camel_api::MulticastConfig;
724
725        let step = BuilderStep::Multicast {
726            steps: vec![BuilderStep::To("direct:a".into())],
727            config: MulticastConfig::new(),
728        };
729
730        assert!(matches!(step, BuilderStep::Multicast { .. }));
731    }
732
733    #[test]
734    fn test_route_definition_defaults() {
735        let def = RouteDefinition::new("direct:test", vec![]).with_route_id("test-route");
736        assert_eq!(def.route_id(), "test-route");
737        assert!(def.auto_startup());
738        assert_eq!(def.startup_order(), 1000);
739    }
740
741    #[test]
742    fn test_route_definition_builders() {
743        let def = RouteDefinition::new("direct:test", vec![])
744            .with_route_id("my-route")
745            .with_auto_startup(false)
746            .with_startup_order(50);
747        assert_eq!(def.route_id(), "my-route");
748        assert!(!def.auto_startup());
749        assert_eq!(def.startup_order(), 50);
750    }
751
752    #[test]
753    fn test_route_definition_accessors_cover_core_fields() {
754        let def = RouteDefinition::new("direct:in", vec![BuilderStep::To("mock:out".into())])
755            .with_route_id("accessor-route");
756
757        assert_eq!(def.from_uri(), "direct:in");
758        assert_eq!(def.steps().len(), 1);
759        assert!(matches!(def.steps()[0], BuilderStep::To(_)));
760    }
761
762    #[test]
763    fn test_route_definition_error_handler_circuit_breaker_and_concurrency_accessors() {
764        use camel_api::circuit_breaker::CircuitBreakerConfig;
765        use camel_api::error_handler::ErrorHandlerConfig;
766        use camel_component_api::ConcurrencyModel;
767
768        let def = RouteDefinition::new("direct:test", vec![])
769            .with_route_id("eh-route")
770            .with_error_handler(ErrorHandlerConfig::dead_letter_channel("log:dlc"))
771            .with_circuit_breaker(CircuitBreakerConfig::new())
772            .with_concurrency(ConcurrencyModel::Concurrent { max: Some(4) });
773
774        let eh = def
775            .error_handler_config()
776            .expect("error handler should be set");
777        assert_eq!(eh.dlc_uri.as_deref(), Some("log:dlc"));
778        assert!(def.circuit_breaker_config().is_some());
779        assert!(matches!(
780            def.concurrency_override(),
781            Some(ConcurrencyModel::Concurrent { max: Some(4) })
782        ));
783    }
784
785    #[test]
786    fn test_builder_step_debug_covers_many_variants() {
787        use camel_api::splitter::{AggregationStrategy, SplitterConfig, split_body_lines};
788        use camel_api::{
789            DynamicRouterConfig, Exchange, IdentityProcessor, RoutingSlipConfig, Value,
790        };
791        use std::sync::Arc;
792
793        let expr = LanguageExpressionDef {
794            language: "simple".into(),
795            source: "${body}".into(),
796        };
797
798        let steps = vec![
799            BuilderStep::Processor(BoxProcessor::new(IdentityProcessor)),
800            BuilderStep::To("mock:out".into()),
801            BuilderStep::Stop,
802            BuilderStep::Log {
803                level: camel_processor::LogLevel::Info,
804                message: "hello".into(),
805            },
806            BuilderStep::DeclarativeSetHeader {
807                key: "k".into(),
808                value: ValueSourceDef::Literal(Value::String("v".into())),
809            },
810            BuilderStep::DeclarativeSetBody {
811                value: ValueSourceDef::Expression(expr.clone()),
812            },
813            BuilderStep::DeclarativeFilter {
814                predicate: expr.clone(),
815                steps: vec![BuilderStep::Stop],
816            },
817            BuilderStep::DeclarativeChoice {
818                whens: vec![DeclarativeWhenStep {
819                    predicate: expr.clone(),
820                    steps: vec![BuilderStep::Stop],
821                }],
822                otherwise: Some(vec![BuilderStep::Stop]),
823            },
824            BuilderStep::DeclarativeScript {
825                expression: expr.clone(),
826            },
827            BuilderStep::DeclarativeSplit {
828                expression: expr.clone(),
829                aggregation: AggregationStrategy::Original,
830                parallel: false,
831                parallel_limit: Some(2),
832                stop_on_exception: true,
833                steps: vec![BuilderStep::Stop],
834            },
835            BuilderStep::Split {
836                config: SplitterConfig::new(split_body_lines()),
837                steps: vec![BuilderStep::Stop],
838            },
839            BuilderStep::Aggregate {
840                config: camel_api::AggregatorConfig::correlate_by("id")
841                    .complete_when_size(1)
842                    .build()
843                    .unwrap(),
844            },
845            BuilderStep::Filter {
846                predicate: Arc::new(|_: &Exchange| true),
847                steps: vec![BuilderStep::Stop],
848            },
849            BuilderStep::WireTap {
850                uri: "mock:tap".into(),
851            },
852            BuilderStep::DeclarativeLog {
853                level: camel_processor::LogLevel::Info,
854                message: ValueSourceDef::Expression(expr.clone()),
855            },
856            BuilderStep::Bean {
857                name: "bean".into(),
858                method: "call".into(),
859            },
860            BuilderStep::Script {
861                language: "rhai".into(),
862                script: "body".into(),
863            },
864            BuilderStep::Throttle {
865                config: camel_api::ThrottlerConfig::new(10, std::time::Duration::from_millis(10)),
866                steps: vec![BuilderStep::Stop],
867            },
868            BuilderStep::LoadBalance {
869                config: camel_api::LoadBalancerConfig::round_robin(),
870                steps: vec![BuilderStep::To("mock:l1".into())],
871            },
872            BuilderStep::DynamicRouter {
873                config: DynamicRouterConfig::new(Arc::new(|_| Some("mock:dr".into()))),
874            },
875            BuilderStep::RoutingSlip {
876                config: RoutingSlipConfig::new(Arc::new(|_| Some("mock:rs".into()))),
877            },
878        ];
879
880        for step in steps {
881            let dbg = format!("{step:?}");
882            assert!(!dbg.is_empty());
883        }
884    }
885
886    #[test]
887    fn test_route_definition_to_info_preserves_metadata() {
888        let info = RouteDefinition::new("direct:test", vec![])
889            .with_route_id("meta-route")
890            .with_auto_startup(false)
891            .with_startup_order(7)
892            .to_info();
893
894        assert_eq!(info.route_id(), "meta-route");
895        assert!(!info.auto_startup());
896        assert_eq!(info.startup_order(), 7);
897    }
898
899    #[test]
900    fn test_choice_builder_step_debug() {
901        use camel_api::{Exchange, FilterPredicate};
902        use std::sync::Arc;
903
904        fn always_true(_: &Exchange) -> bool {
905            true
906        }
907
908        let step = BuilderStep::Choice {
909            whens: vec![WhenStep {
910                predicate: Arc::new(always_true) as FilterPredicate,
911                steps: vec![BuilderStep::To("mock:a".into())],
912            }],
913            otherwise: None,
914        };
915        let debug = format!("{step:?}");
916        assert!(debug.contains("Choice"));
917    }
918
919    #[test]
920    fn test_route_definition_unit_of_work() {
921        use camel_api::UnitOfWorkConfig;
922        let config = UnitOfWorkConfig {
923            on_complete: Some("log:complete".into()),
924            on_failure: Some("log:failed".into()),
925        };
926        let def = RouteDefinition::new("direct:test", vec![])
927            .with_route_id("uow-test")
928            .with_unit_of_work(config.clone());
929        assert_eq!(
930            def.unit_of_work_config().unwrap().on_complete.as_deref(),
931            Some("log:complete")
932        );
933        assert_eq!(
934            def.unit_of_work_config().unwrap().on_failure.as_deref(),
935            Some("log:failed")
936        );
937
938        let def_no_uow = RouteDefinition::new("direct:test", vec![]).with_route_id("no-uow");
939        assert!(def_no_uow.unit_of_work_config().is_none());
940    }
941
942    #[test]
943    fn test_route_definition_security_policy_accessor() {
944        use async_trait::async_trait;
945        use camel_api::CamelError;
946        use camel_api::Exchange;
947        use camel_api::security_policy::{
948            AuthorizationDecision, Principal, SecurityPolicy, SecurityPolicyConfig,
949        };
950
951        struct StubPolicy;
952        #[async_trait]
953        impl SecurityPolicy for StubPolicy {
954            async fn evaluate(
955                &self,
956                _exchange: &mut Exchange,
957            ) -> Result<AuthorizationDecision, CamelError> {
958                Ok(AuthorizationDecision::Granted {
959                    principal: Principal {
960                        subject: "test".into(),
961                        issuer: "test".into(),
962                        audience: vec![],
963                        scopes: vec![],
964                        roles: vec![],
965                        claims: serde_json::Value::Null,
966                    },
967                })
968            }
969        }
970
971        let def_no_sp = RouteDefinition::new("direct:test", vec![]).with_route_id("no-sp");
972        assert!(def_no_sp.security_policy_config().is_none());
973
974        let def = RouteDefinition::new("direct:test", vec![])
975            .with_route_id("sp-test")
976            .with_security_policy(SecurityPolicyConfig::new(StubPolicy));
977        assert!(def.security_policy_config().is_some());
978    }
979
980    #[test]
981    fn test_route_definition_security_authenticator_accessor() {
982        use camel_api::security_policy::Principal;
983
984        struct TestAuth;
985        #[async_trait::async_trait]
986        impl TokenAuthenticator for TestAuth {
987            async fn authenticate_bearer(
988                &self,
989                _token: &str,
990            ) -> Result<Principal, camel_api::CamelError> {
991                Ok(Principal {
992                    subject: "test".into(),
993                    issuer: "test".into(),
994                    audience: vec![],
995                    scopes: vec![],
996                    roles: vec![],
997                    claims: serde_json::Value::Null,
998                })
999            }
1000        }
1001
1002        let def_no_auth = RouteDefinition::new("direct:test".to_string(), vec![]);
1003        assert!(def_no_auth.security_authenticator().is_none());
1004
1005        let auth = Arc::new(TestAuth);
1006        let def = RouteDefinition::new("direct:test".to_string(), vec![])
1007            .with_security_authenticator(auth);
1008        assert!(def.security_authenticator().is_some());
1009    }
1010}