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);
101
102 if fragments.is_empty() {
104 return Ok(original);
105 }
106
107 if fragments.len() > max_fragments {
110 return Err(CamelError::ProcessorError(format!(
111 "Splitter produced {} fragments, exceeding max_fragments {}",
112 fragments.len(),
113 max_fragments
114 )));
115 }
116
117 let total = fragments.len();
118
119 for (i, frag) in fragments.iter_mut().enumerate() {
121 frag.set_property(CAMEL_SPLIT_INDEX, Value::from(i as u64));
122 frag.set_property(CAMEL_SPLIT_SIZE, Value::from(total as u64));
123 frag.set_property(CAMEL_SPLIT_COMPLETE, Value::Bool(i == total - 1));
124 }
125
126 if cancel_token.is_cancelled() {
128 return Err(CamelError::ProcessorError(
129 "Splitter cancelled, dropping exchange".to_string(),
130 ));
131 }
132
133 let results = if parallel {
135 process_parallel(
136 fragments,
137 sub_pipeline,
138 parallel_limit,
139 stop_on_exception,
140 cancel_token,
141 )
142 .await
143 } else {
144 process_sequential(fragments, sub_pipeline, stop_on_exception).await
145 };
146
147 aggregate(results, original, aggregation)
149 })
150 }
151}
152
153async fn process_sequential(
156 fragments: Vec<Exchange>,
157 sub_pipeline: BoxProcessor,
158 stop_on_exception: bool,
159) -> Vec<Result<Exchange, CamelError>> {
160 let mut results = Vec::with_capacity(fragments.len());
161
162 for fragment in fragments {
163 let mut pipeline = sub_pipeline.clone();
164 match tower::ServiceExt::ready(&mut pipeline).await {
165 Err(e) => {
166 results.push(Err(e));
167 if stop_on_exception {
168 break;
169 }
170 }
171 Ok(svc) => {
172 let result = svc.call(fragment).await;
173 let is_err = result.is_err();
174 results.push(result);
175 if stop_on_exception && is_err {
176 break;
177 }
178 }
179 }
180 }
181
182 results
183}
184
185async fn process_parallel(
188 fragments: Vec<Exchange>,
189 sub_pipeline: BoxProcessor,
190 parallel_limit: Option<usize>,
191 _stop_on_exception: bool,
192 cancel_token: CancellationToken,
193) -> Vec<Result<Exchange, CamelError>> {
194 let semaphore = parallel_limit.map(|limit| Arc::new(Semaphore::new(limit)));
195
196 let futures: Vec<_> = fragments
197 .into_iter()
198 .map(|fragment| {
199 let mut pipeline = sub_pipeline.clone();
200 let sem = semaphore.clone();
201 let cancel = cancel_token.clone();
202 async move {
203 if cancel.is_cancelled() {
205 return Err(CamelError::ProcessorError("Splitter cancelled".to_string()));
206 }
207
208 let _permit = match &sem {
210 Some(s) => {
211 tokio::select! {
212 result = s.acquire() => {
213 Some(result.map_err(|e| {
214 CamelError::ProcessorError(format!("semaphore error: {e}"))
215 })?)
216 }
217 _ = cancel.cancelled() => {
218 return Err(CamelError::ProcessorError(
219 "Splitter cancelled while waiting for semaphore".to_string(),
220 ));
221 }
222 }
223 }
224 None => None,
225 };
226
227 if cancel.is_cancelled() {
229 return Err(CamelError::ProcessorError("Splitter cancelled".to_string()));
230 }
231
232 tokio::select! {
233 result = async {
234 tower::ServiceExt::ready(&mut pipeline).await?;
235 pipeline.call(fragment).await
236 } => result,
237 _ = cancel.cancelled() => {
238 Err(CamelError::ProcessorError(
239 "Splitter cancelled during processing".to_string(),
240 ))
241 }
242 }
243 }
244 })
245 .collect();
246
247 join_all(futures).await
248}
249
250fn aggregate(
253 results: Vec<Result<Exchange, CamelError>>,
254 original: Exchange,
255 strategy: AggregationStrategy,
256) -> Result<Exchange, CamelError> {
257 match strategy {
258 AggregationStrategy::LastWins => {
259 results.into_iter().last().unwrap_or_else(|| Ok(original))
261 }
262 AggregationStrategy::CollectAll => {
263 let mut bodies = Vec::new();
265 for result in results {
266 let ex = result?;
267 let value = match &ex.input.body {
268 Body::Text(s) => Value::String(s.clone()),
269 Body::Json(v) => v.clone(),
270 Body::Xml(s) => Value::String(s.clone()),
271 Body::Bytes(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
272 Body::Stream(s) => serde_json::json!({
273 "_stream": {
274 "origin": s.metadata.origin,
275 "placeholder": true,
276 "hint": "Materialize exchange body with .into_bytes() before aggregation if content needed"
277 }
278 }),
279 _ => Value::Null,
281 };
282 bodies.push(value);
283 }
284 let mut out = original;
285 out.input.body = Body::Json(Value::Array(bodies));
286 Ok(out)
287 }
288 AggregationStrategy::Custom(fold_fn) => {
289 let mut iter = results.into_iter();
291 let first = iter.next().unwrap_or_else(|| Ok(original.clone()))?;
292 iter.try_fold(first, |acc, next_result| {
293 let next = next_result?;
294 Ok(fold_fn(acc, next))
295 })
296 }
297 _ => Ok(original),
299 }
300}
301
302#[cfg(test)]
305mod tests {
306 use super::*;
307 use camel_api::{BoxProcessorExt, Message};
308 use std::sync::Arc;
309 use std::sync::atomic::{AtomicUsize, Ordering};
310 use tower::ServiceExt;
311
312 fn passthrough_pipeline() -> BoxProcessor {
315 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
316 }
317
318 fn uppercase_pipeline() -> BoxProcessor {
319 BoxProcessor::from_fn(|mut ex: Exchange| {
320 Box::pin(async move {
321 if let Body::Text(s) = &ex.input.body {
322 ex.input.body = Body::Text(s.to_uppercase());
323 }
324 Ok(ex)
325 })
326 })
327 }
328
329 fn failing_pipeline() -> BoxProcessor {
330 BoxProcessor::from_fn(|_ex| {
331 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
332 })
333 }
334
335 fn fail_on_nth(n: usize) -> BoxProcessor {
336 let count = Arc::new(AtomicUsize::new(0));
337 BoxProcessor::from_fn(move |ex: Exchange| {
338 let count = Arc::clone(&count);
339 Box::pin(async move {
340 let c = count.fetch_add(1, Ordering::SeqCst);
341 if c == n {
342 Err(CamelError::ProcessorError(format!("fail on {c}")))
343 } else {
344 Ok(ex)
345 }
346 })
347 })
348 }
349
350 fn make_exchange(text: &str) -> Exchange {
351 Exchange::new(Message::new(text))
352 }
353
354 #[test]
355 fn test_splitter_zero_parallel_limit_rejected() {
356 let config = SplitterConfig::new(camel_api::split_body_lines())
357 .parallel(true)
358 .parallel_limit(0);
359 let result = SplitterService::new(config, passthrough_pipeline());
360 assert!(result.is_err(), "zero parallel_limit should return Err");
361 }
362
363 #[tokio::test]
366 async fn test_split_sequential_last_wins() {
367 let config = SplitterConfig::new(camel_api::split_body_lines())
368 .aggregation(AggregationStrategy::LastWins);
369 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
370
371 let result = svc
372 .ready()
373 .await
374 .unwrap()
375 .call(make_exchange("a\nb\nc"))
376 .await
377 .unwrap();
378 assert_eq!(result.input.body.as_text(), Some("C"));
379 }
380
381 #[tokio::test]
384 async fn test_split_sequential_collect_all() {
385 let config = SplitterConfig::new(camel_api::split_body_lines())
386 .aggregation(AggregationStrategy::CollectAll);
387 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
388
389 let result = svc
390 .ready()
391 .await
392 .unwrap()
393 .call(make_exchange("a\nb\nc"))
394 .await
395 .unwrap();
396 let expected = serde_json::json!(["A", "B", "C"]);
397 match &result.input.body {
398 Body::Json(v) => assert_eq!(*v, expected),
399 other => panic!("expected JSON body, got {other:?}"),
400 }
401 }
402
403 #[tokio::test]
406 async fn test_split_sequential_original() {
407 let config = SplitterConfig::new(camel_api::split_body_lines())
408 .aggregation(AggregationStrategy::Original);
409 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
410
411 let result = svc
412 .ready()
413 .await
414 .unwrap()
415 .call(make_exchange("a\nb\nc"))
416 .await
417 .unwrap();
418 assert_eq!(result.input.body.as_text(), Some("a\nb\nc"));
420 }
421
422 #[tokio::test]
425 async fn test_split_sequential_custom_aggregation() {
426 let joiner: Arc<dyn Fn(Exchange, Exchange) -> Exchange + Send + Sync> =
427 Arc::new(|mut acc: Exchange, next: Exchange| {
428 let acc_text = acc.input.body.as_text().unwrap_or("").to_string();
429 let next_text = next.input.body.as_text().unwrap_or("").to_string();
430 acc.input.body = Body::Text(format!("{acc_text}+{next_text}"));
431 acc
432 });
433
434 let config = SplitterConfig::new(camel_api::split_body_lines())
435 .aggregation(AggregationStrategy::Custom(joiner));
436 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
437
438 let result = svc
439 .ready()
440 .await
441 .unwrap()
442 .call(make_exchange("a\nb\nc"))
443 .await
444 .unwrap();
445 assert_eq!(result.input.body.as_text(), Some("A+B+C"));
446 }
447
448 #[tokio::test]
451 async fn test_split_stop_on_exception() {
452 let config = SplitterConfig::new(camel_api::split_body_lines()).stop_on_exception(true);
454 let mut svc = SplitterService::new(config, fail_on_nth(1)).unwrap();
455
456 let result = svc
457 .ready()
458 .await
459 .unwrap()
460 .call(make_exchange("a\nb\nc\nd\ne"))
461 .await;
462
463 assert!(result.is_err(), "expected error due to stop_on_exception");
465 }
466
467 #[tokio::test]
470 async fn test_split_continue_on_exception() {
471 let config = SplitterConfig::new(camel_api::split_body_lines())
473 .stop_on_exception(false)
474 .aggregation(AggregationStrategy::LastWins);
475 let mut svc = SplitterService::new(config, fail_on_nth(1)).unwrap();
476
477 let result = svc
478 .ready()
479 .await
480 .unwrap()
481 .call(make_exchange("a\nb\nc"))
482 .await;
483
484 assert!(result.is_ok(), "last fragment should succeed");
486 }
487
488 #[tokio::test]
491 async fn test_split_empty_fragments() {
492 let config = SplitterConfig::new(camel_api::split_body_lines());
494 let mut svc = SplitterService::new(config, passthrough_pipeline()).unwrap();
495
496 let mut ex = Exchange::new(Message::default()); ex.set_property("marker", Value::Bool(true));
498
499 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
500 assert!(result.input.body.is_empty());
501 assert_eq!(result.property("marker"), Some(&Value::Bool(true)));
502 }
503
504 #[tokio::test]
507 async fn test_split_metadata_properties() {
508 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
512 Box::pin(async move {
513 let idx = ex.property(CAMEL_SPLIT_INDEX).cloned();
514 let size = ex.property(CAMEL_SPLIT_SIZE).cloned();
515 let complete = ex.property(CAMEL_SPLIT_COMPLETE).cloned();
516 let body = serde_json::json!({
517 "index": idx,
518 "size": size,
519 "complete": complete,
520 });
521 let mut out = ex;
522 out.input.body = Body::Json(body);
523 Ok(out)
524 })
525 });
526
527 let config = SplitterConfig::new(camel_api::split_body_lines())
528 .aggregation(AggregationStrategy::CollectAll);
529 let mut svc = SplitterService::new(config, recorder).unwrap();
530
531 let result = svc
532 .ready()
533 .await
534 .unwrap()
535 .call(make_exchange("x\ny\nz"))
536 .await
537 .unwrap();
538
539 let expected = serde_json::json!([
540 {"index": 0, "size": 3, "complete": false},
541 {"index": 1, "size": 3, "complete": false},
542 {"index": 2, "size": 3, "complete": true},
543 ]);
544 match &result.input.body {
545 Body::Json(v) => assert_eq!(*v, expected),
546 other => panic!("expected JSON body, got {other:?}"),
547 }
548 }
549
550 #[tokio::test]
553 async fn test_poll_ready_delegates_to_sub_pipeline() {
554 use std::sync::atomic::AtomicBool;
555
556 #[derive(Clone)]
558 struct DelayedReady {
559 ready: Arc<AtomicBool>,
560 }
561
562 impl Service<Exchange> for DelayedReady {
563 type Response = Exchange;
564 type Error = CamelError;
565 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
566
567 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
568 if self.ready.load(Ordering::SeqCst) {
569 Poll::Ready(Ok(()))
570 } else {
571 cx.waker().wake_by_ref();
572 Poll::Pending
573 }
574 }
575
576 fn call(&mut self, exchange: Exchange) -> Self::Future {
577 Box::pin(async move { Ok(exchange) })
578 }
579 }
580
581 let ready_flag = Arc::new(AtomicBool::new(false));
582 let inner = DelayedReady {
583 ready: Arc::clone(&ready_flag),
584 };
585 let boxed: BoxProcessor = BoxProcessor::new(inner);
586
587 let config = SplitterConfig::new(camel_api::split_body_lines());
588 let mut svc = SplitterService::new(config, boxed).unwrap();
589
590 let waker = futures::task::noop_waker();
592 let mut cx = Context::from_waker(&waker);
593 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
594 assert!(
595 poll.is_pending(),
596 "expected Pending when sub_pipeline not ready"
597 );
598
599 ready_flag.store(true, Ordering::SeqCst);
601
602 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
603 assert!(
604 matches!(poll, Poll::Ready(Ok(()))),
605 "expected Ready after sub_pipeline becomes ready"
606 );
607 }
608
609 #[tokio::test]
612 async fn test_split_parallel_basic() {
613 let config = SplitterConfig::new(camel_api::split_body_lines())
614 .parallel(true)
615 .aggregation(AggregationStrategy::CollectAll);
616 let mut svc = SplitterService::new(config, uppercase_pipeline()).unwrap();
617
618 let result = svc
619 .ready()
620 .await
621 .unwrap()
622 .call(make_exchange("a\nb\nc"))
623 .await
624 .unwrap();
625
626 let expected = serde_json::json!(["A", "B", "C"]);
627 match &result.input.body {
628 Body::Json(v) => assert_eq!(*v, expected),
629 other => panic!("expected JSON body, got {other:?}"),
630 }
631 }
632
633 #[tokio::test]
636 async fn test_split_parallel_with_limit() {
637 use std::sync::atomic::AtomicUsize;
638
639 let concurrent = Arc::new(AtomicUsize::new(0));
640 let max_concurrent = Arc::new(AtomicUsize::new(0));
641
642 let c = Arc::clone(&concurrent);
643 let mc = Arc::clone(&max_concurrent);
644 let pipeline = BoxProcessor::from_fn(move |ex: Exchange| {
645 let c = Arc::clone(&c);
646 let mc = Arc::clone(&mc);
647 Box::pin(async move {
648 let current = c.fetch_add(1, Ordering::SeqCst) + 1;
649 mc.fetch_max(current, Ordering::SeqCst);
651 tokio::task::yield_now().await;
653 c.fetch_sub(1, Ordering::SeqCst);
654 Ok(ex)
655 })
656 });
657
658 let config = SplitterConfig::new(camel_api::split_body_lines())
659 .parallel(true)
660 .parallel_limit(2)
661 .aggregation(AggregationStrategy::CollectAll);
662 let mut svc = SplitterService::new(config, pipeline).unwrap();
663
664 let result = svc
665 .ready()
666 .await
667 .unwrap()
668 .call(make_exchange("a\nb\nc\nd"))
669 .await;
670 assert!(result.is_ok());
671
672 let observed_max = max_concurrent.load(Ordering::SeqCst);
673 assert!(
674 observed_max <= 2,
675 "max concurrency was {observed_max}, expected <= 2"
676 );
677 }
678
679 #[tokio::test]
682 async fn test_split_parallel_stop_on_exception() {
683 let config = SplitterConfig::new(camel_api::split_body_lines())
684 .parallel(true)
685 .stop_on_exception(true);
686 let mut svc = SplitterService::new(config, failing_pipeline()).unwrap();
687
688 let result = svc
689 .ready()
690 .await
691 .unwrap()
692 .call(make_exchange("a\nb\nc"))
693 .await;
694
695 assert!(result.is_err(), "expected error when all fragments fail");
697 }
698
699 #[tokio::test]
702 async fn test_splitter_stream_bodies_creates_valid_json() {
703 use bytes::Bytes;
704 use camel_api::{StreamBody, StreamMetadata};
705 use futures::stream;
706 use tokio::sync::Mutex;
707
708 let chunks = vec![Ok(Bytes::from("test"))];
709 let stream_body = StreamBody {
710 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
711 metadata: StreamMetadata {
712 origin: Some("kafka://topic/partition".to_string()),
713 ..Default::default()
714 },
715 };
716
717 let original = Exchange::new(Message {
718 headers: Default::default(),
719 body: Body::Empty,
720 });
721
722 let results = vec![Ok(Exchange::new(Message {
723 headers: Default::default(),
724 body: Body::Stream(stream_body),
725 }))];
726
727 let result = aggregate(results, original, AggregationStrategy::CollectAll);
728
729 let exchange = result.expect("Expected Ok result");
730 assert!(
731 matches!(exchange.input.body, Body::Json(_)),
732 "Expected Json body"
733 );
734
735 if let Body::Json(value) = exchange.input.body {
736 let json_str = serde_json::to_string(&value).unwrap();
737 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
738
739 assert!(parsed.is_array());
740 let arr = parsed.as_array().unwrap();
741 assert!(arr[0].is_object());
742 assert!(arr[0]["_stream"].is_object());
743 assert_eq!(arr[0]["_stream"]["origin"], "kafka://topic/partition");
744 assert_eq!(arr[0]["_stream"]["placeholder"], true);
745 }
746 }
747
748 #[tokio::test]
749 async fn test_splitter_stream_with_none_origin_creates_valid_json() {
750 use bytes::Bytes;
751 use camel_api::{StreamBody, StreamMetadata};
752 use futures::stream;
753 use tokio::sync::Mutex;
754
755 let chunks = vec![Ok(Bytes::from("test"))];
756 let stream_body = StreamBody {
757 stream: Arc::new(Mutex::new(Some(Box::pin(stream::iter(chunks))))),
758 metadata: StreamMetadata {
759 origin: None,
760 ..Default::default()
761 },
762 };
763
764 let original = Exchange::new(Message {
765 headers: Default::default(),
766 body: Body::Empty,
767 });
768
769 let results = vec![Ok(Exchange::new(Message {
770 headers: Default::default(),
771 body: Body::Stream(stream_body),
772 }))];
773
774 let result = aggregate(results, original, AggregationStrategy::CollectAll);
775
776 let exchange = result.expect("Expected Ok result");
777 assert!(
778 matches!(exchange.input.body, Body::Json(_)),
779 "Expected Json body"
780 );
781
782 if let Body::Json(value) = exchange.input.body {
783 let json_str = serde_json::to_string(&value).unwrap();
784 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
785
786 assert!(parsed.is_array());
787 let arr = parsed.as_array().unwrap();
788 assert!(arr[0].is_object());
789 assert!(arr[0]["_stream"].is_object());
790 assert_eq!(arr[0]["_stream"]["origin"], serde_json::Value::Null);
791 assert_eq!(arr[0]["_stream"]["placeholder"], true);
792 }
793 }
794
795 #[tokio::test]
798 async fn test_splitter_parallel_cancel_aborts_processing() {
799 use std::sync::atomic::AtomicBool;
800
801 let started = Arc::new(AtomicBool::new(false));
802
803 let s = Arc::clone(&started);
804 let pipeline = BoxProcessor::from_fn(move |ex: Exchange| {
805 let s = Arc::clone(&s);
806 Box::pin(async move {
807 s.store(true, Ordering::SeqCst);
808 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
810 Ok(ex)
811 })
812 });
813
814 let config = SplitterConfig::new(camel_api::split_body_lines())
815 .parallel(true)
816 .aggregation(AggregationStrategy::LastWins);
817 let svc = SplitterService::new(config, pipeline).unwrap();
818
819 svc.cancel();
821 assert!(svc.is_cancelled());
822
823 let mut svc_clone = svc.clone();
824 let result = svc_clone
825 .ready()
826 .await
827 .unwrap()
828 .call(make_exchange("a\nb\nc"))
829 .await;
830
831 assert!(result.is_err(), "cancelled splitter should return error");
832 }
833
834 #[tokio::test]
837 async fn test_splitter_rejects_fragment_flood() {
838 let expression: camel_api::SplitExpression = std::sync::Arc::new(|_| {
840 (0..5)
841 .map(|i| Exchange::new(Message::new(Body::Text(i.to_string()))))
842 .collect::<Vec<_>>()
843 });
844 let cfg = SplitterConfig::new(expression).max_fragments(2);
845 let passthrough = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
846 let mut svc = SplitterService::new(cfg, passthrough).unwrap();
847
848 let ex = Exchange::new(Message::new(Body::Text("parent".into())));
849 let result = svc.ready().await.unwrap().call(ex).await;
850 let err = result.unwrap_err();
851 assert!(
852 format!("{err}").contains("max_fragments"),
853 "error should mention max_fragments: {err}"
854 );
855 }
856
857 #[tokio::test]
863 async fn test_splitter_each_fragment_has_unique_correlation_id() {
864 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
866 Box::pin(async move {
867 let id = ex.correlation_id().to_string();
868 let mut out = ex;
869 out.input.body = Body::Text(id);
870 Ok(out)
871 })
872 });
873
874 let config = SplitterConfig::new(camel_api::split_body_lines())
875 .aggregation(AggregationStrategy::CollectAll);
876 let mut svc = SplitterService::new(config, recorder).unwrap();
877
878 let result = svc
879 .ready()
880 .await
881 .unwrap()
882 .call(make_exchange("a\nb\nc\nd"))
883 .await
884 .unwrap();
885
886 let ids: Vec<String> = match &result.input.body {
887 Body::Json(serde_json::Value::Array(arr)) => arr
888 .iter()
889 .map(|v| v.as_str().unwrap_or("").to_string())
890 .collect(),
891 other => panic!("expected JSON array of ids, got {other:?}"),
892 };
893 assert_eq!(ids.len(), 4, "should have 4 fragments");
894 let unique: std::collections::HashSet<&String> = ids.iter().collect();
895 assert_eq!(unique.len(), 4, "fragment correlation_ids must be unique");
896 }
897
898 #[tokio::test]
904 async fn test_splitter_json_array_body() {
905 let recorder = BoxProcessor::from_fn(|ex: Exchange| {
906 Box::pin(async move {
907 let v = match &ex.input.body {
908 Body::Json(v) => v.clone(),
909 other => panic!("expected JSON fragment, got {other:?}"),
910 };
911 let mut out = ex;
912 out.input.body = Body::Json(v);
913 Ok(out)
914 })
915 });
916
917 let config = SplitterConfig::new(camel_api::split_body_json_array())
918 .aggregation(AggregationStrategy::CollectAll);
919 let mut svc = SplitterService::new(config, recorder).unwrap();
920
921 let msg = Message::new(Body::Json(serde_json::json!([1, 2, 3])));
922 let parent = Exchange::new(msg);
923
924 let result = svc.ready().await.unwrap().call(parent).await.unwrap();
925 match &result.input.body {
926 Body::Json(serde_json::Value::Array(arr)) => {
927 assert_eq!(arr.len(), 3, "should split array into 3 fragments");
928 assert_eq!(arr[0], 1);
929 assert_eq!(arr[1], 2);
930 assert_eq!(arr[2], 3);
931 }
932 other => panic!("expected JSON array body, got {other:?}"),
933 }
934 }
935}