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