1use futures::future::join_all;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6use tokio::sync::Semaphore;
7use tokio_util::sync::CancellationToken;
8use tower::Service;
9
10use camel_api::{
11 AggregationStrategy, Body, BoxProcessor, CamelError, Exchange, SplitterConfig, Value,
12};
13
14pub const CAMEL_SPLIT_INDEX: &str = "CamelSplitIndex";
18pub const CAMEL_SPLIT_SIZE: &str = "CamelSplitSize";
20pub const CAMEL_SPLIT_COMPLETE: &str = "CamelSplitComplete";
22
23#[derive(Clone)]
40pub struct SplitterService {
41 expression: camel_api::SplitExpression,
42 sub_pipeline: BoxProcessor,
43 aggregation: AggregationStrategy,
44 parallel: bool,
45 parallel_limit: Option<usize>,
46 stop_on_exception: bool,
47 max_fragments: usize,
48 cancel_token: CancellationToken,
49}
50
51impl SplitterService {
52 pub fn new(config: SplitterConfig, sub_pipeline: BoxProcessor) -> Result<Self, CamelError> {
54 config.validate()?;
55 Ok(Self {
56 expression: config.expression,
57 sub_pipeline,
58 aggregation: config.aggregation,
59 parallel: config.parallel,
60 parallel_limit: config.parallel_limit,
61 stop_on_exception: config.stop_on_exception,
62 max_fragments: config.max_fragments,
63 cancel_token: CancellationToken::new(),
64 })
65 }
66
67 pub fn cancel(&self) {
69 self.cancel_token.cancel();
70 }
71
72 pub fn is_cancelled(&self) -> bool {
74 self.cancel_token.is_cancelled()
75 }
76}
77
78impl Service<Exchange> for SplitterService {
79 type Response = Exchange;
80 type Error = CamelError;
81 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
82
83 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
84 self.sub_pipeline.poll_ready(cx)
85 }
86
87 fn call(&mut self, exchange: Exchange) -> Self::Future {
88 let original = exchange.clone();
89 let expression = self.expression.clone();
90 let sub_pipeline = self.sub_pipeline.clone();
91 let aggregation = self.aggregation.clone();
92 let parallel = self.parallel;
93 let parallel_limit = self.parallel_limit;
94 let stop_on_exception = self.stop_on_exception;
95 let max_fragments = self.max_fragments;
96 let cancel_token = self.cancel_token.clone();
97
98 Box::pin(async move {
99 let mut fragments = expression(&exchange)?;
103
104 if fragments.is_empty() {
106 return Ok(original);
107 }
108
109 if fragments.len() > max_fragments {
112 return Err(CamelError::ProcessorError(format!(
113 "Splitter produced {} fragments, exceeding max_fragments {}",
114 fragments.len(),
115 max_fragments
116 )));
117 }
118
119 let total = fragments.len();
120
121 for (i, frag) in fragments.iter_mut().enumerate() {
123 frag.set_property(CAMEL_SPLIT_INDEX, Value::from(i as u64));
124 frag.set_property(CAMEL_SPLIT_SIZE, Value::from(total as u64));
125 frag.set_property(CAMEL_SPLIT_COMPLETE, Value::Bool(i == total - 1));
126 }
127
128 if cancel_token.is_cancelled() {
130 return Err(CamelError::ProcessorError(
131 "Splitter cancelled, dropping exchange".to_string(),
132 ));
133 }
134
135 let results = if parallel {
137 process_parallel(
138 fragments,
139 sub_pipeline,
140 parallel_limit,
141 stop_on_exception,
142 cancel_token,
143 )
144 .await
145 } else {
146 process_sequential(fragments, sub_pipeline, stop_on_exception).await
147 };
148
149 aggregate(results, original, aggregation)
151 })
152 }
153}
154
155async fn process_sequential(
158 fragments: Vec<Exchange>,
159 sub_pipeline: BoxProcessor,
160 stop_on_exception: bool,
161) -> Vec<Result<Exchange, CamelError>> {
162 let mut results = Vec::with_capacity(fragments.len());
163
164 for fragment in fragments {
165 let mut pipeline = sub_pipeline.clone();
166 match tower::ServiceExt::ready(&mut pipeline).await {
167 Err(e) => {
168 results.push(Err(e));
169 if stop_on_exception {
170 break;
171 }
172 }
173 Ok(svc) => {
174 let result = svc.call(fragment).await;
175 let is_err = result.is_err();
176 results.push(result);
177 if stop_on_exception && is_err {
178 break;
179 }
180 }
181 }
182 }
183
184 results
185}
186
187async fn process_parallel(
190 fragments: Vec<Exchange>,
191 sub_pipeline: BoxProcessor,
192 parallel_limit: Option<usize>,
193 _stop_on_exception: bool,
194 cancel_token: CancellationToken,
195) -> Vec<Result<Exchange, CamelError>> {
196 let semaphore = parallel_limit.map(|limit| Arc::new(Semaphore::new(limit)));
197
198 let futures: Vec<_> = fragments
199 .into_iter()
200 .map(|fragment| {
201 let mut pipeline = sub_pipeline.clone();
202 let sem = semaphore.clone();
203 let cancel = cancel_token.clone();
204 async move {
205 if cancel.is_cancelled() {
207 return Err(CamelError::ProcessorError("Splitter cancelled".to_string()));
208 }
209
210 let _permit = match &sem {
212 Some(s) => {
213 tokio::select! {
214 result = s.acquire() => {
215 Some(result.map_err(|e| {
216 CamelError::ProcessorError(format!("semaphore error: {e}"))
217 })?)
218 }
219 _ = cancel.cancelled() => {
220 return Err(CamelError::ProcessorError(
221 "Splitter cancelled while waiting for semaphore".to_string(),
222 ));
223 }
224 }
225 }
226 None => None,
227 };
228
229 if cancel.is_cancelled() {
231 return Err(CamelError::ProcessorError("Splitter cancelled".to_string()));
232 }
233
234 tokio::select! {
235 result = async {
236 tower::ServiceExt::ready(&mut pipeline).await?;
237 pipeline.call(fragment).await
238 } => result,
239 _ = cancel.cancelled() => {
240 Err(CamelError::ProcessorError(
241 "Splitter cancelled during processing".to_string(),
242 ))
243 }
244 }
245 }
246 })
247 .collect();
248
249 join_all(futures).await
250}
251
252fn aggregate(
255 results: Vec<Result<Exchange, CamelError>>,
256 original: Exchange,
257 strategy: AggregationStrategy,
258) -> Result<Exchange, CamelError> {
259 match strategy {
260 AggregationStrategy::LastWins => {
261 results.into_iter().last().unwrap_or_else(|| Ok(original))
263 }
264 AggregationStrategy::CollectAll => {
265 let mut bodies = Vec::new();
267 for result in results {
268 let ex = result?;
269 let value = match &ex.input.body {
270 Body::Text(s) => Value::String(s.clone()),
271 Body::Json(v) => v.clone(),
272 Body::Xml(s) => Value::String(s.clone()),
273 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
274 Body::Stream(s) => serde_json::json!({
275 "_stream": {
276 "origin": s.metadata.origin,
277 "placeholder": true,
278 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
279 }
280 }),
281 _ => Value::Null,
283 };
284 bodies.push(value);
285 }
286 let mut out = original;
287 out.input.body = Body::Json(Value::Array(bodies));
288 Ok(out)
289 }
290 AggregationStrategy::Custom(fold_fn) => {
291 let mut iter = results.into_iter();
293 let first = iter.next().unwrap_or_else(|| Ok(original.clone()))?;
294 iter.try_fold(first, |acc, next_result| {
295 let next = next_result?;
296 Ok(fold_fn(acc, next))
297 })
298 }
299 _ => Ok(original),
301 }
302}
303
304#[cfg(test)]
307mod tests {
308 use super::*;
309 use camel_api::{BoxProcessorExt, Message};
310 use std::sync::Arc;
311 use std::sync::atomic::{AtomicUsize, Ordering};
312 use tower::ServiceExt;
313
314 fn passthrough_pipeline() -> BoxProcessor {
317 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
318 }
319
320 fn uppercase_pipeline() -> BoxProcessor {
321 BoxProcessor::from_fn(|mut ex: Exchange| {
322 Box::pin(async move {
323 if let Body::Text(s) = &ex.input.body {
324 ex.input.body = Body::Text(s.to_uppercase());
325 }
326 Ok(ex)
327 })
328 })
329 }
330
331 fn failing_pipeline() -> BoxProcessor {
332 BoxProcessor::from_fn(|_ex| {
333 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
334 })
335 }
336
337 fn fail_on_nth(n: usize) -> BoxProcessor {
338 let count = Arc::new(AtomicUsize::new(0));
339 BoxProcessor::from_fn(move |ex: Exchange| {
340 let count = Arc::clone(&count);
341 Box::pin(async move {
342 let c = count.fetch_add(1, Ordering::SeqCst);
343 if c == n {
344 Err(CamelError::ProcessorError(format!("fail on {c}")))
345 } else {
346 Ok(ex)
347 }
348 })
349 })
350 }
351
352 fn make_exchange(text: &str) -> Exchange {
353 Exchange::new(Message::new(text))
354 }
355
356 #[test]
357 fn test_splitter_zero_parallel_limit_rejected() {
358 let config = SplitterConfig::new(camel_api::split_body_lines())
359 .parallel(true)
360 .parallel_limit(0);
361 let result = SplitterService::new(config, passthrough_pipeline());
362 assert!(result.is_err(), "zero parallel_limit should return Err");
363 }
364
365 #[tokio::test]
368 async fn test_split_sequential_last_wins() {
369 let config = SplitterConfig::new(camel_api::split_body_lines())
370 .aggregation(AggregationStrategy::LastWins);
371 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
372
373 let result = svc
374 .ready()
375 .await
376 .unwrap()
377 .call(make_exchange("a\nb\nc"))
378 .await
379 .unwrap();
380 assert_eq!(result.input.body.as_text(), Some("C"));
381 }
382
383 #[tokio::test]
386 async fn test_split_sequential_collect_all() {
387 let config = SplitterConfig::new(camel_api::split_body_lines())
388 .aggregation(AggregationStrategy::CollectAll);
389 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
390
391 let result = svc
392 .ready()
393 .await
394 .unwrap()
395 .call(make_exchange("a\nb\nc"))
396 .await
397 .unwrap();
398 let expected = serde_json::json!(["A", "B", "C"]);
399 match &result.input.body {
400 Body::Json(v) => assert_eq!(*v, expected),
401 other => panic!("expected JSON body, got {other:?}"),
402 }
403 }
404
405 #[tokio::test]
408 async fn test_split_sequential_original() {
409 let config = SplitterConfig::new(camel_api::split_body_lines())
410 .aggregation(AggregationStrategy::Original);
411 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
412
413 let result = svc
414 .ready()
415 .await
416 .unwrap()
417 .call(make_exchange("a\nb\nc"))
418 .await
419 .unwrap();
420 assert_eq!(result.input.body.as_text(), Some("a\nb\nc"));
422 }
423
424 #[tokio::test]
427 async fn test_split_sequential_custom_aggregation() {
428 let joiner: Arc<dyn Fn(Exchange, Exchange) -> Exchange + Send + Sync> =
429 Arc::new(|mut acc: Exchange, next: Exchange| {
430 let acc_text = acc.input.body.as_text().unwrap_or("").to_string();
431 let next_text = next.input.body.as_text().unwrap_or("").to_string();
432 acc.input.body = Body::Text(format!("{acc_text}+{next_text}"));
433 acc
434 });
435
436 let config = SplitterConfig::new(camel_api::split_body_lines())
437 .aggregation(AggregationStrategy::Custom(joiner));
438 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
439
440 let result = svc
441 .ready()
442 .await
443 .unwrap()
444 .call(make_exchange("a\nb\nc"))
445 .await
446 .unwrap();
447 assert_eq!(result.input.body.as_text(), Some("A+B+C"));
448 }
449
450 #[tokio::test]
453 async fn test_split_stop_on_exception() {
454 let config = SplitterConfig::new(camel_api::split_body_lines()).stop_on_exception(true);
456 let mut svc = SplitterService::new(config, fail_on_nth(1)).unwrap();
457
458 let result = svc
459 .ready()
460 .await
461 .unwrap()
462 .call(make_exchange("a\nb\nc\nd\ne"))
463 .await;
464
465 assert!(result.is_err(), "expected error due to stop_on_exception");
467 }
468
469 #[tokio::test]
472 async fn test_split_continue_on_exception() {
473 let config = SplitterConfig::new(camel_api::split_body_lines())
475 .stop_on_exception(false)
476 .aggregation(AggregationStrategy::LastWins);
477 let mut svc = SplitterService::new(config, fail_on_nth(1)).unwrap();
478
479 let result = svc
480 .ready()
481 .await
482 .unwrap()
483 .call(make_exchange("a\nb\nc"))
484 .await;
485
486 assert!(result.is_ok(), "last fragment should succeed");
488 }
489
490 #[tokio::test]
493 async fn test_split_empty_fragments() {
494 let config = SplitterConfig::new(camel_api::split_body_lines());
496 let mut svc = SplitterService::new(config, passthrough_pipeline()).unwrap();
497
498 let mut ex = Exchange::new(Message::default()); ex.set_property("marker", Value::Bool(true));
500
501 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
502 assert!(result.input.body.is_empty());
503 assert_eq!(result.property("marker"), Some(&Value::Bool(true)));
504 }
505
506 #[tokio::test]
509 async fn test_split_metadata_properties() {
510 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
514 Box::pin(async move {
515 let idx = ex.property(CAMEL_SPLIT_INDEX).cloned();
516 let size = ex.property(CAMEL_SPLIT_SIZE).cloned();
517 let complete = ex.property(CAMEL_SPLIT_COMPLETE).cloned();
518 let body = serde_json::json!({
519 "index": idx,
520 "size": size,
521 "complete": complete,
522 });
523 let mut out = ex;
524 out.input.body = Body::Json(body);
525 Ok(out)
526 })
527 });
528
529 let config = SplitterConfig::new(camel_api::split_body_lines())
530 .aggregation(AggregationStrategy::CollectAll);
531 let mut svc = SplitterService::new(config, recorder).unwrap();
532
533 let result = svc
534 .ready()
535 .await
536 .unwrap()
537 .call(make_exchange("x\ny\nz"))
538 .await
539 .unwrap();
540
541 let expected = serde_json::json!([
542 {"index": 0, "size": 3, "complete": false},
543 {"index": 1, "size": 3, "complete": false},
544 {"index": 2, "size": 3, "complete": true},
545 ]);
546 match &result.input.body {
547 Body::Json(v) => assert_eq!(*v, expected),
548 other => panic!("expected JSON body, got {other:?}"),
549 }
550 }
551
552 #[tokio::test]
555 async fn test_poll_ready_delegates_to_sub_pipeline() {
556 use std::sync::atomic::AtomicBool;
557
558 #[derive(Clone)]
560 struct DelayedReady {
561 ready: Arc<AtomicBool>,
562 }
563
564 impl Service<Exchange> for DelayedReady {
565 type Response = Exchange;
566 type Error = CamelError;
567 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
568
569 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
570 if self.ready.load(Ordering::SeqCst) {
571 Poll::Ready(Ok(()))
572 } else {
573 cx.waker().wake_by_ref();
574 Poll::Pending
575 }
576 }
577
578 fn call(&mut self, exchange: Exchange) -> Self::Future {
579 Box::pin(async move { Ok(exchange) })
580 }
581 }
582
583 let ready_flag = Arc::new(AtomicBool::new(false));
584 let inner = DelayedReady {
585 ready: Arc::clone(&ready_flag),
586 };
587 let boxed: BoxProcessor = BoxProcessor::new(inner);
588
589 let config = SplitterConfig::new(camel_api::split_body_lines());
590 let mut svc = SplitterService::new(config, boxed).unwrap();
591
592 let waker = futures::task::noop_waker();
594 let mut cx = Context::from_waker(&waker);
595 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
596 assert!(
597 poll.is_pending(),
598 "expected Pending when sub_pipeline not ready"
599 );
600
601 ready_flag.store(true, Ordering::SeqCst);
603
604 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
605 assert!(
606 matches!(poll, Poll::Ready(Ok(()))),
607 "expected Ready after sub_pipeline becomes ready"
608 );
609 }
610
611 #[tokio::test]
614 async fn test_split_parallel_basic() {
615 let config = SplitterConfig::new(camel_api::split_body_lines())
616 .parallel(true)
617 .aggregation(AggregationStrategy::CollectAll);
618 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
619
620 let result = svc
621 .ready()
622 .await
623 .unwrap()
624 .call(make_exchange("a\nb\nc"))
625 .await
626 .unwrap();
627
628 let expected = serde_json::json!(["A", "B", "C"]);
629 match &result.input.body {
630 Body::Json(v) => assert_eq!(*v, expected),
631 other => panic!("expected JSON body, got {other:?}"),
632 }
633 }
634
635 #[tokio::test]
638 async fn test_split_parallel_with_limit() {
639 use std::sync::atomic::AtomicUsize;
640
641 let concurrent = Arc::new(AtomicUsize::new(0));
642 let max_concurrent = Arc::new(AtomicUsize::new(0));
643
644 let c = Arc::clone(&concurrent);
645 let mc = Arc::clone(&max_concurrent);
646 let pipeline = BoxProcessor::from_fn(move |ex: Exchange| {
647 let c = Arc::clone(&c);
648 let mc = Arc::clone(&mc);
649 Box::pin(async move {
650 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
651 mc.fetch_max(current, Ordering::SeqCst);
653 tokio::task::yield_now().await;
655 c.fetch_sub(1, Ordering::SeqCst);
656 Ok(ex)
657 })
658 });
659
660 let config = SplitterConfig::new(camel_api::split_body_lines())
661 .parallel(true)
662 .parallel_limit(2)
663 .aggregation(AggregationStrategy::CollectAll);
664 let mut svc = SplitterService::new(config, pipeline).unwrap();
665
666 let result = svc
667 .ready()
668 .await
669 .unwrap()
670 .call(make_exchange("a\nb\nc\nd"))
671 .await;
672 assert!(result.is_ok());
673
674 let observed_max = max_concurrent.load(Ordering::SeqCst);
675 assert!(
676 observed_max <= 2,
677 "max concurrency was {observed_max}, expected <= 2"
678 );
679 }
680
681 #[tokio::test]
684 async fn test_split_parallel_stop_on_exception() {
685 let config = SplitterConfig::new(camel_api::split_body_lines())
686 .parallel(true)
687 .stop_on_exception(true);
688 let mut svc = SplitterService::new(config, failing_pipeline()).unwrap();
689
690 let result = svc
691 .ready()
692 .await
693 .unwrap()
694 .call(make_exchange("a\nb\nc"))
695 .await;
696
697 assert!(result.is_err(), "expected error when all fragments fail");
699 }
700
701 #[tokio::test]
704 async fn test_splitter_stream_bodies_creates_valid_json() {
705 use bytes::Bytes;
706 use camel_api::{StreamBody, StreamMetadata};
707 use futures::stream;
708 use tokio::sync::Mutex;
709
710 let chunks = vec![Ok(Bytes::from("test"))];
711 let stream_body = StreamBody {
712 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
713 metadata: StreamMetadata {
714 origin: Some("kafka://topic/partition".to_string()),
715 ..Default::default()
716 },
717 };
718
719 let original = Exchange::new(Message {
720 headers: Default::default(),
721 body: Body::Empty,
722 });
723
724 let results = vec![Ok(Exchange::new(Message {
725 headers: Default::default(),
726 body: Body::Stream(stream_body),
727 }))];
728
729 let result = aggregate(results, original, AggregationStrategy::CollectAll);
730
731 let exchange = result.expect("Expected Ok result");
732 assert!(
733 matches!(exchange.input.body, Body::Json(_)),
734 "Expected Json body"
735 );
736
737 if let Body::Json(value) = exchange.input.body {
738 let json_str = serde_json::to_string(&value).unwrap();
739 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
740
741 assert!(parsed.is_array());
742 let arr = parsed.as_array().unwrap();
743 assert!(arr[0].is_object());
744 assert!(arr[0]["_stream"].is_object());
745 assert_eq!(arr[0]["_stream"]["origin"], "kafka://topic/partition");
746 assert_eq!(arr[0]["_stream"]["placeholder"], true);
747 }
748 }
749
750 #[tokio::test]
751 async fn test_splitter_stream_with_none_origin_creates_valid_json() {
752 use bytes::Bytes;
753 use camel_api::{StreamBody, StreamMetadata};
754 use futures::stream;
755 use tokio::sync::Mutex;
756
757 let chunks = vec![Ok(Bytes::from("test"))];
758 let stream_body = StreamBody {
759 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
760 metadata: StreamMetadata {
761 origin: None,
762 ..Default::default()
763 },
764 };
765
766 let original = Exchange::new(Message {
767 headers: Default::default(),
768 body: Body::Empty,
769 });
770
771 let results = vec![Ok(Exchange::new(Message {
772 headers: Default::default(),
773 body: Body::Stream(stream_body),
774 }))];
775
776 let result = aggregate(results, original, AggregationStrategy::CollectAll);
777
778 let exchange = result.expect("Expected Ok result");
779 assert!(
780 matches!(exchange.input.body, Body::Json(_)),
781 "Expected Json body"
782 );
783
784 if let Body::Json(value) = exchange.input.body {
785 let json_str = serde_json::to_string(&value).unwrap();
786 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
787
788 assert!(parsed.is_array());
789 let arr = parsed.as_array().unwrap();
790 assert!(arr[0].is_object());
791 assert!(arr[0]["_stream"].is_object());
792 assert_eq!(arr[0]["_stream"]["origin"], serde_json::Value::Null);
793 assert_eq!(arr[0]["_stream"]["placeholder"], true);
794 }
795 }
796
797 #[tokio::test]
800 async fn test_splitter_parallel_cancel_aborts_processing() {
801 use std::sync::atomic::AtomicBool;
802
803 let started = Arc::new(AtomicBool::new(false));
804
805 let s = Arc::clone(&started);
806 let pipeline = BoxProcessor::from_fn(move |ex: Exchange| {
807 let s = Arc::clone(&s);
808 Box::pin(async move {
809 s.store(true, Ordering::SeqCst);
810 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
812 Ok(ex)
813 })
814 });
815
816 let config = SplitterConfig::new(camel_api::split_body_lines())
817 .parallel(true)
818 .aggregation(AggregationStrategy::LastWins);
819 let svc = SplitterService::new(config, pipeline).unwrap();
820
821 svc.cancel();
823 assert!(svc.is_cancelled());
824
825 let mut svc_clone = svc.clone();
826 let result = svc_clone
827 .ready()
828 .await
829 .unwrap()
830 .call(make_exchange("a\nb\nc"))
831 .await;
832
833 assert!(result.is_err(), "cancelled splitter should return error");
834 }
835
836 #[tokio::test]
839 async fn test_splitter_rejects_fragment_flood() {
840 let expression: camel_api::SplitExpression = std::sync::Arc::new(|_| {
842 Ok((0..5)
843 .map(|i| Exchange::new(Message::new(Body::Text(i.to_string()))))
844 .collect::<Vec<_>>())
845 });
846 let cfg = SplitterConfig::new(expression).max_fragments(2);
847 let passthrough = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
848 let mut svc = SplitterService::new(cfg, passthrough).unwrap();
849
850 let ex = Exchange::new(Message::new(Body::Text("parent".into())));
851 let result = svc.ready().await.unwrap().call(ex).await;
852 let err = result.unwrap_err();
853 assert!(
854 format!("{err}").contains("max_fragments"),
855 "error should mention max_fragments: {err}"
856 );
857 }
858
859 #[tokio::test]
865 async fn test_splitter_each_fragment_has_unique_correlation_id() {
866 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
868 Box::pin(async move {
869 let id = ex.correlation_id().to_string();
870 let mut out = ex;
871 out.input.body = Body::Text(id);
872 Ok(out)
873 })
874 });
875
876 let config = SplitterConfig::new(camel_api::split_body_lines())
877 .aggregation(AggregationStrategy::CollectAll);
878 let mut svc = SplitterService::new(config, recorder).unwrap();
879
880 let result = svc
881 .ready()
882 .await
883 .unwrap()
884 .call(make_exchange("a\nb\nc\nd"))
885 .await
886 .unwrap();
887
888 let ids: Vec<String> = match &result.input.body {
889 Body::Json(serde_json::Value::Array(arr)) => arr
890 .iter()
891 .map(|v| v.as_str().unwrap_or("").to_string())
892 .collect(),
893 other => panic!("expected JSON array of ids, got {other:?}"),
894 };
895 assert_eq!(ids.len(), 4, "should have 4 fragments");
896 let unique: std::collections::HashSet<&String> = ids.iter().collect();
897 assert_eq!(unique.len(), 4, "fragment correlation_ids must be unique");
898 }
899
900 #[tokio::test]
906 async fn test_splitter_json_array_body() {
907 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
908 Box::pin(async move {
909 let v = match &ex.input.body {
910 Body::Json(v) => v.clone(),
911 other => panic!("expected JSON fragment, got {other:?}"),
912 };
913 let mut out = ex;
914 out.input.body = Body::Json(v);
915 Ok(out)
916 })
917 });
918
919 let config = SplitterConfig::new(camel_api::split_body_json_array())
920 .aggregation(AggregationStrategy::CollectAll);
921 let mut svc = SplitterService::new(config, recorder).unwrap();
922
923 let msg = Message::new(Body::Json(serde_json::json!([1, 2, 3])));
924 let parent = Exchange::new(msg);
925
926 let result = svc.ready().await.unwrap().call(parent).await.unwrap();
927 match &result.input.body {
928 Body::Json(serde_json::Value::Array(arr)) => {
929 assert_eq!(arr.len(), 3, "should split array into 3 fragments");
930 assert_eq!(arr[0], 1);
931 assert_eq!(arr[1], 2);
932 assert_eq!(arr[2], 3);
933 }
934 other => panic!("expected JSON array body, got {other:?}"),
935 }
936 }
937
938 #[tokio::test]
941 async fn test_splitter_wrong_type_body_fails_loud() {
942 let config = SplitterConfig::new(camel_api::split_body_json_array());
945 let mut svc = SplitterService::new(config, passthrough_pipeline()).unwrap();
946
947 let ex = Exchange::new(Message::new(Body::Text("a,b".to_string())));
948 let result = svc.ready().await.unwrap().call(ex).await;
949
950 let err = result.expect_err("wrong body type must fail loud, not pass through");
951 let msg = err.to_string();
952 for needle in [
953 "body_json_array",
954 "text",
955 "json (array)",
956 "add an unmarshal step before split",
957 ] {
958 assert!(msg.contains(needle), "message '{msg}' missing '{needle}'");
959 }
960 }
961}