Skip to main content

camel_api/
body.rs

1use crate::error::CamelError;
2/// General-purpose default limit for [`Body::materialize()`] (10 MB).
3///
4/// This is separate from `stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD` (128 KB),
5/// which is the OOM-protection limit used by `StreamCacheService`.
6pub const DEFAULT_MATERIALIZE_LIMIT: usize = 10 * 1024 * 1024;
7
8use bytes::{Bytes, BytesMut};
9use futures::stream::BoxStream;
10use futures::{StreamExt, TryStreamExt};
11use std::io;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::task::{Context as TaskContext, Poll};
15use tokio::io::{AsyncRead, ReadBuf};
16use tokio::sync::Mutex;
17use tokio_util::io::StreamReader;
18
19/// A boxed [`AsyncRead`] for reading body content without materializing it into memory.
20///
21/// Returned by [`Body::into_async_read()`].
22pub type BoxAsyncRead = Pin<Box<dyn AsyncRead + Send + Unpin>>;
23
24/// Metadata associated with a stream body.
25#[derive(Debug, Clone, Default)]
26pub struct StreamMetadata {
27    /// Expected size of the stream if known.
28    pub size_hint: Option<u64>,
29    /// Content type of the stream content.
30    pub content_type: Option<String>,
31    /// Origin of the stream (e.g. "file:///path/to/file").
32    pub origin: Option<String>,
33}
34
35/// A body that wraps a lazy-evaluated stream of bytes.
36///
37/// # Clone Semantics
38///
39/// The stream is **single-consumption**. When cloning a `Body::Stream`,
40/// all clones share the same underlying stream handle. Only the first
41/// clone to consume the stream will succeed; subsequent attempts will
42/// return `CamelError::AlreadyConsumed`.
43///
44/// # Example
45///
46/// ```rust
47/// use camel_api::{Body, StreamBody, error::CamelError};
48/// use futures::stream;
49/// use bytes::Bytes;
50/// use std::sync::Arc;
51/// use tokio::sync::Mutex;
52///
53/// # #[tokio::main]
54/// # async fn main() -> Result<(), CamelError> {
55/// let chunks = vec![Ok(Bytes::from("data"))];
56/// let stream = stream::iter(chunks);
57/// let body = Body::Stream(StreamBody {
58///     stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
59///     metadata: Default::default(),
60/// });
61///
62/// let clone = body.clone();
63///
64/// // First consumption succeeds
65/// let _ = body.into_bytes(1024).await?;
66///
67/// // Second consumption fails
68/// let result = clone.into_bytes(1024).await;
69/// assert!(matches!(result, Err(CamelError::AlreadyConsumed)));
70/// # Ok(())
71/// # }
72/// ```
73pub struct StreamBody {
74    /// The actual byte stream, wrapped in an Arc and Mutex to allow Clone for Body.
75    #[allow(clippy::type_complexity)]
76    pub stream: Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>,
77    /// Metadata associated with the stream.
78    pub metadata: StreamMetadata,
79}
80
81impl std::fmt::Debug for StreamBody {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("StreamBody")
84            .field("metadata", &self.metadata)
85            .field("stream", &"<BoxStream>")
86            .finish()
87    }
88}
89
90impl Clone for StreamBody {
91    fn clone(&self) -> Self {
92        Self {
93            stream: Arc::clone(&self.stream),
94            metadata: self.metadata.clone(),
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// StreamAsyncRead — adapts Body::Stream into AsyncRead
101// ---------------------------------------------------------------------------
102
103/// Private adapter that implements [`AsyncRead`] for [`Body::Stream`].
104///
105/// On the first `poll_read`, attempts a non-blocking `try_lock()` on the inner
106/// `Arc<Mutex<Option<BoxStream>>>`:
107/// - If the lock succeeds and the stream is `Some`, extracts it and creates
108///   an active [`StreamReader`].
109/// - If the stream is `None` (already consumed), returns an [`io::Error`].
110/// - If the lock is contended (extremely rare), wakes the task and returns
111///   `Poll::Pending` to retry.
112#[allow(clippy::type_complexity)]
113struct StreamAsyncRead {
114    arc: Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>,
115    /// Holds the active reader after the stream is extracted on first poll.
116    reader: Option<Box<dyn AsyncRead + Send + Unpin>>,
117    /// Set to true when the stream was already consumed (prevents further reads).
118    consumed: bool,
119}
120
121impl AsyncRead for StreamAsyncRead {
122    fn poll_read(
123        mut self: Pin<&mut Self>,
124        cx: &mut TaskContext<'_>,
125        buf: &mut ReadBuf<'_>,
126    ) -> Poll<io::Result<()>> {
127        if self.consumed {
128            return Poll::Ready(Err(io::Error::other("stream already consumed")));
129        }
130        // Lazy init: extract the stream on first poll
131        if self.reader.is_none() {
132            // Extract stream in a separate scope to avoid holding the lock while modifying self
133            let extracted = {
134                match self.arc.try_lock() {
135                    Ok(mut guard) => guard.take(),
136                    Err(_) => {
137                        // Lock contended — schedule a retry
138                        cx.waker().wake_by_ref();
139                        return Poll::Pending;
140                    }
141                }
142            };
143            // Now safe to modify self since lock is dropped
144            match extracted {
145                Some(stream) => {
146                    let mapped = stream.map_err(|e: CamelError| io::Error::other(e.to_string()));
147                    self.reader = Some(Box::new(StreamReader::new(mapped)));
148                }
149                None => {
150                    self.consumed = true;
151                    return Poll::Ready(Err(io::Error::other("stream already consumed")));
152                }
153            }
154        }
155        // Delegate to the active reader
156        Pin::new(self.reader.as_mut().unwrap()).poll_read(cx, buf) // allow-unwrap
157    }
158}
159
160/// The body of a message, supporting common payload types.
161#[derive(Debug, Default)]
162#[non_exhaustive]
163pub enum Body {
164    /// No body content.
165    #[default]
166    Empty,
167    /// Raw bytes payload.
168    Bytes(Bytes),
169    /// UTF-8 string payload.
170    Text(String),
171    /// JSON payload.
172    Json(serde_json::Value),
173    /// XML payload (well-formed XML string; use `try_into_xml()` for validation).
174    Xml(String),
175    /// Streaming payload.
176    Stream(StreamBody),
177}
178
179impl Clone for Body {
180    fn clone(&self) -> Self {
181        match self {
182            Body::Empty => Body::Empty,
183            Body::Bytes(b) => Body::Bytes(b.clone()),
184            Body::Text(s) => Body::Text(s.clone()),
185            Body::Json(v) => Body::Json(v.clone()),
186            Body::Xml(s) => Body::Xml(s.clone()),
187            Body::Stream(s) => Body::Stream(s.clone()),
188        }
189    }
190}
191
192impl PartialEq for Body {
193    fn eq(&self, other: &Self) -> bool {
194        match (self, other) {
195            (Body::Empty, Body::Empty) => true,
196            (Body::Text(a), Body::Text(b)) => a == b,
197            (Body::Json(a), Body::Json(b)) => a == b,
198            (Body::Bytes(a), Body::Bytes(b)) => a == b,
199            (Body::Xml(a), Body::Xml(b)) => a == b,
200            // Stream: two streams are never equal (single-consumption)
201            _ => false,
202        }
203    }
204}
205
206impl Body {
207    /// Returns `true` if the body is empty.
208    pub fn is_empty(&self) -> bool {
209        matches!(self, Body::Empty)
210    }
211
212    /// Convert the body into `Bytes`, consuming it if it is a stream.
213    /// This is an async operation because it may need to read from an underlying stream.
214    /// A `max_size` limit is enforced to prevent OOM errors.
215    pub async fn into_bytes(self, max_size: usize) -> Result<Bytes, CamelError> {
216        match self {
217            Body::Empty => Ok(Bytes::new()),
218            Body::Bytes(b) => {
219                if b.len() > max_size {
220                    return Err(CamelError::StreamLimitExceeded(max_size));
221                }
222                Ok(b)
223            }
224            Body::Text(s) => {
225                if s.len() > max_size {
226                    return Err(CamelError::StreamLimitExceeded(max_size));
227                }
228                Ok(Bytes::from(s))
229            }
230            Body::Json(v) => {
231                let b = serde_json::to_vec(&v)
232                    .map_err(|e| CamelError::TypeConversionFailed(e.to_string()))?;
233                if b.len() > max_size {
234                    return Err(CamelError::StreamLimitExceeded(max_size));
235                }
236                Ok(Bytes::from(b))
237            }
238            Body::Xml(s) => {
239                if s.len() > max_size {
240                    return Err(CamelError::StreamLimitExceeded(max_size));
241                }
242                Ok(Bytes::from(s))
243            }
244            Body::Stream(s) => {
245                let mut stream_lock = s.stream.lock().await;
246                let mut stream = stream_lock.take().ok_or(CamelError::AlreadyConsumed)?;
247
248                let mut buffer = BytesMut::new();
249                while let Some(chunk_res) = stream.next().await {
250                    let chunk = chunk_res?;
251                    if buffer.len() + chunk.len() > max_size {
252                        return Err(CamelError::StreamLimitExceeded(max_size));
253                    }
254                    buffer.extend_from_slice(&chunk);
255                }
256                Ok(buffer.freeze())
257            }
258        }
259    }
260
261    /// Materialize stream with sensible default limit (10 MB).
262    ///
263    /// Convenience method for common cases where you need the stream content
264    /// but don't want to specify a custom limit. For the tighter stream-cache
265    /// threshold (128 KB), use `stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD`
266    /// with [`Body::into_bytes()`] instead.
267    ///
268    /// # Example
269    /// ```ignore
270    /// let body = Body::Stream(stream);
271    /// let bytes = body.materialize().await?;
272    /// ```
273    pub async fn materialize(self) -> Result<Bytes, CamelError> {
274        self.into_bytes(DEFAULT_MATERIALIZE_LIMIT).await
275    }
276
277    /// Convert the body into an [`AsyncRead`] without materializing it into memory.
278    ///
279    /// - [`Body::Empty`] → empty reader (0 bytes)
280    /// - [`Body::Bytes`] → in-memory cursor
281    /// - [`Body::Text`] → UTF-8 bytes cursor
282    /// - [`Body::Json`] → serialized JSON bytes cursor
283    /// - [`Body::Xml`] → UTF-8 bytes cursor
284    /// - [`Body::Stream`] → streams chunk-by-chunk via [`StreamReader`];
285    ///   if the stream was already consumed, the reader returns an [`io::Error`]
286    ///   on the first read
287    pub fn into_async_read(self) -> Result<BoxAsyncRead, CamelError> {
288        match self {
289            Body::Empty => Ok(Box::pin(tokio::io::empty())),
290            Body::Bytes(b) => Ok(Box::pin(std::io::Cursor::new(b))),
291            Body::Text(s) => Ok(Box::pin(std::io::Cursor::new(s.into_bytes()))),
292            Body::Json(v) => {
293                let bytes = serde_json::to_vec(&v)
294                    .map_err(|e| CamelError::TypeConversionFailed(e.to_string()))?;
295                Ok(Box::pin(std::io::Cursor::new(bytes)) as BoxAsyncRead)
296            }
297            Body::Xml(s) => Ok(Box::pin(std::io::Cursor::new(s.into_bytes()))),
298            Body::Stream(s) => Ok(Box::pin(StreamAsyncRead {
299                arc: s.stream,
300                reader: None,
301                consumed: false,
302            })),
303        }
304    }
305
306    /// Try to get the body as a string, converting from bytes if needed.
307    pub fn as_text(&self) -> Option<&str> {
308        match self {
309            Body::Text(s) => Some(s.as_str()),
310            _ => None,
311        }
312    }
313
314    /// Try to get the body as an XML string.
315    pub fn as_xml(&self) -> Option<&str> {
316        match self {
317            Body::Xml(s) => Some(s.as_str()),
318            _ => None,
319        }
320    }
321
322    /// Convert this body to `Body::Text`, consuming it.
323    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
324    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
325    pub fn try_into_text(self) -> Result<Body, CamelError> {
326        crate::body_converter::convert(self, crate::body_converter::BodyType::Text)
327    }
328
329    /// Convert this body to `Body::Json`, consuming it.
330    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
331    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
332    pub fn try_into_json(self) -> Result<Body, CamelError> {
333        crate::body_converter::convert(self, crate::body_converter::BodyType::Json)
334    }
335
336    /// Convert this body to `Body::Bytes`, consuming it.
337    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
338    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
339    pub fn try_into_bytes_body(self) -> Result<Body, CamelError> {
340        crate::body_converter::convert(self, crate::body_converter::BodyType::Bytes)
341    }
342
343    /// Convert this body to `Body::Xml`, consuming it.
344    /// Returns `Err(TypeConversionFailed)` if the conversion is not possible.
345    /// `Body::Stream` always fails — materialize with `into_bytes()` first.
346    pub fn try_into_xml(self) -> Result<Body, CamelError> {
347        crate::body_converter::convert(self, crate::body_converter::BodyType::Xml)
348    }
349}
350
351/// Returns a human-readable name for the body type variant.
352///
353/// The camel-core tracer reuses this public helper. The `_ => "unknown"` arm
354/// stays for forward compatibility (`Body` is `#[non_exhaustive]`).
355#[allow(unreachable_patterns)]
356pub fn body_type_name(body: &Body) -> &'static str {
357    match body {
358        Body::Empty => "empty",
359        Body::Bytes(_) => "bytes",
360        Body::Text(_) => "text",
361        Body::Json(_) => "json",
362        Body::Xml(_) => "xml",
363        Body::Stream(_) => "stream",
364        _ => "unknown",
365    }
366}
367
368// Conversion impls
369impl From<String> for Body {
370    fn from(s: String) -> Self {
371        Body::Text(s)
372    }
373}
374
375impl From<&str> for Body {
376    fn from(s: &str) -> Self {
377        Body::Text(s.to_string())
378    }
379}
380
381impl From<Bytes> for Body {
382    fn from(b: Bytes) -> Self {
383        Body::Bytes(b)
384    }
385}
386
387impl From<Vec<u8>> for Body {
388    fn from(v: Vec<u8>) -> Self {
389        Body::Bytes(Bytes::from(v))
390    }
391}
392
393impl From<serde_json::Value> for Body {
394    fn from(v: serde_json::Value) -> Self {
395        Body::Json(v)
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_body_type_name_variants() {
405        assert_eq!(body_type_name(&Body::Empty), "empty");
406        assert_eq!(body_type_name(&Body::Bytes(Bytes::new())), "bytes");
407        assert_eq!(body_type_name(&Body::Text(String::new())), "text");
408        assert_eq!(body_type_name(&Body::Json(serde_json::Value::Null)), "json");
409        assert_eq!(body_type_name(&Body::Xml(String::new())), "xml");
410        let stream_body = Body::Stream(StreamBody {
411            stream: Arc::new(Mutex::new(None)),
412            metadata: StreamMetadata::default(),
413        });
414        assert_eq!(body_type_name(&stream_body), "stream");
415    }
416
417    #[test]
418    fn test_body_default_is_empty() {
419        let body = Body::default();
420        assert!(body.is_empty());
421    }
422
423    #[test]
424    fn test_body_from_string() {
425        let body = Body::from("hello".to_string());
426        assert_eq!(body.as_text(), Some("hello"));
427    }
428
429    #[test]
430    fn test_body_from_str() {
431        let body = Body::from("world");
432        assert_eq!(body.as_text(), Some("world"));
433    }
434
435    #[test]
436    fn test_body_from_bytes() {
437        let body = Body::from(Bytes::from_static(b"data"));
438        assert!(!body.is_empty());
439        assert!(matches!(body, Body::Bytes(_)));
440    }
441
442    #[test]
443    fn test_body_from_json() {
444        let val = serde_json::json!({"key": "value"});
445        let body = Body::from(val.clone());
446        assert!(matches!(body, Body::Json(_)));
447    }
448
449    #[tokio::test]
450    async fn test_into_bytes_from_stream() {
451        use futures::stream;
452        let chunks = vec![Ok(Bytes::from("hello ")), Ok(Bytes::from("world"))];
453        let stream = stream::iter(chunks);
454        let body = Body::Stream(StreamBody {
455            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
456            metadata: StreamMetadata::default(),
457        });
458
459        let result = body.into_bytes(100).await.unwrap();
460        assert_eq!(result, Bytes::from("hello world"));
461    }
462
463    #[tokio::test]
464    async fn test_into_bytes_limit_exceeded() {
465        use futures::stream;
466        let chunks = vec![Ok(Bytes::from("this is too long"))];
467        let stream = stream::iter(chunks);
468        let body = Body::Stream(StreamBody {
469            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
470            metadata: StreamMetadata::default(),
471        });
472
473        let result = body.into_bytes(5).await;
474        assert!(matches!(result, Err(CamelError::StreamLimitExceeded(5))));
475    }
476
477    #[tokio::test]
478    async fn test_into_bytes_already_consumed() {
479        use futures::stream;
480        let chunks = vec![Ok(Bytes::from("data"))];
481        let stream = stream::iter(chunks);
482        let body = Body::Stream(StreamBody {
483            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
484            metadata: StreamMetadata::default(),
485        });
486
487        let cloned = body.clone();
488        let _ = body.into_bytes(100).await.unwrap();
489
490        let result = cloned.into_bytes(100).await;
491        assert!(matches!(result, Err(CamelError::AlreadyConsumed)));
492    }
493
494    #[tokio::test]
495    async fn test_materialize_with_default_limit() {
496        use futures::stream;
497
498        // Small stream under limit - should succeed with default 10MB limit
499        let chunks = vec![Ok(Bytes::from("test data"))];
500        let stream = stream::iter(chunks);
501        let body = Body::Stream(StreamBody {
502            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
503            metadata: StreamMetadata::default(),
504        });
505
506        let result = body.materialize().await;
507        assert!(result.is_ok());
508        assert_eq!(result.unwrap(), Bytes::from("test data"));
509    }
510
511    #[tokio::test]
512    async fn test_materialize_non_stream_body_types() {
513        // Verify materialize() works with all body types, not just streams
514
515        // Body::Empty
516        let body = Body::Empty;
517        let result = body.materialize().await.unwrap();
518        assert!(result.is_empty());
519
520        // Body::Bytes
521        let body = Body::Bytes(Bytes::from("bytes data"));
522        let result = body.materialize().await.unwrap();
523        assert_eq!(result, Bytes::from("bytes data"));
524
525        // Body::Text
526        let body = Body::Text("text data".to_string());
527        let result = body.materialize().await.unwrap();
528        assert_eq!(result, Bytes::from("text data"));
529
530        // Body::Json
531        let body = Body::Json(serde_json::json!({"key": "value"}));
532        let result = body.materialize().await.unwrap();
533        assert_eq!(result, Bytes::from_static(br#"{"key":"value"}"#));
534
535        // Body::Xml
536        let xml = "<root><child>value</child></root>";
537        let body = Body::Xml(xml.to_string());
538        let result = body.materialize().await.unwrap();
539        assert_eq!(result, Bytes::from(xml));
540    }
541
542    #[tokio::test]
543    async fn test_materialize_exceeds_default_limit() {
544        use futures::stream;
545
546        // 11MB stream - should fail with default 10MB limit
547        let large_data = vec![0u8; 11 * 1024 * 1024];
548        let chunks = vec![Ok(Bytes::from(large_data))];
549        let stream = stream::iter(chunks);
550        let body = Body::Stream(StreamBody {
551            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
552            metadata: StreamMetadata::default(),
553        });
554
555        let result = body.materialize().await;
556        assert!(matches!(
557            result,
558            Err(CamelError::StreamLimitExceeded(10_485_760))
559        ));
560    }
561
562    #[test]
563    fn stream_variants_are_never_equal() {
564        use futures::stream;
565
566        let make_stream = || {
567            let s = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
568            Body::Stream(StreamBody {
569                stream: Arc::new(Mutex::new(Some(Box::pin(s)))),
570                metadata: StreamMetadata::default(),
571            })
572        };
573        assert_ne!(make_stream(), make_stream());
574    }
575
576    // XML body tests
577
578    #[test]
579    fn test_body_xml_as_xml() {
580        let xml = "<root><child>value</child></root>";
581        let body = Body::Xml(xml.to_string());
582        assert_eq!(body.as_xml(), Some(xml));
583    }
584
585    #[test]
586    fn test_body_non_xml_as_xml_returns_none() {
587        // Body::Text should return None for as_xml()
588        let body = Body::Text("<root/>".to_string());
589        assert_eq!(body.as_xml(), None);
590
591        // Body::Empty should return None
592        let body = Body::Empty;
593        assert_eq!(body.as_xml(), None);
594
595        // Body::Bytes should return None
596        let body = Body::Bytes(Bytes::from("<root/>"));
597        assert_eq!(body.as_xml(), None);
598
599        // Body::Json should return None
600        let body = Body::Json(serde_json::json!({"key": "value"}));
601        assert_eq!(body.as_xml(), None);
602    }
603
604    #[test]
605    fn test_body_xml_partial_eq() {
606        // Same XML content should be equal
607        let body1 = Body::Xml("a".to_string());
608        let body2 = Body::Xml("a".to_string());
609        assert_eq!(body1, body2);
610
611        // Different XML content should not be equal
612        let body1 = Body::Xml("a".to_string());
613        let body2 = Body::Xml("b".to_string());
614        assert_ne!(body1, body2);
615    }
616
617    #[test]
618    fn test_body_xml_not_equal_to_other_variants() {
619        // Body::Xml should not equal Body::Text even with same content
620        let xml_body = Body::Xml("x".to_string());
621        let text_body = Body::Text("x".to_string());
622        assert_ne!(xml_body, text_body);
623    }
624
625    #[test]
626    fn test_try_into_xml_from_text() {
627        let body = Body::Text("<root/>".to_string());
628        let result = body.try_into_xml();
629        assert!(matches!(result, Ok(Body::Xml(ref s)) if s == "<root/>"));
630    }
631
632    #[test]
633    fn test_try_into_xml_invalid_text() {
634        let body = Body::Text("not xml".to_string());
635        let result = body.try_into_xml();
636        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
637    }
638
639    #[test]
640    fn test_body_xml_clone() {
641        let original = Body::Xml("hello".to_string());
642        let cloned = original.clone();
643        assert_eq!(original, cloned);
644    }
645
646    // ---------- into_async_read tests ----------
647
648    #[tokio::test]
649    async fn test_into_async_read_empty() {
650        use tokio::io::AsyncReadExt;
651        let body = Body::Empty;
652        let mut reader = body.into_async_read().unwrap();
653        let mut buf = Vec::new();
654        reader.read_to_end(&mut buf).await.unwrap();
655        assert!(buf.is_empty());
656    }
657
658    #[tokio::test]
659    async fn test_into_async_read_bytes() {
660        use tokio::io::AsyncReadExt;
661        let body = Body::Bytes(Bytes::from("hello"));
662        let mut reader = body.into_async_read().unwrap();
663        let mut buf = Vec::new();
664        reader.read_to_end(&mut buf).await.unwrap();
665        assert_eq!(buf, b"hello");
666    }
667
668    #[tokio::test]
669    async fn test_into_async_read_text() {
670        use tokio::io::AsyncReadExt;
671        let body = Body::Text("world".to_string());
672        let mut reader = body.into_async_read().unwrap();
673        let mut buf = Vec::new();
674        reader.read_to_end(&mut buf).await.unwrap();
675        assert_eq!(buf, b"world");
676    }
677
678    #[tokio::test]
679    async fn test_into_async_read_json() {
680        use tokio::io::AsyncReadExt;
681        let body = Body::Json(serde_json::json!({"key": "val"}));
682        let mut reader = body.into_async_read().unwrap();
683        let mut buf = Vec::new();
684        reader.read_to_end(&mut buf).await.unwrap();
685        let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
686        assert_eq!(parsed["key"], "val");
687    }
688
689    #[tokio::test]
690    async fn test_into_async_read_xml() {
691        use tokio::io::AsyncReadExt;
692        let body = Body::Xml("<root/>".to_string());
693        let mut reader = body.into_async_read().unwrap();
694        let mut buf = Vec::new();
695        reader.read_to_end(&mut buf).await.unwrap();
696        assert_eq!(buf, b"<root/>");
697    }
698
699    #[tokio::test]
700    async fn test_into_async_read_stream_multichunk() {
701        use tokio::io::AsyncReadExt;
702        let chunks: Vec<Result<Bytes, CamelError>> = vec![
703            Ok(Bytes::from("foo")),
704            Ok(Bytes::from("bar")),
705            Ok(Bytes::from("baz")),
706        ];
707        let stream = futures::stream::iter(chunks);
708        let body = Body::Stream(StreamBody {
709            stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
710            metadata: StreamMetadata {
711                size_hint: None,
712                content_type: None,
713                origin: None,
714            },
715        });
716        let mut reader = body.into_async_read().unwrap();
717        let mut buf = Vec::new();
718        reader.read_to_end(&mut buf).await.unwrap();
719        assert_eq!(buf, b"foobarbaz");
720    }
721
722    #[tokio::test]
723    async fn test_into_async_read_already_consumed() {
724        use tokio::io::AsyncReadExt;
725        // Mutex holds None → stream already consumed
726        type MaybeStream = Arc<Mutex<Option<BoxStream<'static, Result<Bytes, CamelError>>>>>;
727        let arc: MaybeStream = Arc::new(Mutex::new(None));
728        let body = Body::Stream(StreamBody {
729            stream: arc,
730            metadata: StreamMetadata {
731                size_hint: None,
732                content_type: None,
733                origin: None,
734            },
735        });
736        let mut reader = body.into_async_read().unwrap();
737        let mut buf = Vec::new();
738        let result = reader.read_to_end(&mut buf).await;
739        assert!(result.is_err());
740    }
741}