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