1use std::collections::BTreeMap;
7
8use camel_api::DelayConfig;
9use camel_api::aggregator::{
10 AggregationStrategy, AggregatorConfig, CompletionCondition, CompletionMode, CorrelationStrategy,
11};
12use camel_api::body::Body;
13use camel_api::body_converter::BodyType;
14use camel_api::circuit_breaker::CircuitBreakerConfig;
15use camel_api::dynamic_router::{DynamicRouterConfig, RouterExpression};
16use camel_api::error_handler::{ErrorHandlerConfig, RedeliveryPolicy};
17use camel_api::load_balancer::LoadBalancerConfig;
18use camel_api::loop_eip::{LoopConfig, LoopMode};
19use camel_api::multicast::{MulticastConfig, MulticastStrategy};
20use camel_api::recipient_list::{RecipientListConfig, RecipientListExpression};
21use camel_api::routing_slip::{RoutingSlipConfig, RoutingSlipExpression};
22use camel_api::splitter::SplitterConfig;
23use camel_api::throttler::{ThrottleStrategy, ThrottlerConfig};
24use camel_api::{
25 BoxProcessor, CamelError, CanonicalRouteSpec, EndpointUri, Exchange, FilterPredicate,
26 IdentityProcessor, LanguageExpressionDef, OpaqueProcessor, ProcessorFn, Value,
27 runtime::{
28 CanonicalAggregateSpec, CanonicalAggregateStrategySpec, CanonicalCircuitBreakerSpec,
29 CanonicalSplitAggregationSpec, CanonicalSplitExpressionSpec, CanonicalStepSpec,
30 CanonicalWhenSpec,
31 },
32};
33use camel_component_api::ConcurrencyModel;
34use camel_core::route::{BuilderStep, DeclarativeWhenStep, RouteDefinition, WhenStep};
35use camel_processor::{
36 ConvertBodyTo, DynamicSetHeader, LogLevel, MapBody, MarshalService, SetBody, SetHeader,
37 StreamCacheService, UnmarshalService, builtin_data_format,
38};
39
40pub mod do_try;
42pub use do_try::{DoCatchBuilder, DoFinallyBuilder, DoTryBuilder};
43
44pub trait StepAccumulator: Sized {
50 fn steps_mut(&mut self) -> &mut Vec<BuilderStep>;
51
52 fn to(mut self, endpoint: impl Into<String>) -> Self {
53 self.steps_mut().push(BuilderStep::To(endpoint.into()));
54 self
55 }
56
57 fn process<F, Fut>(mut self, f: F) -> Self
58 where
59 F: Fn(Exchange) -> Fut + Send + Sync + 'static,
60 Fut: std::future::Future<Output = Result<Exchange, CamelError>> + Send + 'static,
61 {
62 let svc = ProcessorFn::new(f);
63 self.steps_mut()
64 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
65 svc,
66 ))));
67 self
68 }
69
70 fn process_fn(mut self, processor: BoxProcessor) -> Self {
71 self.steps_mut()
72 .push(BuilderStep::Processor(OpaqueProcessor(processor)));
73 self
74 }
75
76 fn set_header(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
77 let svc = SetHeader::new(IdentityProcessor, key, value);
78 self.steps_mut()
79 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
80 svc,
81 ))));
82 self
83 }
84
85 fn map_body<F>(mut self, mapper: F) -> Self
86 where
87 F: Fn(Body) -> Body + Clone + Send + Sync + 'static,
88 {
89 let svc = MapBody::new(IdentityProcessor, mapper);
90 self.steps_mut()
91 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
92 svc,
93 ))));
94 self
95 }
96
97 fn set_body<B>(mut self, body: B) -> Self
98 where
99 B: Into<Body> + Clone + Send + Sync + 'static,
100 {
101 let body: Body = body.into();
102 let svc = SetBody::new(IdentityProcessor, move |_ex: &Exchange| body.clone());
103 self.steps_mut()
104 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
105 svc,
106 ))));
107 self
108 }
109
110 fn transform<B>(self, body: B) -> Self
115 where
116 B: Into<Body> + Clone + Send + Sync + 'static,
117 {
118 self.set_body(body)
119 }
120
121 fn set_body_fn<F>(mut self, expr: F) -> Self
122 where
123 F: Fn(&Exchange) -> Body + Clone + Send + Sync + 'static,
124 {
125 let svc = SetBody::new(IdentityProcessor, expr);
126 self.steps_mut()
127 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
128 svc,
129 ))));
130 self
131 }
132
133 fn set_header_fn<F>(mut self, key: impl Into<String>, expr: F) -> Self
134 where
135 F: Fn(&Exchange) -> Value + Clone + Send + Sync + 'static,
136 {
137 let svc = DynamicSetHeader::new(IdentityProcessor, key, expr);
138 self.steps_mut()
139 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
140 svc,
141 ))));
142 self
143 }
144
145 fn aggregate(mut self, config: AggregatorConfig) -> Self {
146 self.steps_mut().push(BuilderStep::Aggregate { config });
147 self
148 }
149
150 fn stop(mut self) -> Self {
156 self.steps_mut().push(BuilderStep::Stop);
157 self
158 }
159
160 fn delay(mut self, duration: std::time::Duration) -> Self {
161 self.steps_mut().push(BuilderStep::Delay {
162 config: DelayConfig::from_duration(duration),
163 });
164 self
165 }
166
167 fn delay_with_header(
168 mut self,
169 duration: std::time::Duration,
170 header: impl Into<String>,
171 ) -> Self {
172 self.steps_mut().push(BuilderStep::Delay {
173 config: DelayConfig::from_duration_with_header(duration, header),
174 });
175 self
176 }
177
178 fn log(mut self, message: impl Into<String>, level: LogLevel) -> Self {
182 self.steps_mut().push(BuilderStep::Log {
183 level,
184 message: message.into(),
185 });
186 self
187 }
188
189 fn convert_body_to(mut self, target: BodyType) -> Self {
201 let svc = ConvertBodyTo::new(IdentityProcessor, target);
202 self.steps_mut()
203 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
204 svc,
205 ))));
206 self
207 }
208
209 fn stream_cache(mut self, threshold: usize) -> Self {
210 let config = camel_api::stream_cache::StreamCacheConfig::new(threshold);
211 let svc = StreamCacheService::new(IdentityProcessor, config);
212 self.steps_mut()
213 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
214 svc,
215 ))));
216 self
217 }
218
219 fn stream_cache_default(self) -> Self {
223 self.stream_cache(camel_api::stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD)
224 }
225
226 fn marshal(mut self, format: impl Into<String>) -> Result<Self, CamelError> {
237 let name = format.into();
238 let df = builtin_data_format(&name)
239 .ok_or_else(|| CamelError::Config(format!("unknown data format: '{name}'")))?;
240 let svc = MarshalService::new(IdentityProcessor, df);
241 self.steps_mut()
242 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
243 svc,
244 ))));
245 Ok(self)
246 }
247
248 fn unmarshal(mut self, format: impl Into<String>) -> Result<Self, CamelError> {
259 let name = format.into();
260 let df = builtin_data_format(&name)
261 .ok_or_else(|| CamelError::Config(format!("unknown data format: '{name}'")))?;
262 let svc = UnmarshalService::new(IdentityProcessor, df);
263 self.steps_mut()
264 .push(BuilderStep::Processor(OpaqueProcessor(BoxProcessor::new(
265 svc,
266 ))));
267 Ok(self)
268 }
269
270 fn validate(mut self, expression: impl Into<String>) -> Self {
280 let source = expression.into();
281 let expression = LanguageExpressionDef {
282 language: "simple".into(),
283 source,
284 };
285 self.steps_mut().push(BuilderStep::Validate {
286 predicate: expression,
287 });
288 self
289 }
290
291 fn script(mut self, language: impl Into<String>, script: impl Into<String>) -> Self {
302 self.steps_mut().push(BuilderStep::Script {
303 language: language.into(),
304 script: script.into(),
305 });
306 self
307 }
308
309 fn enrich(mut self, uri: impl Into<String>) -> Self {
315 self.steps_mut().push(BuilderStep::Enrich {
316 uri: uri.into(),
317 strategy: None,
318 timeout_ms: None,
319 });
320 self
321 }
322
323 fn poll_enrich(mut self, uri: impl Into<String>, timeout_ms: u64) -> Self {
329 self.steps_mut().push(BuilderStep::PollEnrich {
330 uri: uri.into(),
331 strategy: None,
332 timeout_ms: Some(timeout_ms),
333 });
334 self
335 }
336
337 fn bean(mut self, name: impl Into<String>, method: impl Into<String>) -> Self {
338 self.steps_mut().push(BuilderStep::Bean {
339 name: name.into(),
340 method: method.into(),
341 });
342 self
343 }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
348enum EndpointSlot {
349 From,
351 Step(usize),
353}
354
355#[derive(Clone)]
371pub struct RouteBuilder {
372 from_uri: String,
373 steps: Vec<BuilderStep>,
374 parameter_assignments: Vec<(EndpointSlot, BTreeMap<String, String>)>,
376 parameter_misuse: Option<String>,
378 error_handler: Option<ErrorHandlerConfig>,
379 error_handler_mode: ErrorHandlerMode,
380 circuit_breaker_config: Option<CircuitBreakerConfig>,
381 security_policy_config: Option<camel_api::security_policy::SecurityPolicyConfig>,
382 security_authenticator: Option<std::sync::Arc<dyn camel_auth::TokenAuthenticator>>,
383 concurrency: Option<ConcurrencyModel>,
384 route_id: Option<String>,
385 auto_startup: Option<bool>,
386 startup_order: Option<i32>,
387}
388
389#[derive(Default, Clone)]
390enum ErrorHandlerMode {
391 #[default]
392 None,
393 ExplicitConfig,
394 Shorthand {
395 dlc_uri: Option<String>,
396 specs: Vec<OnExceptionSpec>,
397 },
398 Mixed,
399}
400
401#[derive(Clone)]
402struct OnExceptionSpec {
403 matches: std::sync::Arc<dyn Fn(&CamelError) -> bool + Send + Sync>,
404 retry: Option<RedeliveryPolicy>,
405 handled_by: Option<String>,
406}
407
408impl RouteBuilder {
409 pub fn from(endpoint: &str) -> Self {
411 Self {
412 from_uri: endpoint.to_string(),
413 steps: Vec::new(),
414 parameter_assignments: Vec::new(),
415 parameter_misuse: None,
416 error_handler: None,
417 error_handler_mode: ErrorHandlerMode::None,
418 circuit_breaker_config: None,
419 security_policy_config: None,
420 security_authenticator: None,
421 concurrency: None,
422 route_id: None,
423 auto_startup: None,
424 startup_order: None,
425 }
426 }
427
428 pub fn filter<F>(self, predicate: F) -> FilterBuilder
432 where
433 F: Fn(&Exchange) -> bool + Send + Sync + 'static,
434 {
435 FilterBuilder {
436 parent: self,
437 predicate: camel_api::FilterPredicate::new(predicate),
438 steps: vec![],
439 }
440 }
441
442 pub fn choice(self) -> ChoiceBuilder {
448 ChoiceBuilder {
449 parent: self,
450 whens: vec![],
451 _otherwise: None,
452 }
453 }
454
455 pub fn wire_tap(mut self, endpoint: &str) -> Self {
459 self.steps.push(BuilderStep::WireTap {
460 uri: endpoint.to_string(),
461 });
462 self
463 }
464
465 pub fn parameters(mut self, params: BTreeMap<String, String>) -> Self {
473 let slot = if self.steps.is_empty() {
474 EndpointSlot::From
475 } else {
476 EndpointSlot::Step(self.steps.len() - 1)
477 };
478
479 if self
480 .parameter_assignments
481 .iter()
482 .any(|(existing, _)| *existing == slot)
483 {
484 self.parameter_misuse =
485 Some("multiple .parameters() calls attached to the same endpoint".to_string());
486 } else if !endpoint_slot_bears_uri(&slot, &self.steps) {
487 self.parameter_misuse =
488 Some(".parameters() called with no pending endpoint step".to_string());
489 }
490
491 self.parameter_assignments.push((slot, params));
492 self
493 }
494
495 pub fn error_handler(mut self, config: ErrorHandlerConfig) -> Self {
497 self.error_handler_mode = match self.error_handler_mode {
498 ErrorHandlerMode::None | ErrorHandlerMode::ExplicitConfig => {
499 ErrorHandlerMode::ExplicitConfig
500 }
501 ErrorHandlerMode::Shorthand { .. } | ErrorHandlerMode::Mixed => ErrorHandlerMode::Mixed,
502 };
503 self.error_handler = Some(config);
504 self
505 }
506
507 pub fn dead_letter_channel(mut self, uri: impl Into<String>) -> Self {
509 let uri = uri.into();
510 self.error_handler_mode = match self.error_handler_mode {
511 ErrorHandlerMode::None => ErrorHandlerMode::Shorthand {
512 dlc_uri: Some(uri),
513 specs: Vec::new(),
514 },
515 ErrorHandlerMode::Shorthand { specs, .. } => ErrorHandlerMode::Shorthand {
516 dlc_uri: Some(uri),
517 specs,
518 },
519 ErrorHandlerMode::ExplicitConfig | ErrorHandlerMode::Mixed => ErrorHandlerMode::Mixed,
520 };
521 self
522 }
523
524 pub fn on_exception<F>(mut self, matches: F) -> OnExceptionBuilder
526 where
527 F: Fn(&CamelError) -> bool + Send + Sync + 'static,
528 {
529 self.error_handler_mode = match self.error_handler_mode {
530 ErrorHandlerMode::None => ErrorHandlerMode::Shorthand {
531 dlc_uri: None,
532 specs: Vec::new(),
533 },
534 ErrorHandlerMode::ExplicitConfig | ErrorHandlerMode::Mixed => ErrorHandlerMode::Mixed,
535 shorthand @ ErrorHandlerMode::Shorthand { .. } => shorthand,
536 };
537
538 OnExceptionBuilder {
539 parent: self,
540 policy: OnExceptionSpec {
541 matches: std::sync::Arc::new(matches),
542 retry: None,
543 handled_by: None,
544 },
545 }
546 }
547
548 pub fn circuit_breaker(mut self, config: CircuitBreakerConfig) -> Self {
550 self.circuit_breaker_config = Some(config);
551 self
552 }
553
554 pub fn security_policy(
555 mut self,
556 config: camel_api::security_policy::SecurityPolicyConfig,
557 ) -> Self {
558 self.security_policy_config = Some(config);
559 self
560 }
561
562 pub fn security_authenticator(
563 mut self,
564 auth: std::sync::Arc<dyn camel_auth::TokenAuthenticator>,
565 ) -> Self {
566 self.security_authenticator = Some(auth);
567 self
568 }
569
570 pub fn concurrent(mut self, max: usize) -> Self {
584 let max = if max == 0 { None } else { Some(max) };
585 self.concurrency = Some(ConcurrencyModel::Concurrent { max });
586 self
587 }
588
589 pub fn sequential(mut self) -> Self {
594 self.concurrency = Some(ConcurrencyModel::Sequential);
595 self
596 }
597
598 pub fn route_id(mut self, id: impl Into<String>) -> Self {
602 self.route_id = Some(id.into());
603 self
604 }
605
606 pub fn auto_startup(mut self, auto: bool) -> Self {
610 self.auto_startup = Some(auto);
611 self
612 }
613
614 pub fn startup_order(mut self, order: i32) -> Self {
618 self.startup_order = Some(order);
619 self
620 }
621
622 pub fn split(self, config: SplitterConfig) -> SplitBuilder {
628 SplitBuilder {
629 parent: self,
630 config,
631 steps: Vec::new(),
632 }
633 }
634
635 pub fn multicast(self) -> MulticastBuilder {
641 MulticastBuilder {
642 parent: self,
643 steps: Vec::new(),
644 config: MulticastConfig::new(),
645 }
646 }
647
648 pub fn throttle(self, max_requests: usize, period: std::time::Duration) -> ThrottleBuilder {
655 ThrottleBuilder {
656 parent: self,
657 config: ThrottlerConfig::new(max_requests, period),
658 steps: Vec::new(),
659 }
660 }
661
662 pub fn loop_count(self, count: usize) -> LoopBuilder {
664 LoopBuilder {
665 parent: self,
666 config: LoopConfig::new(LoopMode::Count(count)),
667 steps: vec![],
668 }
669 }
670
671 pub fn loop_while<F>(self, predicate: F) -> LoopBuilder
673 where
674 F: Fn(&Exchange) -> bool + Send + Sync + 'static,
675 {
676 LoopBuilder {
677 parent: self,
678 config: LoopConfig::new(LoopMode::While(camel_api::FilterPredicate::new(predicate))),
679 steps: vec![],
680 }
681 }
682
683 pub fn load_balance(self) -> LoadBalancerBuilder {
689 LoadBalancerBuilder {
690 parent: self,
691 config: LoadBalancerConfig::round_robin(),
692 steps: Vec::new(),
693 }
694 }
695
696 pub fn dynamic_router(self, expression: RouterExpression) -> Self {
712 self.dynamic_router_with_config(DynamicRouterConfig::new(expression))
713 }
714
715 pub fn dynamic_router_with_config(mut self, config: DynamicRouterConfig) -> Self {
719 self.steps.push(BuilderStep::DynamicRouter { config });
720 self
721 }
722
723 pub fn routing_slip(self, expression: RoutingSlipExpression) -> Self {
724 self.routing_slip_with_config(RoutingSlipConfig::new(expression))
725 }
726
727 pub fn routing_slip_with_config(mut self, config: RoutingSlipConfig) -> Self {
728 self.steps.push(BuilderStep::RoutingSlip { config });
729 self
730 }
731
732 pub fn recipient_list(self, expression: RecipientListExpression) -> Self {
733 self.recipient_list_with_config(RecipientListConfig::new(expression))
734 }
735
736 pub fn recipient_list_with_config(mut self, config: RecipientListConfig) -> Self {
737 self.steps.push(BuilderStep::RecipientList { config });
738 self
739 }
740
741 pub fn build(mut self) -> Result<RouteDefinition, CamelError> {
745 validate_uri(&self.from_uri)?;
746 let route_id = self
747 .route_id
748 .filter(|s| !s.trim().is_empty())
749 .ok_or_else(|| {
750 CamelError::RouteError(
751 "route must have a non-empty 'route_id' — call .route_id(\"name\") on the builder"
752 .to_string(),
753 )
754 })?;
755 let resolved_error_handler = match self.error_handler_mode {
756 ErrorHandlerMode::None => self.error_handler,
757 ErrorHandlerMode::ExplicitConfig => self.error_handler,
758 ErrorHandlerMode::Mixed => {
759 return Err(CamelError::RouteError(
760 "mixed error handler modes: cannot combine .error_handler(config) with shorthand methods".into(),
761 ));
762 }
763 ErrorHandlerMode::Shorthand { dlc_uri, specs } => {
764 let mut config = if let Some(uri) = dlc_uri {
765 ErrorHandlerConfig::dead_letter_channel(uri)
766 } else {
767 ErrorHandlerConfig::log_only()
768 };
769
770 for spec in specs {
771 let matcher = spec.matches.clone();
772 let mut builder = config.on_exception(move |e| matcher(e));
773
774 if let Some(retry) = spec.retry {
775 builder = builder.retry(retry.max_attempts).with_backoff(
776 retry.initial_delay,
777 retry.multiplier,
778 retry.max_delay,
779 );
780 if retry.jitter_factor > 0.0 {
781 builder = builder.with_jitter(retry.jitter_factor);
782 }
783 }
784
785 if let Some(uri) = spec.handled_by {
786 builder = builder.handled_by(uri);
787 }
788
789 config = builder.build();
790 }
791
792 Some(config)
793 }
794 };
795
796 apply_parameter_assignments(
799 &mut self.from_uri,
800 &mut self.steps,
801 std::mem::take(&mut self.parameter_assignments),
802 std::mem::take(&mut self.parameter_misuse),
803 )?;
804
805 let definition = RouteDefinition::new(self.from_uri, self.steps);
806 let definition = if let Some(eh) = resolved_error_handler {
807 definition.with_error_handler(eh)
808 } else {
809 definition
810 };
811 let definition = if let Some(cb) = self.circuit_breaker_config {
812 definition.with_circuit_breaker(cb)
813 } else {
814 definition
815 };
816 let definition = if let Some(sp) = self.security_policy_config {
817 definition.with_security_policy(sp)
818 } else {
819 definition
820 };
821 let definition = if let Some(auth) = self.security_authenticator {
822 definition.with_security_authenticator(auth)
823 } else {
824 definition
825 };
826 let definition = if let Some(concurrency) = self.concurrency {
827 definition.with_concurrency(concurrency)
828 } else {
829 definition
830 };
831 let definition = definition.with_route_id(route_id);
832 let definition = if let Some(auto) = self.auto_startup {
833 definition.with_auto_startup(auto)
834 } else {
835 definition
836 };
837 let definition = if let Some(order) = self.startup_order {
838 definition.with_startup_order(order)
839 } else {
840 definition
841 };
842 Ok(definition)
843 }
844
845 pub fn build_canonical(mut self) -> Result<CanonicalRouteSpec, CamelError> {
847 validate_uri(&self.from_uri)?;
848 let route_id = self
849 .route_id
850 .filter(|s| !s.trim().is_empty())
851 .ok_or_else(|| {
852 CamelError::RouteError(
853 "route must have a non-empty 'route_id' — call .route_id(\"name\") on the builder"
854 .to_string(),
855 )
856 })?;
857
858 apply_parameter_assignments(
860 &mut self.from_uri,
861 &mut self.steps,
862 std::mem::take(&mut self.parameter_assignments),
863 std::mem::take(&mut self.parameter_misuse),
864 )?;
865
866 let steps = canonicalize_steps(self.steps)?;
867 let circuit_breaker = self
868 .circuit_breaker_config
869 .map(canonicalize_circuit_breaker)
870 .transpose()?;
871
872 if self.security_policy_config.is_some() {
873 return Err(CamelError::RouteError(
874 "routes with security_policy cannot use the canonical/hot-reload path (not yet supported)"
875 .into(),
876 ));
877 }
878
879 let spec = CanonicalRouteSpec {
880 route_id,
881 from: self.from_uri,
882 steps,
883 circuit_breaker,
884 auto_startup: None,
885 startup_order: None,
886 concurrency: None,
887 version: camel_api::CANONICAL_CONTRACT_VERSION,
888 };
889 spec.validate_contract()?;
890 Ok(spec)
891 }
892}
893
894pub struct OnExceptionBuilder {
895 parent: RouteBuilder,
896 policy: OnExceptionSpec,
897}
898
899impl OnExceptionBuilder {
900 pub fn retry(mut self, max_attempts: u32) -> Self {
901 self.policy.retry = Some(RedeliveryPolicy::new(max_attempts));
902 self
903 }
904
905 pub fn with_backoff(
906 mut self,
907 initial: std::time::Duration,
908 multiplier: f64,
909 max: std::time::Duration,
910 ) -> Self {
911 if let Some(ref mut retry) = self.policy.retry {
912 retry.initial_delay = initial;
913 retry.multiplier = multiplier;
914 retry.max_delay = max;
915 } else {
916 tracing::warn!("backoff/jitter configuration has no effect when retry_count is 0");
917 }
918 self
919 }
920
921 pub fn with_jitter(mut self, jitter_factor: f64) -> Self {
922 if let Some(ref mut retry) = self.policy.retry {
923 retry.jitter_factor = jitter_factor.clamp(0.0, 1.0);
924 } else {
925 tracing::warn!("backoff/jitter configuration has no effect when retry_count is 0");
926 }
927 self
928 }
929
930 pub fn handled_by(mut self, uri: impl Into<String>) -> Self {
931 self.policy.handled_by = Some(uri.into());
932 self
933 }
934
935 pub fn end_on_exception(mut self) -> RouteBuilder {
936 if let ErrorHandlerMode::Shorthand { ref mut specs, .. } = self.parent.error_handler_mode {
937 specs.push(self.policy);
938 }
939 self.parent
940 }
941}
942
943fn endpoint_slot_bears_uri(slot: &EndpointSlot, steps: &[BuilderStep]) -> bool {
946 match slot {
947 EndpointSlot::From => true,
948 EndpointSlot::Step(i) => steps.get(*i).is_some_and(|step| {
949 matches!(
950 step,
951 BuilderStep::To(_)
952 | BuilderStep::WireTap { .. }
953 | BuilderStep::Enrich { .. }
954 | BuilderStep::PollEnrich { .. }
955 )
956 }),
957 }
958}
959
960fn apply_parameter_assignments(
969 from_uri: &mut String,
970 steps: &mut [BuilderStep],
971 assignments: Vec<(EndpointSlot, BTreeMap<String, String>)>,
972 misuse: Option<String>,
973) -> Result<(), CamelError> {
974 if let Some(reason) = misuse {
975 return Err(CamelError::RouteError(reason));
976 }
977
978 for (slot, params) in assignments {
979 if params.is_empty() {
983 continue;
984 }
985
986 let uri = match slot {
987 EndpointSlot::From => &mut *from_uri,
988 EndpointSlot::Step(i) => match steps.get_mut(i) {
989 Some(BuilderStep::To(uri)) => uri,
990 Some(BuilderStep::WireTap { uri, .. }) => uri,
991 Some(BuilderStep::Enrich { uri, .. }) => uri,
992 Some(BuilderStep::PollEnrich { uri, .. }) => uri,
993 Some(other) => {
994 return Err(CamelError::RouteError(format!(
995 ".parameters() attached to a non-endpoint step (`{}`)",
996 canonical_step_name(other)
997 )));
998 }
999 None => {
1000 return Err(CamelError::RouteError(
1001 ".parameters() attached to an out-of-range step index".to_string(),
1002 ));
1003 }
1004 },
1005 };
1006 let merged = EndpointUri::try_from_uri_and_params(uri, params)?.to_canonical_string();
1007 *uri = merged;
1008 }
1009
1010 Ok(())
1011}
1012
1013fn validate_uri(uri: &str) -> Result<(), CamelError> {
1015 let trimmed = uri.trim();
1016 if trimmed.is_empty() {
1017 return Err(CamelError::RouteError(
1018 "route must have a 'from' URI".to_string(),
1019 ));
1020 }
1021 if !trimmed.contains(':') {
1022 return Err(CamelError::RouteError(
1023 "URI must have a scheme (e.g. 'timer:tick')".to_string(),
1024 ));
1025 }
1026 let scheme = trimmed.split(':').next().unwrap_or("");
1027 if scheme.trim().is_empty() {
1028 return Err(CamelError::RouteError(
1029 "URI scheme must not be empty".to_string(),
1030 ));
1031 }
1032 Ok(())
1033}
1034
1035fn canonicalize_steps(steps: Vec<BuilderStep>) -> Result<Vec<CanonicalStepSpec>, CamelError> {
1036 let mut canonical = Vec::with_capacity(steps.len());
1037 for step in steps {
1038 canonical.push(canonicalize_step(step)?);
1039 }
1040 Ok(canonical)
1041}
1042
1043fn canonicalize_step(step: BuilderStep) -> Result<CanonicalStepSpec, CamelError> {
1044 match step {
1045 BuilderStep::To(uri) => Ok(CanonicalStepSpec::To { uri }),
1046 BuilderStep::Log { message, .. } => Ok(CanonicalStepSpec::Log { message }),
1047 BuilderStep::Stop => Ok(CanonicalStepSpec::Stop),
1048 BuilderStep::WireTap { uri } => Ok(CanonicalStepSpec::WireTap { uri }),
1049 BuilderStep::Delay { config } => Ok(CanonicalStepSpec::Delay {
1050 delay_ms: config.delay_ms,
1051 dynamic_header: config.dynamic_header,
1052 }),
1053 BuilderStep::DeclarativeScript { expression } => {
1054 Ok(CanonicalStepSpec::Script { expression })
1055 }
1056 BuilderStep::DeclarativeFilter { predicate, steps } => Ok(CanonicalStepSpec::Filter {
1057 predicate,
1058 steps: canonicalize_steps(steps)?,
1059 }),
1060 BuilderStep::DeclarativeChoice { whens, otherwise } => {
1061 let mut canonical_whens = Vec::with_capacity(whens.len());
1062 for DeclarativeWhenStep { predicate, steps } in whens {
1063 canonical_whens.push(CanonicalWhenSpec {
1064 predicate,
1065 steps: canonicalize_steps(steps)?,
1066 });
1067 }
1068 let otherwise = match otherwise {
1069 Some(steps) => Some(canonicalize_steps(steps)?),
1070 None => None,
1071 };
1072 Ok(CanonicalStepSpec::Choice {
1073 whens: canonical_whens,
1074 otherwise,
1075 })
1076 }
1077 BuilderStep::DeclarativeSplit {
1078 expression,
1079 aggregation,
1080 parallel,
1081 parallel_limit,
1082 stop_on_exception,
1083 steps,
1084 } => Ok(CanonicalStepSpec::Split {
1085 expression: CanonicalSplitExpressionSpec::Language(expression),
1086 aggregation: canonicalize_split_aggregation(aggregation)?,
1087 parallel,
1088 parallel_limit,
1089 stop_on_exception,
1090 steps: canonicalize_steps(steps)?,
1091 }),
1092 BuilderStep::Aggregate { config } => Ok(CanonicalStepSpec::Aggregate(
1093 canonicalize_aggregate(config)?,
1094 )),
1095 other => {
1096 let step_name = canonical_step_name(&other);
1097 let detail = camel_api::canonical_contract_rejection_reason(step_name)
1098 .unwrap_or("not included in canonical v2");
1099 Err(CamelError::RouteError(format!(
1100 "canonical v2 does not support step `{step_name}`: {detail}"
1101 )))
1102 }
1103 }
1104}
1105
1106fn canonicalize_split_aggregation(
1107 strategy: camel_api::splitter::AggregationStrategy,
1108) -> Result<CanonicalSplitAggregationSpec, CamelError> {
1109 match strategy {
1110 camel_api::splitter::AggregationStrategy::LastWins => {
1111 Ok(CanonicalSplitAggregationSpec::LastWins)
1112 }
1113 camel_api::splitter::AggregationStrategy::CollectAll => {
1114 Ok(CanonicalSplitAggregationSpec::CollectAll)
1115 }
1116 camel_api::splitter::AggregationStrategy::Custom(_) => Err(CamelError::RouteError(
1117 "canonical v2 does not support custom split aggregation".to_string(),
1118 )),
1119 camel_api::splitter::AggregationStrategy::Original => {
1120 Ok(CanonicalSplitAggregationSpec::Original)
1121 }
1122 _ => Err(CamelError::RouteError(
1123 "canonical v2 does not support this split aggregation strategy".to_string(),
1124 )),
1125 }
1126}
1127
1128fn extract_completion_fields(
1129 mode: &CompletionMode,
1130) -> Result<(Option<usize>, Option<u64>), CamelError> {
1131 match mode {
1132 CompletionMode::Single(cond) => match cond {
1133 CompletionCondition::Size(n) => Ok((Some(*n), None)),
1134 CompletionCondition::Timeout(d) => Ok((None, Some(d.as_millis() as u64))),
1135 CompletionCondition::Predicate(_) | CompletionCondition::PredicateExpr { .. } => {
1136 Err(CamelError::RouteError(
1137 "aggregate PredicateExpr/Predicate completion cannot reverse-map to canonical \
1138 (forward-only in rc-zit); build the canonical spec directly"
1139 .to_string(),
1140 ))
1141 }
1142 _ => Err(CamelError::RouteError(
1143 "unsupported completion condition".to_string(),
1144 )),
1145 },
1146 CompletionMode::Any(conds) => {
1147 let mut size = None;
1148 let mut timeout_ms = None;
1149 for cond in conds {
1150 match cond {
1151 CompletionCondition::Size(n) => size = Some(*n),
1152 CompletionCondition::Timeout(d) => timeout_ms = Some(d.as_millis() as u64),
1153 CompletionCondition::Predicate(_)
1154 | CompletionCondition::PredicateExpr { .. } => {
1155 return Err(CamelError::RouteError(
1156 "aggregate PredicateExpr/Predicate completion cannot reverse-map to \
1157 canonical (forward-only in rc-zit); build the canonical spec directly"
1158 .to_string(),
1159 ));
1160 }
1161 _ => {
1162 return Err(CamelError::RouteError(
1163 "unsupported completion condition".to_string(),
1164 ));
1165 }
1166 }
1167 }
1168 Ok((size, timeout_ms))
1169 }
1170 _ => Err(CamelError::RouteError(
1171 "unsupported completion mode".to_string(),
1172 )),
1173 }
1174}
1175
1176fn canonicalize_aggregate(config: AggregatorConfig) -> Result<CanonicalAggregateSpec, CamelError> {
1177 let (completion_size, completion_timeout_ms) = extract_completion_fields(&config.completion)?;
1178
1179 let header = match &config.correlation {
1180 CorrelationStrategy::HeaderName(h) => h.clone(),
1181 CorrelationStrategy::Expression { expr, .. } => expr.clone(),
1182 CorrelationStrategy::Fn(_) => {
1183 return Err(CamelError::RouteError(
1184 "canonical v2 does not support Fn correlation strategy".to_string(),
1185 ));
1186 }
1187 _ => {
1188 return Err(CamelError::RouteError(
1189 "canonical v2 does not support this correlation strategy".to_string(),
1190 ));
1191 }
1192 };
1193
1194 let correlation_key = match &config.correlation {
1195 CorrelationStrategy::HeaderName(_) => None,
1196 CorrelationStrategy::Expression { expr, .. } => Some(expr.clone()),
1197 _ => unreachable!(),
1201 };
1202
1203 let strategy = match config.strategy {
1204 AggregationStrategy::CollectAll => CanonicalAggregateStrategySpec::CollectAll,
1205 AggregationStrategy::Custom(_) => {
1206 return Err(CamelError::RouteError(
1207 "canonical v2 does not support custom aggregate strategy".to_string(),
1208 ));
1209 }
1210 _ => {
1211 return Err(CamelError::RouteError(
1212 "canonical v2 does not support this aggregate strategy".to_string(),
1213 ));
1214 }
1215 };
1216 let bucket_ttl_ms = config
1217 .bucket_ttl
1218 .map(|ttl| u64::try_from(ttl.as_millis()).unwrap_or(u64::MAX));
1219
1220 Ok(CanonicalAggregateSpec {
1221 header,
1222 completion_size,
1223 completion_timeout_ms,
1224 correlation_key,
1225 force_completion_on_stop: if config.force_completion_on_stop {
1226 Some(true)
1227 } else {
1228 None
1229 },
1230 discard_on_timeout: if config.discard_on_timeout {
1231 Some(true)
1232 } else {
1233 None
1234 },
1235 strategy,
1236 max_buckets: config.max_buckets,
1237 bucket_ttl_ms,
1238 completion_predicate: None,
1239 })
1240}
1241
1242fn canonicalize_circuit_breaker(
1243 config: CircuitBreakerConfig,
1244) -> Result<CanonicalCircuitBreakerSpec, CamelError> {
1245 if config.fallback.is_some() {
1246 return Err(CamelError::RouteError(
1247 "canonical v2 does not support circuit breaker `fallback` (opaque BoxProcessor \
1248 cannot reverse-map to canonical steps); build the canonical spec directly"
1249 .to_string(),
1250 ));
1251 }
1252 Ok(CanonicalCircuitBreakerSpec {
1253 failure_threshold: config.failure_threshold,
1254 open_duration_ms: u64::try_from(config.open_duration.as_millis()).unwrap_or(u64::MAX),
1255 fallback: Vec::new(),
1256 })
1257}
1258
1259fn canonical_step_name(step: &BuilderStep) -> &'static str {
1260 match step {
1261 BuilderStep::Processor(_) => "processor",
1262 BuilderStep::To(_) => "to",
1263 BuilderStep::Stop => "stop",
1264 BuilderStep::Log { .. } => "log",
1265 BuilderStep::DeclarativeSetHeader { .. } => "set_header",
1266 BuilderStep::DeclarativeSetHeaderIfAbsent { .. } => "set_header_if_absent",
1267 BuilderStep::DeclarativeRemoveHeader { .. } => "remove_header",
1268 BuilderStep::DeclarativeSetBody { .. } => "set_body",
1269 BuilderStep::DeclarativeFilter { .. } => "filter",
1270 BuilderStep::DeclarativeChoice { .. } => "choice",
1271 BuilderStep::DeclarativeScript { .. } => "script",
1272 BuilderStep::DeclarativeFunction { .. } => "function",
1273 BuilderStep::DeclarativeSplit { .. } => "split",
1274 BuilderStep::Split { .. } => "split",
1275 BuilderStep::Loop { .. } | BuilderStep::DeclarativeLoop { .. } => "loop",
1276 BuilderStep::Aggregate { .. } => "aggregate",
1277 BuilderStep::Filter { .. } => "filter",
1278 BuilderStep::Choice { .. } => "choice",
1279 BuilderStep::WireTap { .. } => "wire_tap",
1280 BuilderStep::Delay { .. } => "delay",
1281 BuilderStep::Multicast { .. } => "multicast",
1282 BuilderStep::DeclarativeLog { .. } => "log",
1283 BuilderStep::Bean { .. } => "bean",
1284 BuilderStep::Script { .. } => "script",
1285 BuilderStep::Throttle { .. } => "throttle",
1286 BuilderStep::LoadBalance { .. } => "load_balancer",
1287 BuilderStep::DynamicRouter { .. } => "dynamic_router",
1288 BuilderStep::RoutingSlip { .. } => "routing_slip",
1289 BuilderStep::DeclarativeDynamicRouter { .. } => "declarative_dynamic_router",
1290 BuilderStep::DeclarativeRoutingSlip { .. } => "declarative_routing_slip",
1291 BuilderStep::RecipientList { .. } => "recipient_list",
1292 BuilderStep::DeclarativeRecipientList { .. } => "declarative_recipient_list",
1293 BuilderStep::DeclarativeSetProperty { .. } => "set_property",
1294 BuilderStep::DeclarativeStreamSplit { .. } => "stream_split",
1295 BuilderStep::Enrich { .. } => "enrich",
1296 BuilderStep::PollEnrich { .. } => "poll_enrich",
1297 BuilderStep::Validate { .. } => "validate",
1298 BuilderStep::IdempotentConsumer { .. } => "idempotent_consumer",
1299 BuilderStep::ClaimCheck { .. } => "claim_check",
1300 BuilderStep::Cache { .. } => "cache",
1301 BuilderStep::CacheInvalidate { .. } => "cache_invalidate",
1302 BuilderStep::CachePeekStale { .. } => "cache_peek_stale",
1303 BuilderStep::Sampling { .. } => "sampling",
1304 BuilderStep::Sort { .. } => "sort",
1305 BuilderStep::DeclarativeDoTry { .. } => "do_try",
1306 BuilderStep::Resequence { .. } => "resequence",
1307 }
1308}
1309
1310impl StepAccumulator for RouteBuilder {
1311 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1312 &mut self.steps
1313 }
1314}
1315
1316pub struct SplitBuilder {
1324 parent: RouteBuilder,
1325 config: SplitterConfig,
1326 steps: Vec<BuilderStep>,
1327}
1328
1329impl SplitBuilder {
1330 pub fn filter<F>(self, predicate: F) -> FilterInSplitBuilder
1332 where
1333 F: Fn(&Exchange) -> bool + Send + Sync + 'static,
1334 {
1335 FilterInSplitBuilder {
1336 parent: self,
1337 predicate: camel_api::FilterPredicate::new(predicate),
1338 steps: vec![],
1339 }
1340 }
1341
1342 pub fn end_split(mut self) -> RouteBuilder {
1345 let split_step = BuilderStep::Split {
1346 config: self.config,
1347 steps: self.steps,
1348 };
1349 self.parent.steps.push(split_step);
1350 self.parent
1351 }
1352}
1353
1354impl StepAccumulator for SplitBuilder {
1355 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1356 &mut self.steps
1357 }
1358}
1359
1360pub struct FilterBuilder {
1362 parent: RouteBuilder,
1363 predicate: FilterPredicate,
1364 steps: Vec<BuilderStep>,
1365}
1366
1367impl FilterBuilder {
1368 pub fn end_filter(mut self) -> RouteBuilder {
1371 let step = BuilderStep::Filter {
1372 predicate: self.predicate,
1373 steps: self.steps,
1374 };
1375 self.parent.steps.push(step);
1376 self.parent
1377 }
1378}
1379
1380impl StepAccumulator for FilterBuilder {
1381 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1382 &mut self.steps
1383 }
1384}
1385
1386pub struct FilterInSplitBuilder {
1388 parent: SplitBuilder,
1389 predicate: FilterPredicate,
1390 steps: Vec<BuilderStep>,
1391}
1392
1393impl FilterInSplitBuilder {
1394 pub fn end_filter(mut self) -> SplitBuilder {
1396 let step = BuilderStep::Filter {
1397 predicate: self.predicate,
1398 steps: self.steps,
1399 };
1400 self.parent.steps.push(step);
1401 self.parent
1402 }
1403}
1404
1405impl StepAccumulator for FilterInSplitBuilder {
1406 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1407 &mut self.steps
1408 }
1409}
1410
1411pub struct ChoiceBuilder {
1418 parent: RouteBuilder,
1419 whens: Vec<WhenStep>,
1420 _otherwise: Option<Vec<BuilderStep>>,
1421}
1422
1423impl ChoiceBuilder {
1424 pub fn when<F>(self, predicate: F) -> WhenBuilder
1427 where
1428 F: Fn(&Exchange) -> bool + Send + Sync + 'static,
1429 {
1430 WhenBuilder {
1431 parent: self,
1432 predicate: camel_api::FilterPredicate::new(predicate),
1433 steps: vec![],
1434 }
1435 }
1436
1437 pub fn otherwise(self) -> OtherwiseBuilder {
1441 OtherwiseBuilder {
1442 parent: self,
1443 steps: vec![],
1444 }
1445 }
1446
1447 pub fn end_choice(mut self) -> RouteBuilder {
1451 let step = BuilderStep::Choice {
1452 whens: self.whens,
1453 otherwise: self._otherwise,
1454 };
1455 self.parent.steps.push(step);
1456 self.parent
1457 }
1458}
1459
1460pub struct WhenBuilder {
1462 parent: ChoiceBuilder,
1463 predicate: camel_api::FilterPredicate,
1464 steps: Vec<BuilderStep>,
1465}
1466
1467impl WhenBuilder {
1468 pub fn end_when(mut self) -> ChoiceBuilder {
1471 self.parent.whens.push(WhenStep {
1472 predicate: self.predicate,
1473 steps: self.steps,
1474 });
1475 self.parent
1476 }
1477}
1478
1479impl StepAccumulator for WhenBuilder {
1480 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1481 &mut self.steps
1482 }
1483}
1484
1485pub struct OtherwiseBuilder {
1487 parent: ChoiceBuilder,
1488 steps: Vec<BuilderStep>,
1489}
1490
1491impl OtherwiseBuilder {
1492 pub fn end_otherwise(self) -> ChoiceBuilder {
1494 let OtherwiseBuilder { mut parent, steps } = self;
1495 parent._otherwise = Some(steps);
1496 parent
1497 }
1498}
1499
1500impl StepAccumulator for OtherwiseBuilder {
1501 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1502 &mut self.steps
1503 }
1504}
1505
1506pub struct MulticastBuilder {
1514 parent: RouteBuilder,
1515 steps: Vec<BuilderStep>,
1516 config: MulticastConfig,
1517}
1518
1519impl MulticastBuilder {
1520 pub fn parallel(mut self, parallel: bool) -> Self {
1521 self.config = self.config.parallel(parallel);
1522 self
1523 }
1524
1525 pub fn parallel_limit(mut self, limit: usize) -> Self {
1526 self.config = self.config.parallel_limit(limit);
1527 self
1528 }
1529
1530 pub fn stop_on_exception(mut self, stop: bool) -> Self {
1531 self.config = self.config.stop_on_exception(stop);
1532 self
1533 }
1534
1535 pub fn timeout(mut self, duration: std::time::Duration) -> Self {
1536 self.config = self.config.timeout(duration);
1537 self
1538 }
1539
1540 pub fn aggregation(mut self, strategy: MulticastStrategy) -> Self {
1541 self.config = self.config.aggregation(strategy);
1542 self
1543 }
1544
1545 pub fn end_multicast(mut self) -> RouteBuilder {
1546 let step = BuilderStep::Multicast {
1547 steps: self.steps,
1548 config: self.config,
1549 };
1550 self.parent.steps.push(step);
1551 self.parent
1552 }
1553}
1554
1555impl StepAccumulator for MulticastBuilder {
1556 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1557 &mut self.steps
1558 }
1559}
1560
1561pub struct ThrottleBuilder {
1569 parent: RouteBuilder,
1570 config: ThrottlerConfig,
1571 steps: Vec<BuilderStep>,
1572}
1573
1574impl ThrottleBuilder {
1575 pub fn strategy(mut self, strategy: ThrottleStrategy) -> Self {
1581 self.config = self.config.strategy(strategy);
1582 self
1583 }
1584
1585 pub fn end_throttle(mut self) -> RouteBuilder {
1588 let step = BuilderStep::Throttle {
1589 config: self.config,
1590 steps: self.steps,
1591 };
1592 self.parent.steps.push(step);
1593 self.parent
1594 }
1595}
1596
1597impl StepAccumulator for ThrottleBuilder {
1598 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1599 &mut self.steps
1600 }
1601}
1602
1603pub struct LoopBuilder {
1605 parent: RouteBuilder,
1606 config: LoopConfig,
1607 steps: Vec<BuilderStep>,
1608}
1609
1610impl LoopBuilder {
1611 pub fn loop_count(self, count: usize) -> LoopInLoopBuilder {
1612 LoopInLoopBuilder {
1613 parent: self,
1614 config: LoopConfig::new(LoopMode::Count(count)),
1615 steps: vec![],
1616 }
1617 }
1618
1619 pub fn loop_while<F>(self, predicate: F) -> LoopInLoopBuilder
1620 where
1621 F: Fn(&Exchange) -> bool + Send + Sync + 'static,
1622 {
1623 LoopInLoopBuilder {
1624 parent: self,
1625 config: LoopConfig::new(LoopMode::While(camel_api::FilterPredicate::new(predicate))),
1626 steps: vec![],
1627 }
1628 }
1629
1630 pub fn end_loop(mut self) -> RouteBuilder {
1631 let step = BuilderStep::Loop {
1632 config: self.config,
1633 steps: self.steps,
1634 };
1635 self.parent.steps.push(step);
1636 self.parent
1637 }
1638}
1639
1640impl StepAccumulator for LoopBuilder {
1641 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1642 &mut self.steps
1643 }
1644}
1645
1646pub struct LoopInLoopBuilder {
1647 parent: LoopBuilder,
1648 config: LoopConfig,
1649 steps: Vec<BuilderStep>,
1650}
1651
1652impl LoopInLoopBuilder {
1653 pub fn end_loop(mut self) -> LoopBuilder {
1654 let step = BuilderStep::Loop {
1655 config: self.config,
1656 steps: self.steps,
1657 };
1658 self.parent.steps.push(step);
1659 self.parent
1660 }
1661}
1662
1663impl StepAccumulator for LoopInLoopBuilder {
1664 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1665 &mut self.steps
1666 }
1667}
1668
1669pub struct LoadBalancerBuilder {
1677 parent: RouteBuilder,
1678 config: LoadBalancerConfig,
1679 steps: Vec<BuilderStep>,
1680}
1681
1682impl LoadBalancerBuilder {
1683 pub fn round_robin(mut self) -> Self {
1685 self.config = LoadBalancerConfig::round_robin();
1686 self
1687 }
1688
1689 pub fn random(mut self) -> Self {
1691 self.config = LoadBalancerConfig::random();
1692 self
1693 }
1694
1695 pub fn weighted(mut self, weights: Vec<(String, u32)>) -> Self {
1700 self.config = LoadBalancerConfig::weighted(weights);
1701 self
1702 }
1703
1704 pub fn failover(mut self) -> Self {
1709 self.config = LoadBalancerConfig::failover();
1710 self
1711 }
1712
1713 pub fn end_load_balance(mut self) -> RouteBuilder {
1716 let step = BuilderStep::LoadBalance {
1717 config: self.config,
1718 steps: self.steps,
1719 };
1720 self.parent.steps.push(step);
1721 self.parent
1722 }
1723}
1724
1725impl StepAccumulator for LoadBalancerBuilder {
1726 fn steps_mut(&mut self) -> &mut Vec<BuilderStep> {
1727 &mut self.steps
1728 }
1729}
1730
1731#[cfg(test)]
1736mod tests {
1737 use super::*;
1738 use camel_api::error_handler::ErrorHandlerConfig;
1739 use camel_api::load_balancer::LoadBalanceStrategy;
1740 use camel_api::{Exchange, Message};
1741 use camel_core::route::BuilderStep;
1742 use std::sync::Arc;
1743 use std::time::Duration;
1744 use tower::{Service, ServiceExt};
1745
1746 #[test]
1747 fn test_builder_from_creates_definition() {
1748 let definition = RouteBuilder::from("timer:tick")
1749 .route_id("test-route")
1750 .build()
1751 .unwrap();
1752 assert_eq!(definition.from_uri(), "timer:tick");
1753 }
1754
1755 #[test]
1756 fn test_builder_empty_from_uri_errors() {
1757 let result = RouteBuilder::from("").route_id("test-route").build();
1758 assert!(result.is_err());
1759 }
1760
1761 #[test]
1762 fn test_build_rejects_schemeless_uri() {
1763 let result = RouteBuilder::from("no-scheme-here")
1764 .route_id("test-route")
1765 .build();
1766 match result {
1767 Err(err) => {
1768 let err_msg = format!("{err}");
1769 assert!(
1770 err_msg.contains("scheme"),
1771 "expected scheme-related error, got: {err_msg}"
1772 );
1773 }
1774 Ok(_) => panic!("schemeless URI should fail"),
1775 }
1776 }
1777
1778 #[test]
1779 fn test_build_rejects_empty_scheme_uri() {
1780 let result = RouteBuilder::from(":missing-scheme")
1781 .route_id("test-route")
1782 .build();
1783 match result {
1784 Err(err) => {
1785 let err_msg = format!("{err}");
1786 assert!(
1787 err_msg.contains("scheme"),
1788 "expected scheme-related error, got: {err_msg}"
1789 );
1790 }
1791 Ok(_) => panic!("empty-scheme URI should fail"),
1792 }
1793 }
1794
1795 #[test]
1796 fn test_build_accepts_valid_uri() {
1797 let result = RouteBuilder::from("timer:tick")
1798 .route_id("test-route")
1799 .build();
1800 assert!(result.is_ok());
1801 }
1802
1803 #[test]
1804 fn test_build_canonical_rejects_schemeless_uri() {
1805 let result = RouteBuilder::from("no-scheme-here")
1806 .route_id("test-route")
1807 .build_canonical();
1808 assert!(result.is_err());
1809 }
1810
1811 #[test]
1812 fn test_builder_to_adds_step() {
1813 let definition = RouteBuilder::from("timer:tick")
1814 .route_id("test-route")
1815 .to("log:info")
1816 .build()
1817 .unwrap();
1818
1819 assert_eq!(definition.from_uri(), "timer:tick");
1820 assert!(matches!(&definition.steps()[0], BuilderStep::To(uri) if uri == "log:info"));
1822 }
1823
1824 #[test]
1825 fn test_builder_filter_adds_filter_step() {
1826 let definition = RouteBuilder::from("timer:tick")
1827 .route_id("test-route")
1828 .filter(|_ex| true)
1829 .to("mock:result")
1830 .end_filter()
1831 .build()
1832 .unwrap();
1833
1834 assert!(matches!(&definition.steps()[0], BuilderStep::Filter { .. }));
1835 }
1836
1837 #[test]
1838 fn test_builder_set_header_adds_processor_step() {
1839 let definition = RouteBuilder::from("timer:tick")
1840 .route_id("test-route")
1841 .set_header("key", Value::String("value".into()))
1842 .build()
1843 .unwrap();
1844
1845 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
1846 }
1847
1848 #[test]
1849 fn test_builder_map_body_adds_processor_step() {
1850 let definition = RouteBuilder::from("timer:tick")
1851 .route_id("test-route")
1852 .map_body(|body| body)
1853 .build()
1854 .unwrap();
1855
1856 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
1857 }
1858
1859 #[test]
1860 fn test_builder_process_adds_processor_step() {
1861 let definition = RouteBuilder::from("timer:tick")
1862 .route_id("test-route")
1863 .process(|ex| async move { Ok(ex) })
1864 .build()
1865 .unwrap();
1866
1867 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
1868 }
1869
1870 #[test]
1871 fn test_builder_chain_multiple_steps() {
1872 let definition = RouteBuilder::from("timer:tick")
1873 .route_id("test-route")
1874 .set_header("source", Value::String("timer".into()))
1875 .filter(|ex| ex.input.header("source").is_some())
1876 .to("log:info")
1877 .end_filter()
1878 .to("mock:result")
1879 .build()
1880 .unwrap();
1881
1882 assert_eq!(definition.steps().len(), 3); assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_))); assert!(matches!(&definition.steps()[1], BuilderStep::Filter { .. })); assert!(matches!(&definition.steps()[2], BuilderStep::To(uri) if uri == "mock:result"));
1886 }
1887
1888 #[test]
1889 fn test_loop_count_builder() {
1890 use camel_api::loop_eip::LoopMode;
1891
1892 let def = RouteBuilder::from("direct:start")
1893 .route_id("loop-test")
1894 .loop_count(3)
1895 .to("mock:inside")
1896 .end_loop()
1897 .to("mock:after")
1898 .build()
1899 .unwrap();
1900
1901 assert_eq!(def.steps().len(), 2);
1902 match &def.steps()[0] {
1903 BuilderStep::Loop { config, steps } => {
1904 assert!(matches!(config.mode, LoopMode::Count(3)));
1905 assert_eq!(steps.len(), 1);
1906 }
1907 other => panic!("Expected Loop, got {:?}", other),
1908 }
1909 assert!(matches!(def.steps()[1], BuilderStep::To(_)));
1910 }
1911
1912 #[test]
1913 fn test_loop_while_builder() {
1914 use camel_api::loop_eip::LoopMode;
1915
1916 let def = RouteBuilder::from("direct:start")
1917 .route_id("loop-while-test")
1918 .loop_while(|_ex| true)
1919 .to("mock:retry")
1920 .end_loop()
1921 .build()
1922 .unwrap();
1923
1924 assert_eq!(def.steps().len(), 1);
1925 match &def.steps()[0] {
1926 BuilderStep::Loop { config, steps } => {
1927 assert!(matches!(config.mode, LoopMode::While(_)));
1928 assert_eq!(steps.len(), 1);
1929 }
1930 other => panic!("Expected Loop, got {:?}", other),
1931 }
1932 }
1933
1934 #[test]
1935 fn test_nested_loop_builder() {
1936 use camel_api::loop_eip::LoopMode;
1937
1938 let def = RouteBuilder::from("direct:start")
1939 .route_id("nested-loop-test")
1940 .loop_count(2)
1941 .to("mock:outer")
1942 .loop_count(3)
1943 .to("mock:inner")
1944 .end_loop()
1945 .end_loop()
1946 .to("mock:after")
1947 .build()
1948 .unwrap();
1949
1950 assert_eq!(def.steps().len(), 2);
1951 match &def.steps()[0] {
1952 BuilderStep::Loop { steps, .. } => {
1953 assert_eq!(steps.len(), 2);
1954 match &steps[1] {
1955 BuilderStep::Loop {
1956 config,
1957 steps: inner_steps,
1958 } => {
1959 assert!(matches!(config.mode, LoopMode::Count(3)));
1960 assert_eq!(inner_steps.len(), 1);
1961 }
1962 other => panic!("Expected nested Loop, got {:?}", other),
1963 }
1964 }
1965 other => panic!("Expected outer Loop, got {:?}", other),
1966 }
1967 }
1968
1969 #[tokio::test]
1974 async fn test_set_header_processor_works() {
1975 let mut svc = SetHeader::new(IdentityProcessor, "greeting", Value::String("hello".into()));
1976 let exchange = Exchange::new(Message::new("test"));
1977 let result = svc.call(exchange).await.unwrap();
1978 assert_eq!(
1979 result.input.header("greeting"),
1980 Some(&Value::String("hello".into()))
1981 );
1982 }
1983
1984 #[tokio::test]
1985 async fn test_filter_processor_passes() {
1986 use camel_api::BoxProcessorExt;
1987 use camel_processor::FilterService;
1988
1989 let sub = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
1990 let mut svc =
1991 FilterService::new(|ex: &Exchange| ex.input.body.as_text() == Some("pass"), sub);
1992 let exchange = Exchange::new(Message::new("pass"));
1993 let result = svc.ready().await.unwrap().call(exchange).await.unwrap();
1994 assert_eq!(result.input.body.as_text(), Some("pass"));
1995 }
1996
1997 #[tokio::test]
1998 async fn test_filter_processor_blocks() {
1999 use camel_api::BoxProcessorExt;
2000 use camel_processor::FilterService;
2001
2002 let sub = BoxProcessor::from_fn(|_ex| {
2003 Box::pin(async move { Err(CamelError::ProcessorError("should not reach".into())) })
2004 });
2005 let mut svc =
2006 FilterService::new(|ex: &Exchange| ex.input.body.as_text() == Some("pass"), sub);
2007 let exchange = Exchange::new(Message::new("reject"));
2008 let result = svc.ready().await.unwrap().call(exchange).await.unwrap();
2009 assert_eq!(result.input.body.as_text(), Some("reject"));
2010 }
2011
2012 #[tokio::test]
2013 async fn test_map_body_processor_works() {
2014 let mapper = MapBody::new(IdentityProcessor, |body: Body| {
2015 if let Some(text) = body.as_text() {
2016 Body::Text(text.to_uppercase())
2017 } else {
2018 body
2019 }
2020 });
2021 let exchange = Exchange::new(Message::new("hello"));
2022 let result = mapper.oneshot(exchange).await.unwrap();
2023 assert_eq!(result.input.body.as_text(), Some("HELLO"));
2024 }
2025
2026 #[tokio::test]
2027 async fn test_process_custom_processor_works() {
2028 let processor = ProcessorFn::new(|mut ex: Exchange| async move {
2029 ex.set_property("custom", Value::Bool(true));
2030 Ok(ex)
2031 });
2032 let exchange = Exchange::new(Message::default());
2033 let result = processor.oneshot(exchange).await.unwrap();
2034 assert_eq!(result.property("custom"), Some(&Value::Bool(true)));
2035 }
2036
2037 #[tokio::test]
2042 async fn test_compose_pipeline_runs_steps_in_order() {
2043 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
2044
2045 let processors = vec![
2046 CompiledStep::Process {
2047 processor: BoxProcessor::new(SetHeader::new(
2048 IdentityProcessor,
2049 "step",
2050 Value::String("one".into()),
2051 )),
2052 body_contract: None,
2053 lifecycle: None,
2054 },
2055 CompiledStep::Process {
2056 processor: BoxProcessor::new(MapBody::new(IdentityProcessor, |body: Body| {
2057 if let Some(text) = body.as_text() {
2058 Body::Text(format!("{}-processed", text))
2059 } else {
2060 body
2061 }
2062 })),
2063 body_contract: None,
2064 lifecycle: None,
2065 },
2066 ];
2067
2068 let pipeline = compose_pipeline(processors, PipelineRuntimeCtx::compile_time());
2069 let exchange = Exchange::new(Message::new("hello"));
2070 let result = pipeline.oneshot(exchange).await.unwrap();
2071
2072 assert_eq!(
2073 result.input.header("step"),
2074 Some(&Value::String("one".into()))
2075 );
2076 assert_eq!(result.input.body.as_text(), Some("hello-processed"));
2077 }
2078
2079 #[tokio::test]
2080 async fn test_compose_pipeline_empty_is_identity() {
2081 use camel_core::route::{PipelineRuntimeCtx, compose_pipeline};
2082
2083 let pipeline = compose_pipeline(vec![], PipelineRuntimeCtx::compile_time());
2084 let exchange = Exchange::new(Message::new("unchanged"));
2085 let result = pipeline.oneshot(exchange).await.unwrap();
2086 assert_eq!(result.input.body.as_text(), Some("unchanged"));
2087 }
2088
2089 #[test]
2094 fn test_builder_circuit_breaker_sets_config() {
2095 use camel_api::circuit_breaker::CircuitBreakerConfig;
2096
2097 let config = CircuitBreakerConfig::new().failure_threshold(5);
2098 let definition = RouteBuilder::from("timer:tick")
2099 .route_id("test-route")
2100 .circuit_breaker(config)
2101 .build()
2102 .unwrap();
2103
2104 let cb = definition
2105 .circuit_breaker_config()
2106 .expect("circuit breaker should be set");
2107 assert_eq!(cb.failure_threshold, 5);
2108 }
2109
2110 #[test]
2111 fn test_builder_circuit_breaker_with_error_handler() {
2112 use camel_api::circuit_breaker::CircuitBreakerConfig;
2113 use camel_api::error_handler::ErrorHandlerConfig;
2114
2115 let cb_config = CircuitBreakerConfig::new().failure_threshold(3);
2116 let eh_config = ErrorHandlerConfig::log_only();
2117
2118 let definition = RouteBuilder::from("timer:tick")
2119 .route_id("test-route")
2120 .to("log:info")
2121 .circuit_breaker(cb_config)
2122 .error_handler(eh_config)
2123 .build()
2124 .unwrap();
2125
2126 assert!(
2127 definition.circuit_breaker_config().is_some(),
2128 "circuit breaker config should be set"
2129 );
2130 }
2132
2133 #[test]
2134 fn test_builder_on_exception_shorthand_multiple_clauses_preserve_order() {
2135 let definition = RouteBuilder::from("direct:start")
2136 .route_id("test-route")
2137 .dead_letter_channel("log:dlc")
2138 .on_exception(|e| matches!(e, CamelError::Io(_)))
2139 .retry(3)
2140 .handled_by("log:io")
2141 .end_on_exception()
2142 .on_exception(|e| matches!(e, CamelError::ProcessorError(_)))
2143 .retry(1)
2144 .end_on_exception()
2145 .to("mock:out")
2146 .build()
2147 .expect("route should build");
2148
2149 let cfg = definition
2150 .error_handler_config()
2151 .expect("error handler should be set");
2152 assert_eq!(cfg.policies.len(), 2);
2153 assert_eq!(cfg.dlc_uri.as_deref(), Some("log:dlc"));
2154 assert_eq!(
2155 cfg.policies[0].retry.as_ref().map(|p| p.max_attempts),
2156 Some(3)
2157 );
2158 assert_eq!(cfg.policies[0].handled_by.as_deref(), Some("log:io"));
2159 assert_eq!(
2160 cfg.policies[1].retry.as_ref().map(|p| p.max_attempts),
2161 Some(1)
2162 );
2163 }
2164
2165 #[test]
2166 fn test_builder_on_exception_mixed_mode_rejected() {
2167 let result = RouteBuilder::from("direct:start")
2168 .route_id("test-route")
2169 .error_handler(ErrorHandlerConfig::log_only())
2170 .on_exception(|_e| true)
2171 .end_on_exception()
2172 .to("mock:out")
2173 .build();
2174
2175 let err = result.err().expect("mixed mode should fail with an error");
2176
2177 assert!(
2178 format!("{err}").contains("mixed error handler modes"),
2179 "unexpected error: {err}"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_builder_on_exception_backoff_and_jitter_without_retry_noop() {
2185 let definition = RouteBuilder::from("direct:start")
2186 .route_id("test-route")
2187 .on_exception(|_e| true)
2188 .with_backoff(Duration::from_millis(5), 3.0, Duration::from_millis(100))
2189 .with_jitter(0.5)
2190 .end_on_exception()
2191 .to("mock:out")
2192 .build()
2193 .expect("route should build");
2194
2195 let cfg = definition
2196 .error_handler_config()
2197 .expect("error handler should be set");
2198 assert_eq!(cfg.policies.len(), 1);
2199 assert!(cfg.policies[0].retry.is_none());
2200 }
2201
2202 #[test]
2203 fn test_builder_dead_letter_channel_without_on_exception_sets_dlc() {
2204 let definition = RouteBuilder::from("direct:start")
2205 .route_id("test-route")
2206 .dead_letter_channel("log:dlc")
2207 .to("mock:out")
2208 .build()
2209 .expect("route should build");
2210
2211 let cfg = definition
2212 .error_handler_config()
2213 .expect("error handler should be set");
2214 assert_eq!(cfg.dlc_uri.as_deref(), Some("log:dlc"));
2215 assert!(cfg.policies.is_empty());
2216 }
2217
2218 #[test]
2219 fn test_builder_dead_letter_channel_called_twice_uses_latest_and_keeps_policies() {
2220 let definition = RouteBuilder::from("direct:start")
2221 .route_id("test-route")
2222 .dead_letter_channel("log:first")
2223 .on_exception(|e| matches!(e, CamelError::Io(_)))
2224 .retry(2)
2225 .end_on_exception()
2226 .dead_letter_channel("log:second")
2227 .to("mock:out")
2228 .build()
2229 .expect("route should build");
2230
2231 let cfg = definition
2232 .error_handler_config()
2233 .expect("error handler should be set");
2234 assert_eq!(cfg.dlc_uri.as_deref(), Some("log:second"));
2235 assert_eq!(cfg.policies.len(), 1);
2236 assert_eq!(
2237 cfg.policies[0].retry.as_ref().map(|p| p.max_attempts),
2238 Some(2)
2239 );
2240 }
2241
2242 #[test]
2243 fn test_builder_on_exception_without_dlc_defaults_to_log_only() {
2244 let definition = RouteBuilder::from("direct:start")
2245 .route_id("test-route")
2246 .on_exception(|e| matches!(e, CamelError::ProcessorError(_)))
2247 .retry(1)
2248 .end_on_exception()
2249 .to("mock:out")
2250 .build()
2251 .expect("route should build");
2252
2253 let cfg = definition
2254 .error_handler_config()
2255 .expect("error handler should be set");
2256 assert!(cfg.dlc_uri.is_none());
2257 assert_eq!(cfg.policies.len(), 1);
2258 }
2259
2260 #[test]
2261 fn test_builder_error_handler_explicit_overwrite_stays_explicit_mode() {
2262 let first = ErrorHandlerConfig::dead_letter_channel("log:first");
2263 let second = ErrorHandlerConfig::dead_letter_channel("log:second");
2264
2265 let definition = RouteBuilder::from("direct:start")
2266 .route_id("test-route")
2267 .error_handler(first)
2268 .error_handler(second)
2269 .to("mock:out")
2270 .build()
2271 .expect("route should build");
2272
2273 let cfg = definition
2274 .error_handler_config()
2275 .expect("error handler should be set");
2276 assert_eq!(cfg.dlc_uri.as_deref(), Some("log:second"));
2277 }
2278
2279 #[test]
2282 fn test_split_builder_typestate() {
2283 use camel_api::splitter::{SplitterConfig, split_body_lines};
2284
2285 let definition = RouteBuilder::from("timer:test?period=1000")
2287 .route_id("test-route")
2288 .split(SplitterConfig::new(split_body_lines()))
2289 .to("mock:per-fragment")
2290 .end_split()
2291 .to("mock:final")
2292 .build()
2293 .unwrap();
2294
2295 assert_eq!(definition.steps().len(), 2);
2297 }
2298
2299 #[test]
2300 fn test_split_builder_steps_collected() {
2301 use camel_api::splitter::{SplitterConfig, split_body_lines};
2302
2303 let definition = RouteBuilder::from("timer:test?period=1000")
2304 .route_id("test-route")
2305 .split(SplitterConfig::new(split_body_lines()))
2306 .set_header("fragment", Value::String("yes".into()))
2307 .to("mock:per-fragment")
2308 .end_split()
2309 .build()
2310 .unwrap();
2311
2312 assert_eq!(definition.steps().len(), 1);
2314 match &definition.steps()[0] {
2315 BuilderStep::Split { steps, .. } => {
2316 assert_eq!(steps.len(), 2); }
2318 other => panic!("Expected Split, got {:?}", other),
2319 }
2320 }
2321
2322 #[test]
2323 fn test_split_builder_config_propagated() {
2324 use camel_api::splitter::{AggregationStrategy, SplitterConfig, split_body_lines};
2325
2326 let definition = RouteBuilder::from("timer:test?period=1000")
2327 .route_id("test-route")
2328 .split(
2329 SplitterConfig::new(split_body_lines())
2330 .parallel(true)
2331 .parallel_limit(4)
2332 .aggregation(AggregationStrategy::CollectAll),
2333 )
2334 .to("mock:per-fragment")
2335 .end_split()
2336 .build()
2337 .unwrap();
2338
2339 match &definition.steps()[0] {
2340 BuilderStep::Split { config, .. } => {
2341 assert!(config.parallel);
2342 assert_eq!(config.parallel_limit, Some(4));
2343 assert!(matches!(
2344 config.aggregation,
2345 AggregationStrategy::CollectAll
2346 ));
2347 }
2348 other => panic!("Expected Split, got {:?}", other),
2349 }
2350 }
2351
2352 #[test]
2353 fn test_aggregate_builder_adds_step() {
2354 use camel_api::aggregator::AggregatorConfig;
2355 use camel_core::route::BuilderStep;
2356
2357 let definition = RouteBuilder::from("timer:tick")
2358 .route_id("test-route")
2359 .aggregate(
2360 AggregatorConfig::correlate_by("key")
2361 .complete_when_size(2)
2362 .build()
2363 .unwrap(),
2364 )
2365 .build()
2366 .unwrap();
2367
2368 assert_eq!(definition.steps().len(), 1);
2369 assert!(matches!(
2370 definition.steps()[0],
2371 BuilderStep::Aggregate { .. }
2372 ));
2373 }
2374
2375 #[test]
2376 fn test_aggregate_in_split_builder() {
2377 use camel_api::aggregator::AggregatorConfig;
2378 use camel_api::splitter::{SplitterConfig, split_body_lines};
2379 use camel_core::route::BuilderStep;
2380
2381 let definition = RouteBuilder::from("timer:tick")
2382 .route_id("test-route")
2383 .split(SplitterConfig::new(split_body_lines()))
2384 .aggregate(
2385 AggregatorConfig::correlate_by("key")
2386 .complete_when_size(1)
2387 .build()
2388 .unwrap(),
2389 )
2390 .end_split()
2391 .build()
2392 .unwrap();
2393
2394 assert_eq!(definition.steps().len(), 1);
2395 if let BuilderStep::Split { steps, .. } = &definition.steps()[0] {
2396 assert!(matches!(steps[0], BuilderStep::Aggregate { .. }));
2397 } else {
2398 panic!("expected Split step");
2399 }
2400 }
2401
2402 #[test]
2405 fn test_builder_set_body_static_adds_processor() {
2406 let definition = RouteBuilder::from("timer:tick")
2407 .route_id("test-route")
2408 .set_body("fixed")
2409 .build()
2410 .unwrap();
2411 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
2412 }
2413
2414 #[test]
2415 fn test_builder_set_body_fn_adds_processor() {
2416 let definition = RouteBuilder::from("timer:tick")
2417 .route_id("test-route")
2418 .set_body_fn(|_ex: &Exchange| Body::Text("dynamic".into()))
2419 .build()
2420 .unwrap();
2421 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
2422 }
2423
2424 #[test]
2425 fn transform_alias_produces_same_as_set_body() {
2426 let route_transform = RouteBuilder::from("timer:tick")
2427 .route_id("test-route")
2428 .transform("hello")
2429 .build()
2430 .unwrap();
2431
2432 let route_set_body = RouteBuilder::from("timer:tick")
2433 .route_id("test-route")
2434 .set_body("hello")
2435 .build()
2436 .unwrap();
2437
2438 assert_eq!(route_transform.steps().len(), route_set_body.steps().len());
2439 }
2440
2441 #[test]
2442 fn test_builder_set_header_fn_adds_processor() {
2443 let definition = RouteBuilder::from("timer:tick")
2444 .route_id("test-route")
2445 .set_header_fn("k", |_ex: &Exchange| Value::String("v".into()))
2446 .build()
2447 .unwrap();
2448 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
2449 }
2450
2451 #[tokio::test]
2452 async fn test_set_body_static_processor_works() {
2453 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
2454 let def = RouteBuilder::from("t:t")
2455 .route_id("test-route")
2456 .set_body("replaced")
2457 .build()
2458 .unwrap();
2459 let pipeline = compose_pipeline(
2460 def.steps()
2461 .iter()
2462 .filter_map(|s| {
2463 if let BuilderStep::Processor(op) = s {
2464 Some(op.0.clone())
2465 } else {
2466 None
2467 }
2468 })
2469 .map(|p| CompiledStep::Process {
2470 processor: p,
2471 body_contract: None,
2472 lifecycle: None,
2473 })
2474 .collect(),
2475 PipelineRuntimeCtx::compile_time(),
2476 );
2477 let exchange = Exchange::new(Message::new("original"));
2478 let result = pipeline.oneshot(exchange).await.unwrap();
2479 assert_eq!(result.input.body.as_text(), Some("replaced"));
2480 }
2481
2482 #[tokio::test]
2483 async fn test_set_body_fn_processor_works() {
2484 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
2485 let def = RouteBuilder::from("t:t")
2486 .route_id("test-route")
2487 .set_body_fn(|ex: &Exchange| {
2488 Body::Text(ex.input.body.as_text().unwrap_or("").to_uppercase())
2489 })
2490 .build()
2491 .unwrap();
2492 let pipeline = compose_pipeline(
2493 def.steps()
2494 .iter()
2495 .filter_map(|s| {
2496 if let BuilderStep::Processor(op) = s {
2497 Some(op.0.clone())
2498 } else {
2499 None
2500 }
2501 })
2502 .map(|p| CompiledStep::Process {
2503 processor: p,
2504 body_contract: None,
2505 lifecycle: None,
2506 })
2507 .collect(),
2508 PipelineRuntimeCtx::compile_time(),
2509 );
2510 let exchange = Exchange::new(Message::new("hello"));
2511 let result = pipeline.oneshot(exchange).await.unwrap();
2512 assert_eq!(result.input.body.as_text(), Some("HELLO"));
2513 }
2514
2515 #[tokio::test]
2516 async fn test_set_header_fn_processor_works() {
2517 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
2518 let def = RouteBuilder::from("t:t")
2519 .route_id("test-route")
2520 .set_header_fn("echo", |ex: &Exchange| {
2521 ex.input
2522 .body
2523 .as_text()
2524 .map(|t| Value::String(t.into()))
2525 .unwrap_or(Value::Null)
2526 })
2527 .build()
2528 .unwrap();
2529 let pipeline = compose_pipeline(
2530 def.steps()
2531 .iter()
2532 .filter_map(|s| {
2533 if let BuilderStep::Processor(op) = s {
2534 Some(op.0.clone())
2535 } else {
2536 None
2537 }
2538 })
2539 .map(|p| CompiledStep::Process {
2540 processor: p,
2541 body_contract: None,
2542 lifecycle: None,
2543 })
2544 .collect(),
2545 PipelineRuntimeCtx::compile_time(),
2546 );
2547 let exchange = Exchange::new(Message::new("ping"));
2548 let result = pipeline.oneshot(exchange).await.unwrap();
2549 assert_eq!(
2550 result.input.header("echo"),
2551 Some(&Value::String("ping".into()))
2552 );
2553 }
2554
2555 #[test]
2558 fn test_filter_builder_typestate() {
2559 let result = RouteBuilder::from("timer:tick?period=50&repeatCount=1")
2560 .route_id("test-route")
2561 .filter(|_ex| true)
2562 .to("mock:inner")
2563 .end_filter()
2564 .to("mock:outer")
2565 .build();
2566 assert!(result.is_ok());
2567 }
2568
2569 #[test]
2570 fn test_filter_builder_steps_collected() {
2571 let definition = RouteBuilder::from("timer:tick?period=50&repeatCount=1")
2572 .route_id("test-route")
2573 .filter(|_ex| true)
2574 .to("mock:inner")
2575 .end_filter()
2576 .build()
2577 .unwrap();
2578
2579 assert_eq!(definition.steps().len(), 1);
2580 assert!(matches!(&definition.steps()[0], BuilderStep::Filter { .. }));
2581 }
2582
2583 #[test]
2584 fn test_wire_tap_builder_adds_step() {
2585 let definition = RouteBuilder::from("timer:tick")
2586 .route_id("test-route")
2587 .wire_tap("mock:tap")
2588 .to("mock:result")
2589 .build()
2590 .unwrap();
2591
2592 assert_eq!(definition.steps().len(), 2);
2593 assert!(
2594 matches!(&definition.steps()[0], BuilderStep::WireTap { uri } if uri == "mock:tap")
2595 );
2596 assert!(matches!(&definition.steps()[1], BuilderStep::To(uri) if uri == "mock:result"));
2597 }
2598
2599 #[test]
2602 fn test_multicast_builder_typestate() {
2603 let definition = RouteBuilder::from("timer:tick")
2604 .route_id("test-route")
2605 .multicast()
2606 .to("direct:a")
2607 .to("direct:b")
2608 .end_multicast()
2609 .to("mock:result")
2610 .build()
2611 .unwrap();
2612
2613 assert_eq!(definition.steps().len(), 2); }
2615
2616 #[test]
2617 fn test_multicast_builder_steps_collected() {
2618 let definition = RouteBuilder::from("timer:tick")
2619 .route_id("test-route")
2620 .multicast()
2621 .to("direct:a")
2622 .to("direct:b")
2623 .end_multicast()
2624 .build()
2625 .unwrap();
2626
2627 match &definition.steps()[0] {
2628 BuilderStep::Multicast { steps, .. } => {
2629 assert_eq!(steps.len(), 2);
2630 }
2631 other => panic!("Expected Multicast, got {:?}", other),
2632 }
2633 }
2634
2635 #[test]
2638 fn test_builder_concurrent_sets_concurrency() {
2639 use camel_component_api::ConcurrencyModel;
2640
2641 let definition = RouteBuilder::from("http://0.0.0.0:8080/test")
2642 .route_id("test-route")
2643 .concurrent(16)
2644 .to("log:info")
2645 .build()
2646 .unwrap();
2647
2648 assert_eq!(
2649 definition.concurrency_override(),
2650 Some(&ConcurrencyModel::Concurrent { max: Some(16) })
2651 );
2652 }
2653
2654 #[test]
2655 fn test_builder_concurrent_zero_means_unbounded() {
2656 use camel_component_api::ConcurrencyModel;
2657
2658 let definition = RouteBuilder::from("http://0.0.0.0:8080/test")
2659 .route_id("test-route")
2660 .concurrent(0)
2661 .to("log:info")
2662 .build()
2663 .unwrap();
2664
2665 assert_eq!(
2666 definition.concurrency_override(),
2667 Some(&ConcurrencyModel::Concurrent { max: None })
2668 );
2669 }
2670
2671 #[test]
2672 fn test_builder_sequential_sets_concurrency() {
2673 use camel_component_api::ConcurrencyModel;
2674
2675 let definition = RouteBuilder::from("http://0.0.0.0:8080/test")
2676 .route_id("test-route")
2677 .sequential()
2678 .to("log:info")
2679 .build()
2680 .unwrap();
2681
2682 assert_eq!(
2683 definition.concurrency_override(),
2684 Some(&ConcurrencyModel::Sequential)
2685 );
2686 }
2687
2688 #[test]
2689 fn test_builder_default_concurrency_is_none() {
2690 let definition = RouteBuilder::from("timer:tick")
2691 .route_id("test-route")
2692 .to("log:info")
2693 .build()
2694 .unwrap();
2695
2696 assert_eq!(definition.concurrency_override(), None);
2697 }
2698
2699 #[test]
2702 fn test_builder_route_id_sets_id() {
2703 let definition = RouteBuilder::from("timer:tick")
2704 .route_id("my-route")
2705 .build()
2706 .unwrap();
2707
2708 assert_eq!(definition.route_id(), "my-route");
2709 }
2710
2711 #[test]
2712 fn test_build_without_route_id_fails() {
2713 let result = RouteBuilder::from("timer:tick?period=1000")
2714 .to("log:info")
2715 .build();
2716 let err = match result {
2717 Err(e) => e.to_string(),
2718 Ok(_) => panic!("build() should fail without route_id"),
2719 };
2720 assert!(
2721 err.contains("route_id"),
2722 "error should mention route_id, got: {}",
2723 err
2724 );
2725 }
2726
2727 #[test]
2728 fn test_builder_empty_route_id_rejected() {
2729 let result = RouteBuilder::from("timer:tick").route_id("").build();
2730 let err = result.err().expect("empty route_id should be rejected");
2731 assert!(matches!(err, CamelError::RouteError(_)));
2732 }
2733
2734 #[test]
2735 fn test_builder_whitespace_route_id_rejected() {
2736 let result = RouteBuilder::from("timer:tick").route_id(" ").build();
2737 assert!(result.is_err());
2738 }
2739
2740 #[test]
2741 fn test_builder_auto_startup_false() {
2742 let definition = RouteBuilder::from("timer:tick")
2743 .route_id("test-route")
2744 .auto_startup(false)
2745 .build()
2746 .unwrap();
2747
2748 assert!(!definition.auto_startup());
2749 }
2750
2751 #[test]
2752 fn test_builder_startup_order_custom() {
2753 let definition = RouteBuilder::from("timer:tick")
2754 .route_id("test-route")
2755 .startup_order(50)
2756 .build()
2757 .unwrap();
2758
2759 assert_eq!(definition.startup_order(), 50);
2760 }
2761
2762 #[test]
2763 fn test_builder_defaults() {
2764 let definition = RouteBuilder::from("timer:tick")
2765 .route_id("test-route")
2766 .build()
2767 .unwrap();
2768
2769 assert_eq!(definition.route_id(), "test-route");
2770 assert!(definition.auto_startup());
2771 assert_eq!(definition.startup_order(), 1000);
2772 }
2773
2774 #[test]
2777 fn test_choice_builder_single_when() {
2778 let definition = RouteBuilder::from("timer:tick")
2779 .route_id("test-route")
2780 .choice()
2781 .when(|ex: &Exchange| ex.input.header("type").is_some())
2782 .to("mock:typed")
2783 .end_when()
2784 .end_choice()
2785 .build()
2786 .unwrap();
2787 assert_eq!(definition.steps().len(), 1);
2788 assert!(
2789 matches!(&definition.steps()[0], BuilderStep::Choice { whens, otherwise }
2790 if whens.len() == 1 && otherwise.is_none())
2791 );
2792 }
2793
2794 #[test]
2795 fn test_choice_builder_when_otherwise() {
2796 let definition = RouteBuilder::from("timer:tick")
2797 .route_id("test-route")
2798 .choice()
2799 .when(|ex: &Exchange| ex.input.header("a").is_some())
2800 .to("mock:a")
2801 .end_when()
2802 .otherwise()
2803 .to("mock:fallback")
2804 .end_otherwise()
2805 .end_choice()
2806 .build()
2807 .unwrap();
2808 assert!(
2809 matches!(&definition.steps()[0], BuilderStep::Choice { whens, otherwise }
2810 if whens.len() == 1 && otherwise.is_some())
2811 );
2812 }
2813
2814 #[test]
2815 fn test_choice_builder_multiple_whens() {
2816 let definition = RouteBuilder::from("timer:tick")
2817 .route_id("test-route")
2818 .choice()
2819 .when(|ex: &Exchange| ex.input.header("a").is_some())
2820 .to("mock:a")
2821 .end_when()
2822 .when(|ex: &Exchange| ex.input.header("b").is_some())
2823 .to("mock:b")
2824 .end_when()
2825 .end_choice()
2826 .build()
2827 .unwrap();
2828 assert!(
2829 matches!(&definition.steps()[0], BuilderStep::Choice { whens, .. }
2830 if whens.len() == 2)
2831 );
2832 }
2833
2834 #[test]
2835 fn test_choice_step_after_choice() {
2836 let definition = RouteBuilder::from("timer:tick")
2838 .route_id("test-route")
2839 .choice()
2840 .when(|_ex: &Exchange| true)
2841 .to("mock:inner")
2842 .end_when()
2843 .end_choice()
2844 .to("mock:outer") .build()
2846 .unwrap();
2847 assert_eq!(definition.steps().len(), 2);
2848 assert!(matches!(&definition.steps()[1], BuilderStep::To(uri) if uri == "mock:outer"));
2849 }
2850
2851 #[test]
2854 fn test_throttle_builder_typestate() {
2855 let definition = RouteBuilder::from("timer:tick")
2856 .route_id("test-route")
2857 .throttle(10, std::time::Duration::from_secs(1))
2858 .to("mock:result")
2859 .end_throttle()
2860 .build()
2861 .unwrap();
2862
2863 assert_eq!(definition.steps().len(), 1);
2864 assert!(matches!(
2865 &definition.steps()[0],
2866 BuilderStep::Throttle { .. }
2867 ));
2868 }
2869
2870 #[test]
2871 fn test_throttle_builder_with_strategy() {
2872 let definition = RouteBuilder::from("timer:tick")
2873 .route_id("test-route")
2874 .throttle(10, std::time::Duration::from_secs(1))
2875 .strategy(ThrottleStrategy::Reject)
2876 .to("mock:result")
2877 .end_throttle()
2878 .build()
2879 .unwrap();
2880
2881 if let BuilderStep::Throttle { config, .. } = &definition.steps()[0] {
2882 assert_eq!(config.strategy, ThrottleStrategy::Reject);
2883 } else {
2884 panic!("Expected Throttle step");
2885 }
2886 }
2887
2888 #[test]
2889 fn test_throttle_builder_steps_collected() {
2890 let definition = RouteBuilder::from("timer:tick")
2891 .route_id("test-route")
2892 .throttle(5, std::time::Duration::from_secs(1))
2893 .set_header("throttled", Value::Bool(true))
2894 .to("mock:throttled")
2895 .end_throttle()
2896 .build()
2897 .unwrap();
2898
2899 match &definition.steps()[0] {
2900 BuilderStep::Throttle { steps, .. } => {
2901 assert_eq!(steps.len(), 2); }
2903 other => panic!("Expected Throttle, got {:?}", other),
2904 }
2905 }
2906
2907 #[test]
2908 fn test_throttle_step_after_throttle() {
2909 let definition = RouteBuilder::from("timer:tick")
2911 .route_id("test-route")
2912 .throttle(10, std::time::Duration::from_secs(1))
2913 .to("mock:inner")
2914 .end_throttle()
2915 .to("mock:outer")
2916 .build()
2917 .unwrap();
2918
2919 assert_eq!(definition.steps().len(), 2);
2920 assert!(matches!(&definition.steps()[1], BuilderStep::To(uri) if uri == "mock:outer"));
2921 }
2922
2923 #[test]
2926 fn test_load_balance_builder_typestate() {
2927 let definition = RouteBuilder::from("timer:tick")
2928 .route_id("test-route")
2929 .load_balance()
2930 .round_robin()
2931 .to("mock:a")
2932 .to("mock:b")
2933 .end_load_balance()
2934 .build()
2935 .unwrap();
2936
2937 assert_eq!(definition.steps().len(), 1);
2938 assert!(matches!(
2939 &definition.steps()[0],
2940 BuilderStep::LoadBalance { .. }
2941 ));
2942 }
2943
2944 #[test]
2945 fn test_load_balance_builder_with_strategy() {
2946 let definition = RouteBuilder::from("timer:tick")
2947 .route_id("test-route")
2948 .load_balance()
2949 .random()
2950 .to("mock:result")
2951 .end_load_balance()
2952 .build()
2953 .unwrap();
2954
2955 if let BuilderStep::LoadBalance { config, .. } = &definition.steps()[0] {
2956 assert_eq!(config.strategy, LoadBalanceStrategy::Random);
2957 } else {
2958 panic!("Expected LoadBalance step");
2959 }
2960 }
2961
2962 #[test]
2963 fn test_load_balance_builder_steps_collected() {
2964 let definition = RouteBuilder::from("timer:tick")
2965 .route_id("test-route")
2966 .load_balance()
2967 .set_header("lb", Value::Bool(true))
2968 .to("mock:a")
2969 .end_load_balance()
2970 .build()
2971 .unwrap();
2972
2973 match &definition.steps()[0] {
2974 BuilderStep::LoadBalance { steps, .. } => {
2975 assert_eq!(steps.len(), 2); }
2977 other => panic!("Expected LoadBalance, got {:?}", other),
2978 }
2979 }
2980
2981 #[test]
2982 fn test_load_balance_step_after_load_balance() {
2983 let definition = RouteBuilder::from("timer:tick")
2985 .route_id("test-route")
2986 .load_balance()
2987 .to("mock:inner")
2988 .end_load_balance()
2989 .to("mock:outer")
2990 .build()
2991 .unwrap();
2992
2993 assert_eq!(definition.steps().len(), 2);
2994 assert!(matches!(&definition.steps()[1], BuilderStep::To(uri) if uri == "mock:outer"));
2995 }
2996
2997 #[test]
3000 fn test_dynamic_router_builder() {
3001 let definition = RouteBuilder::from("timer:tick")
3002 .route_id("test-route")
3003 .dynamic_router(Arc::new(|_| Some("mock:result".to_string())))
3004 .build()
3005 .unwrap();
3006
3007 assert_eq!(definition.steps().len(), 1);
3008 assert!(matches!(
3009 &definition.steps()[0],
3010 BuilderStep::DynamicRouter { .. }
3011 ));
3012 }
3013
3014 #[test]
3015 fn test_dynamic_router_builder_with_config() {
3016 let config = DynamicRouterConfig::new(Arc::new(|_| Some("mock:a".to_string())))
3017 .max_iterations(100)
3018 .cache_size(500);
3019
3020 let definition = RouteBuilder::from("timer:tick")
3021 .route_id("test-route")
3022 .dynamic_router_with_config(config)
3023 .build()
3024 .unwrap();
3025
3026 assert_eq!(definition.steps().len(), 1);
3027 if let BuilderStep::DynamicRouter { config } = &definition.steps()[0] {
3028 assert_eq!(config.max_iterations, 100);
3029 assert_eq!(config.cache_size, 500);
3030 } else {
3031 panic!("Expected DynamicRouter step");
3032 }
3033 }
3034
3035 #[test]
3036 fn test_dynamic_router_step_after_router() {
3037 let definition = RouteBuilder::from("timer:tick")
3039 .route_id("test-route")
3040 .dynamic_router(Arc::new(|_| Some("mock:inner".to_string())))
3041 .to("mock:outer")
3042 .build()
3043 .unwrap();
3044
3045 assert_eq!(definition.steps().len(), 2);
3046 assert!(matches!(
3047 &definition.steps()[0],
3048 BuilderStep::DynamicRouter { .. }
3049 ));
3050 assert!(matches!(&definition.steps()[1], BuilderStep::To(uri) if uri == "mock:outer"));
3051 }
3052
3053 #[test]
3054 fn routing_slip_builder_creates_step() {
3055 use camel_api::RoutingSlipExpression;
3056
3057 let expression: RoutingSlipExpression = Arc::new(|_| Some("direct:a,direct:b".to_string()));
3058
3059 let route = RouteBuilder::from("direct:start")
3060 .route_id("routing-slip-test")
3061 .routing_slip(expression)
3062 .build()
3063 .unwrap();
3064
3065 assert!(
3066 matches!(route.steps()[0], BuilderStep::RoutingSlip { .. }),
3067 "Expected RoutingSlip step"
3068 );
3069 }
3070
3071 #[test]
3072 fn routing_slip_with_config_builder_creates_step() {
3073 use camel_api::RoutingSlipConfig;
3074
3075 let config = RoutingSlipConfig::new(Arc::new(|_| Some("mock:a".to_string())))
3076 .uri_delimiter("|")
3077 .cache_size(50)
3078 .ignore_invalid_endpoints(true);
3079
3080 let route = RouteBuilder::from("direct:start")
3081 .route_id("routing-slip-config-test")
3082 .routing_slip_with_config(config)
3083 .build()
3084 .unwrap();
3085
3086 if let BuilderStep::RoutingSlip { config } = &route.steps()[0] {
3087 assert_eq!(config.uri_delimiter, "|");
3088 assert_eq!(config.cache_size, 50);
3089 assert!(config.ignore_invalid_endpoints);
3090 } else {
3091 panic!("Expected RoutingSlip step");
3092 }
3093 }
3094
3095 #[test]
3096 fn test_builder_marshal_adds_processor_step() {
3097 let definition = RouteBuilder::from("timer:tick")
3098 .route_id("test-route")
3099 .marshal("json")
3100 .unwrap()
3101 .build()
3102 .unwrap();
3103 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
3104 }
3105
3106 #[test]
3107 fn test_builder_unmarshal_adds_processor_step() {
3108 let definition = RouteBuilder::from("timer:tick")
3109 .route_id("test-route")
3110 .unmarshal("json")
3111 .unwrap()
3112 .build()
3113 .unwrap();
3114 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
3115 }
3116
3117 #[test]
3118 fn test_builder_stream_cache_adds_processor_step() {
3119 let definition = RouteBuilder::from("timer:tick")
3120 .route_id("test-route")
3121 .stream_cache(1024)
3122 .build()
3123 .unwrap();
3124 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
3125 }
3126
3127 #[test]
3128 fn validate_adds_validate_step() {
3129 let def = RouteBuilder::from("direct:in")
3130 .route_id("test")
3131 .validate("schemas/order.xsd")
3132 .build()
3133 .unwrap();
3134 let steps = def.steps();
3135 assert_eq!(steps.len(), 1);
3136 assert!(
3137 matches!(&steps[0], BuilderStep::Validate { predicate } if predicate.language == "simple" && predicate.source == "schemas/order.xsd"),
3138 "got: {:?}",
3139 steps[0]
3140 );
3141 }
3142
3143 #[test]
3144 fn test_builder_marshal_returns_err_for_unknown_format() {
3145 let result = RouteBuilder::from("timer:tick")
3146 .route_id("test-route")
3147 .marshal("protobuf");
3148 let err = match result {
3149 Err(e) => e,
3150 Ok(_) => panic!("marshal with unknown format should return Err"),
3151 };
3152 let msg = err.to_string();
3153 assert!(
3154 msg.contains("unknown data format"),
3155 "error should mention unknown format, got: {msg}"
3156 );
3157 assert!(
3158 msg.contains("protobuf"),
3159 "error should mention format name, got: {msg}"
3160 );
3161 }
3162
3163 #[test]
3164 fn test_builder_unmarshal_returns_err_for_unknown_format() {
3165 let result = RouteBuilder::from("timer:tick")
3166 .route_id("test-route")
3167 .unmarshal("protobuf");
3168 let err = match result {
3169 Err(e) => e,
3170 Ok(_) => panic!("unmarshal with unknown format should return Err"),
3171 };
3172 let msg = err.to_string();
3173 assert!(
3174 msg.contains("unknown data format"),
3175 "error should mention unknown format, got: {msg}"
3176 );
3177 assert!(
3178 msg.contains("protobuf"),
3179 "error should mention format name, got: {msg}"
3180 );
3181 }
3182
3183 #[test]
3184 fn test_builder_recipient_list_creates_step() {
3185 let route = RouteBuilder::from("direct:start")
3186 .route_id("recipient-list-test")
3187 .recipient_list(Arc::new(|_| "direct:a,direct:b".to_string()))
3188 .build()
3189 .unwrap();
3190
3191 assert!(matches!(
3192 &route.steps()[0],
3193 BuilderStep::RecipientList { .. }
3194 ));
3195 }
3196
3197 #[test]
3198 fn test_builder_recipient_list_with_config_creates_step() {
3199 let config = RecipientListConfig::new(Arc::new(|_| "mock:a".to_string()));
3200
3201 let route = RouteBuilder::from("direct:start")
3202 .route_id("recipient-list-config-test")
3203 .recipient_list_with_config(config)
3204 .build()
3205 .unwrap();
3206
3207 assert!(matches!(
3208 &route.steps()[0],
3209 BuilderStep::RecipientList { .. }
3210 ));
3211 }
3212
3213 #[test]
3214 fn test_builder_script_adds_script_step() {
3215 let route = RouteBuilder::from("direct:start")
3216 .route_id("script-test")
3217 .script("rhai", "headers[\"x\"] = \"y\"")
3218 .build()
3219 .unwrap();
3220
3221 assert!(matches!(
3222 &route.steps()[0],
3223 BuilderStep::Script { language, script }
3224 if language == "rhai" && script == "headers[\"x\"] = \"y\""
3225 ));
3226 }
3227
3228 #[test]
3229 fn test_builder_delay_and_delay_with_header_add_steps() {
3230 let route = RouteBuilder::from("direct:start")
3231 .route_id("delay-test")
3232 .delay(Duration::from_millis(250))
3233 .delay_with_header(Duration::from_millis(500), "x-delay")
3234 .build()
3235 .unwrap();
3236
3237 assert_eq!(route.steps().len(), 2);
3238 assert!(matches!(&route.steps()[0], BuilderStep::Delay { .. }));
3239 assert!(matches!(&route.steps()[1], BuilderStep::Delay { .. }));
3240 }
3241
3242 #[test]
3243 fn test_builder_log_and_stop_add_steps_in_order() {
3244 let route = RouteBuilder::from("direct:start")
3245 .route_id("log-stop-test")
3246 .log("hello", LogLevel::Info)
3247 .stop()
3248 .to("mock:after")
3249 .build()
3250 .unwrap();
3251
3252 assert_eq!(route.steps().len(), 3);
3253 assert!(matches!(
3254 &route.steps()[0],
3255 BuilderStep::Log { message, .. } if message == "hello"
3256 ));
3257 assert!(matches!(&route.steps()[1], BuilderStep::Stop));
3258 assert!(matches!(&route.steps()[2], BuilderStep::To(uri) if uri == "mock:after"));
3259 }
3260
3261 #[test]
3262 fn test_builder_stream_cache_default_adds_processor_step() {
3263 let route = RouteBuilder::from("direct:start")
3264 .route_id("stream-cache-default-test")
3265 .stream_cache_default()
3266 .build()
3267 .unwrap();
3268
3269 assert!(matches!(&route.steps()[0], BuilderStep::Processor(_)));
3270 }
3271
3272 #[test]
3273 fn test_validate_creates_validate_step_with_expression() {
3274 let route = RouteBuilder::from("direct:in")
3275 .route_id("validate-prefix-test")
3276 .validate("${body.size()} > 0")
3277 .build()
3278 .unwrap();
3279
3280 assert!(matches!(
3281 &route.steps()[0],
3282 BuilderStep::Validate { predicate } if predicate.language == "simple" && predicate.source == "${body.size()} > 0"
3283 ));
3284 }
3285
3286 #[test]
3287 fn test_load_balance_builder_weighted_failover_config() {
3288 let route = RouteBuilder::from("direct:start")
3289 .route_id("lb-weighted-failover")
3290 .load_balance()
3291 .weighted(vec![
3292 ("direct:a".to_string(), 3),
3293 ("direct:b".to_string(), 1),
3294 ])
3295 .failover()
3296 .to("mock:result")
3297 .end_load_balance()
3298 .build()
3299 .unwrap();
3300
3301 if let BuilderStep::LoadBalance { config, .. } = &route.steps()[0] {
3302 assert_eq!(config.strategy, LoadBalanceStrategy::Failover);
3303 } else {
3304 panic!("Expected LoadBalance step");
3305 }
3306 }
3307
3308 #[test]
3309 fn test_multicast_builder_all_config_setters() {
3310 let route = RouteBuilder::from("direct:start")
3311 .route_id("multicast-config-test")
3312 .multicast()
3313 .parallel(true)
3314 .parallel_limit(4)
3315 .stop_on_exception(true)
3316 .timeout(Duration::from_millis(300))
3317 .aggregation(MulticastStrategy::Original)
3318 .to("mock:a")
3319 .end_multicast()
3320 .build()
3321 .unwrap();
3322
3323 if let BuilderStep::Multicast { config, .. } = &route.steps()[0] {
3324 assert!(config.parallel);
3325 assert_eq!(config.parallel_limit, Some(4));
3326 assert!(config.stop_on_exception);
3327 assert_eq!(config.timeout, Some(Duration::from_millis(300)));
3328 assert!(matches!(config.aggregation, MulticastStrategy::Original));
3329 } else {
3330 panic!("Expected Multicast step");
3331 }
3332 }
3333
3334 #[test]
3335 fn test_build_canonical_rejects_unsupported_processor_step() {
3336 let err = RouteBuilder::from("direct:start")
3337 .route_id("canonical-reject")
3338 .set_header("k", Value::String("v".into()))
3339 .build_canonical()
3340 .unwrap_err();
3341
3342 assert!(format!("{err}").contains("does not support step `processor`"));
3343 }
3344
3345 #[test]
3348 fn test_load_balance_builder_weighted_strategy() {
3349 let route = RouteBuilder::from("direct:start")
3350 .route_id("lb-weighted")
3351 .load_balance()
3352 .weighted(vec![
3353 ("direct:a".to_string(), 5),
3354 ("direct:b".to_string(), 2),
3355 ("direct:c".to_string(), 1),
3356 ])
3357 .to("mock:result")
3358 .end_load_balance()
3359 .build()
3360 .unwrap();
3361
3362 if let BuilderStep::LoadBalance { config, .. } = &route.steps()[0] {
3363 assert!(matches!(config.strategy, LoadBalanceStrategy::Weighted(_)));
3364 } else {
3365 panic!("Expected LoadBalance step");
3366 }
3367 }
3368
3369 #[test]
3370 fn test_load_balance_builder_failover_strategy() {
3371 let route = RouteBuilder::from("direct:start")
3372 .route_id("lb-failover")
3373 .load_balance()
3374 .failover()
3375 .to("mock:primary")
3376 .end_load_balance()
3377 .build()
3378 .unwrap();
3379
3380 if let BuilderStep::LoadBalance { config, .. } = &route.steps()[0] {
3381 assert_eq!(config.strategy, LoadBalanceStrategy::Failover);
3382 } else {
3383 panic!("Expected LoadBalance step");
3384 }
3385 }
3386
3387 #[test]
3390 fn test_filter_in_split_builder_typestate() {
3391 use camel_api::splitter::{SplitterConfig, split_body_lines};
3392
3393 let definition = RouteBuilder::from("timer:test")
3394 .route_id("filter-in-split")
3395 .split(SplitterConfig::new(split_body_lines()))
3396 .filter(|_ex| true)
3397 .to("mock:filtered")
3398 .end_filter()
3399 .end_split()
3400 .build()
3401 .unwrap();
3402
3403 assert_eq!(definition.steps().len(), 1);
3404 if let BuilderStep::Split { steps, .. } = &definition.steps()[0] {
3405 assert_eq!(steps.len(), 1);
3406 assert!(matches!(&steps[0], BuilderStep::Filter { .. }));
3407 } else {
3408 panic!("Expected Split step");
3409 }
3410 }
3411
3412 #[test]
3413 fn test_filter_in_split_builder_multiple_steps() {
3414 use camel_api::splitter::{SplitterConfig, split_body_lines};
3415
3416 let definition = RouteBuilder::from("timer:test")
3417 .route_id("filter-in-split-multi")
3418 .split(SplitterConfig::new(split_body_lines()))
3419 .to("mock:before-filter")
3420 .filter(|_ex| true)
3421 .to("mock:inside-filter")
3422 .end_filter()
3423 .to("mock:after-filter")
3424 .end_split()
3425 .build()
3426 .unwrap();
3427
3428 if let BuilderStep::Split { steps, .. } = &definition.steps()[0] {
3429 assert_eq!(steps.len(), 3);
3431 } else {
3432 panic!("Expected Split step");
3433 }
3434 }
3435
3436 #[test]
3439 fn test_build_canonical_with_circuit_breaker() {
3440 use camel_api::circuit_breaker::CircuitBreakerConfig;
3441
3442 let spec = RouteBuilder::from("direct:start")
3443 .route_id("canonical-cb")
3444 .circuit_breaker(CircuitBreakerConfig::new().failure_threshold(10))
3445 .to("mock:result")
3446 .build_canonical()
3447 .unwrap();
3448
3449 let cb = spec.circuit_breaker.expect("circuit breaker should be set");
3450 assert_eq!(cb.failure_threshold, 10);
3451 }
3452
3453 #[test]
3454 fn test_build_canonical_rejects_custom_split_aggregation() {
3455 use camel_api::splitter::{SplitterConfig, split_body_lines};
3456
3457 let err = RouteBuilder::from("direct:start")
3458 .route_id("canonical-custom-split")
3459 .split(SplitterConfig::new(split_body_lines()).aggregation(
3460 camel_api::splitter::AggregationStrategy::Custom(Arc::new(|_, ex| ex)),
3461 ))
3462 .to("mock:frag")
3463 .end_split()
3464 .build_canonical()
3465 .unwrap_err();
3466
3467 assert!(format!("{err}").contains("canonical v2 does not support step `split`"));
3469 }
3470
3471 #[test]
3472 fn test_build_canonical_rejects_custom_aggregate_strategy() {
3473 let err = RouteBuilder::from("direct:start")
3474 .route_id("canonical-custom-agg")
3475 .aggregate(
3476 AggregatorConfig::correlate_by("key")
3477 .complete_when_size(2)
3478 .strategy(AggregationStrategy::Custom(Arc::new(|_, ex| ex)))
3479 .build()
3480 .unwrap(),
3481 )
3482 .build_canonical()
3483 .unwrap_err();
3484
3485 assert!(format!("{err}").contains("custom aggregate strategy"));
3486 }
3487
3488 #[test]
3489 fn test_build_canonical_rejects_fn_correlation_strategy() {
3490 let err = RouteBuilder::from("direct:start")
3491 .route_id("canonical-fn-corr")
3492 .aggregate(AggregatorConfig {
3493 header_name: "key".to_string(),
3494 completion: CompletionMode::Single(CompletionCondition::Size(1)),
3495 correlation: CorrelationStrategy::Fn(Arc::new(|_| Some("key".to_string()))),
3496 strategy: AggregationStrategy::CollectAll,
3497 max_buckets: None,
3498 bucket_ttl: None,
3499 force_completion_on_stop: false,
3500 discard_on_timeout: false,
3501 max_timeout_tasks: 1024,
3502 })
3503 .build_canonical()
3504 .unwrap_err();
3505
3506 assert!(format!("{err}").contains("Fn correlation strategy"));
3507 }
3508
3509 #[test]
3510 fn test_build_canonical_rejects_predicate_completion() {
3511 let err = RouteBuilder::from("direct:start")
3512 .route_id("canonical-pred-completion")
3513 .aggregate(AggregatorConfig {
3514 header_name: "key".to_string(),
3515 completion: CompletionMode::Single(CompletionCondition::Predicate(Arc::new(
3516 |_| false,
3517 ))),
3518 correlation: CorrelationStrategy::HeaderName("key".to_string()),
3519 strategy: AggregationStrategy::CollectAll,
3520 max_buckets: None,
3521 bucket_ttl: None,
3522 force_completion_on_stop: false,
3523 discard_on_timeout: false,
3524 max_timeout_tasks: 1024,
3525 })
3526 .build_canonical()
3527 .unwrap_err();
3528
3529 assert!(
3530 format!("{err}").contains("cannot reverse-map"),
3531 "reject message must explain forward-only: {}",
3532 err
3533 );
3534 }
3535
3536 #[test]
3537 fn extract_completion_fields_rejects_predicate_expr() {
3538 let mode = CompletionMode::Single(CompletionCondition::PredicateExpr {
3539 expr: "${body} == 'DONE'".to_string(),
3540 language: "simple".to_string(),
3541 });
3542 let result = extract_completion_fields(&mode);
3543 assert!(
3544 result.is_err(),
3545 "PredicateExpr must be rejected (forward-only)"
3546 );
3547 let msg = format!("{}", result.unwrap_err());
3548 assert!(
3549 msg.contains("cannot reverse-map"),
3550 "reject message must explain forward-only: {}",
3551 msg
3552 );
3553 }
3554
3555 #[test]
3556 fn extract_completion_fields_rejects_predicate_expr_any_mode() {
3557 let mode = CompletionMode::Any(vec![
3558 CompletionCondition::Size(5),
3559 CompletionCondition::PredicateExpr {
3560 expr: "${body} == 'DONE'".to_string(),
3561 language: "simple".to_string(),
3562 },
3563 ]);
3564 let result = extract_completion_fields(&mode);
3565 assert!(
3566 result.is_err(),
3567 "PredicateExpr in Any must be rejected (forward-only)"
3568 );
3569 let msg = format!("{}", result.unwrap_err());
3570 assert!(
3571 msg.contains("cannot reverse-map"),
3572 "reject message must explain forward-only: {}",
3573 msg
3574 );
3575 }
3576
3577 #[test]
3578 fn test_build_canonical_with_expression_correlation() {
3579 let spec = RouteBuilder::from("direct:start")
3580 .route_id("canonical-expr-corr")
3581 .aggregate(AggregatorConfig {
3582 header_name: "key".to_string(),
3583 completion: CompletionMode::Single(CompletionCondition::Size(1)),
3584 correlation: CorrelationStrategy::Expression {
3585 expr: "header.key".to_string(),
3586 language: "simple".to_string(),
3587 },
3588 strategy: AggregationStrategy::CollectAll,
3589 max_buckets: None,
3590 bucket_ttl: None,
3591 force_completion_on_stop: false,
3592 discard_on_timeout: false,
3593 max_timeout_tasks: 1024,
3594 })
3595 .build_canonical()
3596 .unwrap();
3597
3598 assert!(spec.steps.iter().any(|s| matches!(s, CanonicalStepSpec::Aggregate(a) if a.correlation_key == Some("header.key".to_string()))));
3599 }
3600
3601 #[test]
3602 fn test_build_canonical_split_rejected_with_closure_expression() {
3603 use camel_api::splitter::{AggregationStrategy, SplitterConfig, split_body_lines};
3604
3605 let err = RouteBuilder::from("direct:start")
3607 .route_id("canonical-split-last")
3608 .split(
3609 SplitterConfig::new(split_body_lines()).aggregation(AggregationStrategy::LastWins),
3610 )
3611 .to("mock:frag")
3612 .end_split()
3613 .build_canonical()
3614 .unwrap_err();
3615
3616 assert!(format!("{err}").contains("canonical v2 does not support step `split`"));
3617 }
3618
3619 #[test]
3622 fn test_on_exception_full_chain_retry_backoff_jitter_handled_by() {
3623 let definition = RouteBuilder::from("direct:start")
3624 .route_id("on-exception-full")
3625 .dead_letter_channel("log:dlc")
3626 .on_exception(|e| matches!(e, CamelError::Io(_)))
3627 .retry(5)
3628 .with_backoff(Duration::from_millis(10), 2.0, Duration::from_millis(500))
3629 .with_jitter(0.3)
3630 .handled_by("log:io-handler")
3631 .end_on_exception()
3632 .to("mock:out")
3633 .build()
3634 .unwrap();
3635
3636 let cfg = definition
3637 .error_handler_config()
3638 .expect("error handler should be set");
3639 assert_eq!(cfg.policies.len(), 1);
3640 let policy = &cfg.policies[0];
3641 let retry = policy.retry.as_ref().expect("retry should be set");
3642 assert_eq!(retry.max_attempts, 5);
3643 assert_eq!(retry.initial_delay, Duration::from_millis(10));
3644 assert_eq!(retry.multiplier, 2.0);
3645 assert_eq!(retry.max_delay, Duration::from_millis(500));
3646 assert!((retry.jitter_factor - 0.3).abs() < f64::EPSILON);
3647 assert_eq!(policy.handled_by.as_deref(), Some("log:io-handler"));
3648 }
3649
3650 #[test]
3651 fn test_on_exception_jitter_clamped_to_valid_range() {
3652 let definition = RouteBuilder::from("direct:start")
3653 .route_id("jitter-clamp")
3654 .on_exception(|_e| true)
3655 .retry(1)
3656 .with_jitter(5.0)
3657 .end_on_exception()
3658 .to("mock:out")
3659 .build()
3660 .unwrap();
3661
3662 let cfg = definition.error_handler_config().unwrap();
3663 let retry = cfg.policies[0].retry.as_ref().unwrap();
3664 assert!((retry.jitter_factor - 1.0).abs() < f64::EPSILON);
3665 }
3666
3667 #[test]
3670 fn test_builder_process_fn_adds_processor_step() {
3671 use camel_api::BoxProcessorExt;
3672 let processor = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
3673 let definition = RouteBuilder::from("timer:tick")
3674 .route_id("process-fn-test")
3675 .process_fn(processor)
3676 .build()
3677 .unwrap();
3678
3679 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
3680 }
3681
3682 #[test]
3683 fn test_builder_convert_body_to_adds_processor_step() {
3684 let definition = RouteBuilder::from("timer:tick")
3685 .route_id("convert-body-test")
3686 .convert_body_to(BodyType::Json)
3687 .build()
3688 .unwrap();
3689
3690 assert!(matches!(&definition.steps()[0], BuilderStep::Processor(_)));
3691 }
3692
3693 #[test]
3694 fn test_builder_bean_adds_bean_step() {
3695 let definition = RouteBuilder::from("timer:tick")
3696 .route_id("bean-test")
3697 .bean("myBean", "process")
3698 .build()
3699 .unwrap();
3700
3701 assert!(
3702 matches!(&definition.steps()[0], BuilderStep::Bean { name, method }
3703 if name == "myBean" && method == "process")
3704 );
3705 }
3706
3707 #[test]
3710 fn test_throttle_builder_delay_strategy() {
3711 let definition = RouteBuilder::from("timer:tick")
3712 .route_id("throttle-delay")
3713 .throttle(10, Duration::from_secs(1))
3714 .strategy(ThrottleStrategy::Delay)
3715 .to("mock:result")
3716 .end_throttle()
3717 .build()
3718 .unwrap();
3719
3720 if let BuilderStep::Throttle { config, .. } = &definition.steps()[0] {
3721 assert_eq!(config.strategy, ThrottleStrategy::Delay);
3722 } else {
3723 panic!("Expected Throttle step");
3724 }
3725 }
3726
3727 #[test]
3728 fn test_throttle_builder_drop_strategy() {
3729 let definition = RouteBuilder::from("timer:tick")
3730 .route_id("throttle-drop")
3731 .throttle(10, Duration::from_secs(1))
3732 .strategy(ThrottleStrategy::Drop)
3733 .to("mock:result")
3734 .end_throttle()
3735 .build()
3736 .unwrap();
3737
3738 if let BuilderStep::Throttle { config, .. } = &definition.steps()[0] {
3739 assert_eq!(config.strategy, ThrottleStrategy::Drop);
3740 } else {
3741 panic!("Expected Throttle step");
3742 }
3743 }
3744
3745 #[test]
3748 fn test_nested_loop_while_builder() {
3749 use camel_api::loop_eip::LoopMode;
3750
3751 let def = RouteBuilder::from("direct:start")
3752 .route_id("nested-loop-while")
3753 .loop_count(2)
3754 .to("mock:outer")
3755 .loop_while(|_ex| true)
3756 .to("mock:inner")
3757 .end_loop()
3758 .end_loop()
3759 .build()
3760 .unwrap();
3761
3762 assert_eq!(def.steps().len(), 1);
3763 if let BuilderStep::Loop { steps, .. } = &def.steps()[0] {
3764 assert_eq!(steps.len(), 2);
3765 if let BuilderStep::Loop { config, .. } = &steps[1] {
3766 assert!(matches!(config.mode, LoopMode::While(_)));
3767 } else {
3768 panic!("Expected inner Loop step");
3769 }
3770 } else {
3771 panic!("Expected outer Loop step");
3772 }
3773 }
3774
3775 #[test]
3778 fn test_choice_builder_multiple_whens_with_otherwise() {
3779 let definition = RouteBuilder::from("timer:tick")
3780 .route_id("choice-multi-otherwise")
3781 .choice()
3782 .when(|ex: &Exchange| ex.input.header("a").is_some())
3783 .to("mock:a")
3784 .end_when()
3785 .when(|ex: &Exchange| ex.input.header("b").is_some())
3786 .to("mock:b")
3787 .end_when()
3788 .when(|ex: &Exchange| ex.input.header("c").is_some())
3789 .to("mock:c")
3790 .end_when()
3791 .otherwise()
3792 .to("mock:fallback")
3793 .end_otherwise()
3794 .end_choice()
3795 .build()
3796 .unwrap();
3797
3798 if let BuilderStep::Choice { whens, otherwise } = &definition.steps()[0] {
3799 assert_eq!(whens.len(), 3);
3800 assert!(otherwise.is_some());
3801 assert_eq!(otherwise.as_ref().unwrap().len(), 1);
3802 } else {
3803 panic!("Expected Choice step");
3804 }
3805 }
3806
3807 #[test]
3810 fn test_multicast_builder_parallel_only() {
3811 let route = RouteBuilder::from("direct:start")
3812 .route_id("multicast-parallel")
3813 .multicast()
3814 .parallel(true)
3815 .to("mock:a")
3816 .end_multicast()
3817 .build()
3818 .unwrap();
3819
3820 if let BuilderStep::Multicast { config, .. } = &route.steps()[0] {
3821 assert!(config.parallel);
3822 assert_eq!(config.parallel_limit, None);
3823 } else {
3824 panic!("Expected Multicast step");
3825 }
3826 }
3827
3828 #[test]
3829 fn test_multicast_builder_timeout_only() {
3830 let route = RouteBuilder::from("direct:start")
3831 .route_id("multicast-timeout")
3832 .multicast()
3833 .timeout(Duration::from_secs(5))
3834 .to("mock:a")
3835 .end_multicast()
3836 .build()
3837 .unwrap();
3838
3839 if let BuilderStep::Multicast { config, .. } = &route.steps()[0] {
3840 assert_eq!(config.timeout, Some(Duration::from_secs(5)));
3841 } else {
3842 panic!("Expected Multicast step");
3843 }
3844 }
3845
3846 #[test]
3847 fn test_multicast_builder_aggregation_collect_all() {
3848 let route = RouteBuilder::from("direct:start")
3849 .route_id("multicast-collect")
3850 .multicast()
3851 .aggregation(MulticastStrategy::CollectAll)
3852 .to("mock:a")
3853 .end_multicast()
3854 .build()
3855 .unwrap();
3856
3857 if let BuilderStep::Multicast { config, .. } = &route.steps()[0] {
3858 assert!(matches!(config.aggregation, MulticastStrategy::CollectAll));
3859 } else {
3860 panic!("Expected Multicast step");
3861 }
3862 }
3863
3864 #[test]
3867 fn test_build_canonical_aggregate_any_completion_mode() {
3868 let spec = RouteBuilder::from("direct:start")
3869 .route_id("canonical-any-completion")
3870 .aggregate(
3871 AggregatorConfig::correlate_by("key")
3872 .complete_on_size_or_timeout(10, Duration::from_secs(30))
3873 .build()
3874 .unwrap(),
3875 )
3876 .build_canonical()
3877 .unwrap();
3878
3879 if let CanonicalStepSpec::Aggregate(agg) = &spec.steps[0] {
3880 assert_eq!(agg.completion_size, Some(10));
3881 assert_eq!(agg.completion_timeout_ms, Some(30_000));
3882 } else {
3883 panic!("Expected Aggregate step");
3884 }
3885 }
3886
3887 #[test]
3888 fn test_build_canonical_aggregate_timeout_completion() {
3889 let spec = RouteBuilder::from("direct:start")
3890 .route_id("canonical-timeout-completion")
3891 .aggregate(
3892 AggregatorConfig::correlate_by("key")
3893 .complete_on_timeout(Duration::from_millis(500))
3894 .build()
3895 .unwrap(),
3896 )
3897 .build_canonical()
3898 .unwrap();
3899
3900 if let CanonicalStepSpec::Aggregate(agg) = &spec.steps[0] {
3901 assert_eq!(agg.completion_size, None);
3902 assert_eq!(agg.completion_timeout_ms, Some(500));
3903 } else {
3904 panic!("Expected Aggregate step");
3905 }
3906 }
3907
3908 #[test]
3911 fn test_build_canonical_aggregate_discard_on_timeout() {
3912 use camel_api::aggregator::AggregatorConfig;
3913
3914 let spec = RouteBuilder::from("direct:start")
3915 .route_id("canonical-discard-timeout")
3916 .aggregate(
3917 AggregatorConfig::correlate_by("key")
3918 .complete_when_size(1)
3919 .discard_on_timeout(true)
3920 .build()
3921 .unwrap(),
3922 )
3923 .build_canonical()
3924 .unwrap();
3925
3926 if let CanonicalStepSpec::Aggregate(agg) = &spec.steps[0] {
3927 assert_eq!(agg.discard_on_timeout, Some(true));
3928 } else {
3929 panic!("Expected Aggregate step");
3930 }
3931 }
3932
3933 #[test]
3934 fn test_build_canonical_aggregate_force_completion_on_stop() {
3935 use camel_api::aggregator::AggregatorConfig;
3936
3937 let spec = RouteBuilder::from("direct:start")
3938 .route_id("canonical-force-stop")
3939 .aggregate(
3940 AggregatorConfig::correlate_by("key")
3941 .complete_when_size(1)
3942 .force_completion_on_stop(true)
3943 .build()
3944 .unwrap(),
3945 )
3946 .build_canonical()
3947 .unwrap();
3948
3949 if let CanonicalStepSpec::Aggregate(agg) = &spec.steps[0] {
3950 assert_eq!(agg.force_completion_on_stop, Some(true));
3951 } else {
3952 panic!("Expected Aggregate step");
3953 }
3954 }
3955
3956 #[test]
3959 fn test_build_canonical_aggregate_max_buckets_and_ttl() {
3960 use camel_api::aggregator::AggregatorConfig;
3961
3962 let spec = RouteBuilder::from("direct:start")
3963 .route_id("canonical-buckets-ttl")
3964 .aggregate(
3965 AggregatorConfig::correlate_by("key")
3966 .complete_when_size(1)
3967 .max_buckets(100)
3968 .bucket_ttl(Duration::from_secs(60))
3969 .build()
3970 .unwrap(),
3971 )
3972 .build_canonical()
3973 .unwrap();
3974
3975 if let CanonicalStepSpec::Aggregate(agg) = &spec.steps[0] {
3976 assert_eq!(agg.max_buckets, Some(100));
3977 assert_eq!(agg.bucket_ttl_ms, Some(60_000));
3978 } else {
3979 panic!("Expected Aggregate step");
3980 }
3981 }
3982
3983 #[test]
3986 fn test_split_builder_with_filter_inside() {
3987 use camel_api::splitter::{SplitterConfig, split_body_lines};
3988
3989 let definition = RouteBuilder::from("timer:test")
3990 .route_id("split-with-filter")
3991 .split(SplitterConfig::new(split_body_lines()))
3992 .filter(|_ex| true)
3993 .to("mock:filtered-frag")
3994 .end_filter()
3995 .end_split()
3996 .build()
3997 .unwrap();
3998
3999 if let BuilderStep::Split { steps, .. } = &definition.steps()[0] {
4000 assert_eq!(steps.len(), 1);
4001 assert!(matches!(&steps[0], BuilderStep::Filter { .. }));
4002 } else {
4003 panic!("Expected Split step");
4004 }
4005 }
4006
4007 #[test]
4010 fn test_wire_tap_multiple_taps() {
4011 let definition = RouteBuilder::from("timer:tick")
4012 .route_id("multi-wire-tap")
4013 .wire_tap("mock:tap1")
4014 .wire_tap("mock:tap2")
4015 .to("mock:result")
4016 .build()
4017 .unwrap();
4018
4019 assert_eq!(definition.steps().len(), 3);
4020 assert!(
4021 matches!(&definition.steps()[0], BuilderStep::WireTap { uri } if uri == "mock:tap1")
4022 );
4023 assert!(
4024 matches!(&definition.steps()[1], BuilderStep::WireTap { uri } if uri == "mock:tap2")
4025 );
4026 }
4027
4028 #[test]
4031 fn test_builder_shorthand_then_explicit_mixed_mode() {
4032 let result = RouteBuilder::from("direct:start")
4033 .route_id("mixed-mode-2")
4034 .dead_letter_channel("log:dlc")
4035 .error_handler(ErrorHandlerConfig::log_only())
4036 .to("mock:out")
4037 .build();
4038
4039 let err = result.err().expect("mixed mode should fail");
4040 assert!(format!("{err}").contains("mixed error handler modes"));
4041 }
4042
4043 #[test]
4046 fn test_build_canonical_empty_from_uri_errors() {
4047 let result = RouteBuilder::from("").route_id("test").build_canonical();
4048 assert!(result.is_err());
4049 }
4050
4051 #[test]
4052 fn test_build_canonical_missing_route_id_errors() {
4053 let result = RouteBuilder::from("direct:start").build_canonical();
4054 assert!(result.is_err());
4055 let err = result.unwrap_err().to_string();
4056 assert!(err.contains("route_id"));
4057 }
4058
4059 #[test]
4062 fn test_split_builder_with_aggregate_inside() {
4063 use camel_api::aggregator::AggregatorConfig;
4064 use camel_api::splitter::{SplitterConfig, split_body_lines};
4065
4066 let definition = RouteBuilder::from("timer:test")
4067 .route_id("split-agg")
4068 .split(SplitterConfig::new(split_body_lines()))
4069 .aggregate(
4070 AggregatorConfig::correlate_by("frag-key")
4071 .complete_when_size(3)
4072 .build()
4073 .unwrap(),
4074 )
4075 .end_split()
4076 .build()
4077 .unwrap();
4078
4079 if let BuilderStep::Split { steps, .. } = &definition.steps()[0] {
4080 assert_eq!(steps.len(), 1);
4081 assert!(matches!(&steps[0], BuilderStep::Aggregate { .. }));
4082 } else {
4083 panic!("Expected Split step");
4084 }
4085 }
4086
4087 #[test]
4090 fn test_throttle_builder_with_steps_inside() {
4091 let definition = RouteBuilder::from("timer:tick")
4092 .route_id("throttle-steps")
4093 .throttle(10, Duration::from_secs(1))
4094 .set_header("throttled", Value::Bool(true))
4095 .to("mock:throttled")
4096 .end_throttle()
4097 .build()
4098 .unwrap();
4099
4100 if let BuilderStep::Throttle { steps, .. } = &definition.steps()[0] {
4101 assert_eq!(steps.len(), 2);
4102 } else {
4103 panic!("Expected Throttle step");
4104 }
4105 }
4106
4107 #[test]
4110 fn test_load_balance_builder_with_steps_inside() {
4111 let definition = RouteBuilder::from("timer:tick")
4112 .route_id("lb-steps")
4113 .load_balance()
4114 .round_robin()
4115 .set_header("lb", Value::Bool(true))
4116 .to("mock:lb")
4117 .end_load_balance()
4118 .build()
4119 .unwrap();
4120
4121 if let BuilderStep::LoadBalance { steps, .. } = &definition.steps()[0] {
4122 assert_eq!(steps.len(), 2);
4123 } else {
4124 panic!("Expected LoadBalance step");
4125 }
4126 }
4127
4128 #[test]
4131 fn test_multicast_builder_with_steps_inside() {
4132 let definition = RouteBuilder::from("timer:tick")
4133 .route_id("multicast-steps")
4134 .multicast()
4135 .set_header("mc", Value::Bool(true))
4136 .to("mock:multicast")
4137 .end_multicast()
4138 .build()
4139 .unwrap();
4140
4141 if let BuilderStep::Multicast { steps, .. } = &definition.steps()[0] {
4142 assert_eq!(steps.len(), 2);
4143 } else {
4144 panic!("Expected Multicast step");
4145 }
4146 }
4147
4148 #[test]
4151 fn test_loop_builder_with_steps_inside() {
4152 let definition = RouteBuilder::from("timer:tick")
4153 .route_id("loop-steps")
4154 .loop_count(3)
4155 .set_header("loop", Value::Bool(true))
4156 .to("mock:loop")
4157 .end_loop()
4158 .build()
4159 .unwrap();
4160
4161 if let BuilderStep::Loop { steps, .. } = &definition.steps()[0] {
4162 assert_eq!(steps.len(), 2);
4163 } else {
4164 panic!("Expected Loop step");
4165 }
4166 }
4167
4168 #[test]
4171 fn test_build_canonical_rejects_loop_step() {
4172 let err = RouteBuilder::from("direct:start")
4173 .route_id("canonical-loop")
4174 .loop_count(3)
4175 .to("mock:loop")
4176 .end_loop()
4177 .build_canonical()
4178 .unwrap_err();
4179
4180 assert!(format!("{err}").contains("does not support step `loop`"));
4181 }
4182
4183 #[test]
4184 fn test_build_canonical_rejects_multicast_step() {
4185 let err = RouteBuilder::from("direct:start")
4186 .route_id("canonical-multicast")
4187 .multicast()
4188 .to("mock:a")
4189 .end_multicast()
4190 .build_canonical()
4191 .unwrap_err();
4192
4193 assert!(format!("{err}").contains("does not support step `multicast`"));
4194 }
4195
4196 #[test]
4197 fn test_build_canonical_rejects_throttle_step() {
4198 let err = RouteBuilder::from("direct:start")
4199 .route_id("canonical-throttle")
4200 .throttle(10, Duration::from_secs(1))
4201 .to("mock:result")
4202 .end_throttle()
4203 .build_canonical()
4204 .unwrap_err();
4205
4206 assert!(format!("{err}").contains("does not support step `throttle`"));
4207 }
4208
4209 #[test]
4210 fn test_build_canonical_rejects_load_balancer_step() {
4211 let err = RouteBuilder::from("direct:start")
4212 .route_id("canonical-lb")
4213 .load_balance()
4214 .round_robin()
4215 .to("mock:result")
4216 .end_load_balance()
4217 .build_canonical()
4218 .unwrap_err();
4219
4220 assert!(format!("{err}").contains("does not support step `load_balancer`"));
4221 }
4222
4223 #[test]
4224 fn test_build_canonical_rejects_bean_step() {
4225 let err = RouteBuilder::from("direct:start")
4226 .route_id("canonical-bean")
4227 .bean("myBean", "process")
4228 .build_canonical()
4229 .unwrap_err();
4230
4231 assert!(format!("{err}").contains("does not support step `bean`"));
4232 }
4233
4234 #[test]
4235 fn test_build_canonical_rejects_script_step() {
4236 let err = RouteBuilder::from("direct:start")
4237 .route_id("canonical-script")
4238 .script("rhai", "x = 1")
4239 .build_canonical()
4240 .unwrap_err();
4241
4242 assert!(format!("{err}").contains("does not support step `script`"));
4243 }
4244
4245 #[test]
4246 fn test_build_canonical_accepts_delay_step() {
4247 let spec = RouteBuilder::from("direct:start")
4248 .route_id("canonical-delay")
4249 .delay(Duration::from_millis(100))
4250 .build_canonical()
4251 .unwrap();
4252
4253 assert!(
4254 spec.steps.iter().any(
4255 |s| matches!(s, CanonicalStepSpec::Delay { delay_ms, .. } if *delay_ms == 100)
4256 )
4257 );
4258 }
4259
4260 #[test]
4261 fn test_build_canonical_accepts_wire_tap_step() {
4262 let spec = RouteBuilder::from("direct:start")
4263 .route_id("canonical-wiretap")
4264 .wire_tap("mock:tap")
4265 .build_canonical()
4266 .unwrap();
4267
4268 assert!(
4269 spec.steps
4270 .iter()
4271 .any(|s| matches!(s, CanonicalStepSpec::WireTap { uri } if uri == "mock:tap"))
4272 );
4273 }
4274
4275 #[test]
4276 fn test_build_canonical_rejects_dynamic_router_step() {
4277 let err = RouteBuilder::from("direct:start")
4278 .route_id("canonical-dyn-router")
4279 .dynamic_router(Arc::new(|_| Some("mock:a".to_string())))
4280 .build_canonical()
4281 .unwrap_err();
4282
4283 assert!(format!("{err}").contains("does not support step `dynamic_router`"));
4284 }
4285
4286 #[test]
4287 fn test_build_canonical_rejects_routing_slip_step() {
4288 let err = RouteBuilder::from("direct:start")
4289 .route_id("canonical-routing-slip")
4290 .routing_slip(Arc::new(|_| Some("mock:a".to_string())))
4291 .build_canonical()
4292 .unwrap_err();
4293
4294 assert!(format!("{err}").contains("does not support step `routing_slip`"));
4295 }
4296
4297 #[test]
4298 fn test_build_canonical_rejects_recipient_list_step() {
4299 let err = RouteBuilder::from("direct:start")
4300 .route_id("canonical-recipient")
4301 .recipient_list(Arc::new(|_| "mock:a".to_string()))
4302 .build_canonical()
4303 .unwrap_err();
4304
4305 assert!(format!("{err}").contains("does not support step `recipient_list`"));
4306 }
4307
4308 #[test]
4311 fn test_build_canonical_rejects_any_mode_with_predicate() {
4312 let err = RouteBuilder::from("direct:start")
4313 .route_id("canonical-any-pred")
4314 .aggregate(AggregatorConfig {
4315 header_name: "key".to_string(),
4316 completion: CompletionMode::Any(vec![
4317 CompletionCondition::Size(5),
4318 CompletionCondition::Predicate(Arc::new(|_| false)),
4319 ]),
4320 correlation: CorrelationStrategy::HeaderName("key".to_string()),
4321 strategy: AggregationStrategy::CollectAll,
4322 max_buckets: None,
4323 bucket_ttl: None,
4324 force_completion_on_stop: false,
4325 discard_on_timeout: false,
4326 max_timeout_tasks: 1024,
4327 })
4328 .build_canonical()
4329 .unwrap_err();
4330
4331 assert!(
4332 format!("{err}").contains("cannot reverse-map"),
4333 "reject message must explain forward-only: {}",
4334 err
4335 );
4336 }
4337
4338 #[test]
4341 fn test_builder_validation_missing_from_uri() {
4342 let result = RouteBuilder::from("")
4343 .route_id("missing-uri-route")
4344 .to("log:info")
4345 .build();
4346 assert!(result.is_err(), "empty from URI should fail validation");
4347 let err = result.err().unwrap().to_string();
4348 assert!(
4349 err.contains("'from'") || err.contains("URI"),
4350 "error should mention from/URI, got: {err}"
4351 );
4352 }
4353
4354 #[test]
4355 fn test_builder_validation_invalid_step_uri_scheme() {
4356 let result = RouteBuilder::from("timer:tick")
4357 .route_id("bad-step-route")
4358 .to("not-a-valid-uri") .build();
4360 assert!(
4363 result.is_ok(),
4364 "builder should accept opaque step URIs; resolution happens later"
4365 );
4366 }
4367
4368 #[test]
4371 fn test_builder_duplicate_route_ids_produce_identical_definitions() {
4372 let route1 = RouteBuilder::from("direct:a")
4375 .route_id("dup-route")
4376 .to("mock:out")
4377 .build();
4378 let route2 = RouteBuilder::from("direct:b")
4379 .route_id("dup-route")
4380 .to("mock:out")
4381 .build();
4382
4383 assert!(route1.is_ok());
4384 assert!(route2.is_ok());
4385 assert_eq!(route1.unwrap().route_id(), route2.unwrap().route_id());
4386 }
4387
4388 #[test]
4389 fn test_builder_clone_reuse_as_template() {
4390 let template = RouteBuilder::from("direct:in")
4393 .set_header("stage", Value::String("shared".into()))
4394 .log("shared prefix", LogLevel::Info);
4395
4396 let route_a = template
4397 .clone()
4398 .route_id("route-a")
4399 .to("mock:a")
4400 .build()
4401 .expect("clone A builds");
4402 let route_b = template
4403 .route_id("route-b")
4404 .to("mock:b")
4405 .build()
4406 .expect("clone B builds");
4407
4408 assert_eq!(route_a.route_id(), "route-a");
4409 assert_eq!(route_b.route_id(), "route-b");
4410 assert_eq!(route_a.steps().len(), route_b.steps().len());
4413 assert_eq!(route_a.from_uri(), route_b.from_uri());
4414 }
4415}