use std::pin::Pin;
use bytes::Bytes;
use futures_core::Stream;
use futures_util::StreamExt;
use http::HeaderMap;
use crate::storage::ChunkStream;
#[cfg(not(target_arch = "wasm32"))]
pub type BodyStream = Pin<Box<dyn Stream<Item = std::io::Result<BodyFrame>> + Send>>;
#[cfg(target_arch = "wasm32")]
pub type BodyStream = Pin<Box<dyn Stream<Item = std::io::Result<BodyFrame>>>>;
#[derive(Debug)]
#[non_exhaustive]
pub enum BodyFrame {
Data(Bytes),
Trailers(HeaderMap),
}
pub enum RequestBody {
Absent,
Bytes(Bytes),
Stream(BodyStream),
}
impl RequestBody {
#[must_use]
pub fn from_bytes(bytes: Bytes) -> Self {
Self::Bytes(bytes)
}
#[must_use]
pub fn empty() -> Self {
Self::Bytes(Bytes::new())
}
#[must_use]
pub fn absent() -> Self {
Self::Absent
}
#[must_use]
pub fn from_stream(stream: BodyStream) -> Self {
Self::Stream(stream)
}
#[must_use]
pub fn is_supplied(&self) -> bool {
!matches!(self, Self::Absent)
}
#[must_use]
pub fn from_chunk_stream(stream: ChunkStream) -> Self {
match stream {
ChunkStream::Buffered(bytes) => Self::Bytes(bytes),
ChunkStream::Stream(stream) => {
Self::Stream(Box::pin(stream.map(|chunk| chunk.map(BodyFrame::Data))))
}
}
}
}
impl std::fmt::Debug for RequestBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequestBody::Absent => write!(f, "RequestBody::Absent"),
RequestBody::Bytes(bytes) => write!(f, "RequestBody::Bytes({} bytes)", bytes.len()),
RequestBody::Stream(_) => write!(f, "RequestBody::Stream(..)"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::ByteStream;
use bytes::Bytes;
use futures::StreamExt;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
#[test]
fn buffered_chunk_stream_becomes_buffered_request_body() {
let body =
RequestBody::from_chunk_stream(ChunkStream::from_bytes(Bytes::from_static(b"abc")));
assert!(matches!(body, RequestBody::Bytes(bytes) if bytes == Bytes::from_static(b"abc")));
}
#[test]
fn empty_chunk_stream_becomes_supplied_empty_request_body() {
let body = RequestBody::from_chunk_stream(ChunkStream::empty());
assert!(matches!(&body, RequestBody::Bytes(bytes) if bytes.is_empty()));
assert!(body.is_supplied());
}
#[test]
fn supplied_body_presence_does_not_poll_streams() {
let polled = Arc::new(AtomicBool::new(false));
let polled_for_stream = Arc::clone(&polled);
let stream: BodyStream = Box::pin(futures::stream::once(async move {
polled_for_stream.store(true, Ordering::SeqCst);
Ok(BodyFrame::Data(Bytes::from_static(b"abc")))
}));
let body = RequestBody::from_stream(stream);
assert!(body.is_supplied());
assert!(!polled.load(Ordering::SeqCst));
}
#[tokio::test]
async fn streamed_chunk_stream_becomes_data_frames() {
let stream: ByteStream = Box::pin(futures::stream::iter([
Ok(Bytes::from_static(b"ab")),
Ok(Bytes::from_static(b"cd")),
]));
let RequestBody::Stream(mut body) =
RequestBody::from_chunk_stream(ChunkStream::from_stream(stream))
else {
panic!("expected streamed request body");
};
let first = body.next().await.unwrap().unwrap();
let second = body.next().await.unwrap().unwrap();
assert!(matches!(first, BodyFrame::Data(bytes) if bytes == Bytes::from_static(b"ab")));
assert!(matches!(second, BodyFrame::Data(bytes) if bytes == Bytes::from_static(b"cd")));
}
}