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