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