Skip to main content

oxihttp_core/
body.rs

1//! HTTP body abstraction for the OxiHTTP stack.
2//!
3//! Provides a unified `Body` enum that supports empty bodies, full (in-memory)
4//! bodies, and streaming bodies. Implements `http_body::Body` for integration
5//! with hyper's transport layer.
6
7use bytes::Bytes;
8use futures_core::Stream;
9use http_body::Frame;
10use pin_project_lite::pin_project;
11use std::pin::Pin;
12use std::task::{Context, Poll};
13
14use crate::OxiHttpError;
15
16/// The body type for requests and responses in the OxiHTTP stack.
17///
18/// This enum supports three modes:
19/// - `Empty`: No body content.
20/// - `Full`: A body fully buffered in memory as `Bytes`.
21/// - `Stream`: A streaming body backed by an async byte stream.
22#[derive(Debug, Default)]
23pub enum Body {
24    /// An empty body with no content.
25    #[default]
26    Empty,
27    /// A body fully loaded in memory.
28    Full(FullBody),
29    /// A streaming body. The inner stream is opaque.
30    Stream(StreamBody),
31}
32
33impl Body {
34    /// Create an empty body.
35    pub fn empty() -> Self {
36        Self::Empty
37    }
38
39    /// Create a body from bytes already in memory.
40    pub fn full(data: impl Into<Bytes>) -> Self {
41        Self::Full(FullBody {
42            data: Some(data.into()),
43        })
44    }
45
46    /// Create a streaming body from a pinned async byte-chunk stream.
47    pub fn stream(inner: Pin<Box<dyn Stream<Item = Result<Bytes, OxiHttpError>> + Send>>) -> Self {
48        Self::Stream(StreamBody { inner })
49    }
50
51    /// Returns the content length if known.
52    ///
53    /// Returns `Some(0)` for empty bodies, `Some(n)` for full bodies,
54    /// and `None` for streams (unknown length).
55    pub fn content_length(&self) -> Option<u64> {
56        match self {
57            Self::Empty => Some(0),
58            Self::Full(full) => full.data.as_ref().map(|d| d.len() as u64),
59            Self::Stream(_) => None,
60        }
61    }
62
63    /// Convert this `Body` into a `PinnedBody` suitable for use with `http_body::Body`.
64    pub fn into_pinned(self) -> PinnedBody {
65        PinnedBody::from(self)
66    }
67}
68
69impl From<()> for Body {
70    fn from(_: ()) -> Self {
71        Self::Empty
72    }
73}
74
75impl From<Bytes> for Body {
76    fn from(b: Bytes) -> Self {
77        if b.is_empty() {
78            Self::Empty
79        } else {
80            Self::full(b)
81        }
82    }
83}
84
85impl From<Vec<u8>> for Body {
86    fn from(v: Vec<u8>) -> Self {
87        Self::from(Bytes::from(v))
88    }
89}
90
91impl From<String> for Body {
92    fn from(s: String) -> Self {
93        Self::from(Bytes::from(s))
94    }
95}
96
97impl From<&'static str> for Body {
98    fn from(s: &'static str) -> Self {
99        Self::from(Bytes::from_static(s.as_bytes()))
100    }
101}
102
103impl From<&'static [u8]> for Body {
104    fn from(s: &'static [u8]) -> Self {
105        Self::from(Bytes::from_static(s))
106    }
107}
108
109/// A full (in-memory) body.
110#[derive(Debug)]
111pub struct FullBody {
112    data: Option<Bytes>,
113}
114
115/// A streaming body wrapping an async byte stream.
116pub struct StreamBody {
117    inner: Pin<Box<dyn Stream<Item = Result<Bytes, OxiHttpError>> + Send>>,
118}
119
120impl std::fmt::Debug for StreamBody {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.debug_struct("StreamBody").finish()
123    }
124}
125
126// ---------------------------------------------------------------------------
127// http_body::Body implementation via PinnedBody
128// ---------------------------------------------------------------------------
129
130pin_project! {
131    /// A pinnable body type that implements `http_body::Body`.
132    ///
133    /// Created from `Body` via `Body::into_pinned()` or `PinnedBody::from(body)`.
134    #[project = PinnedBodyProj]
135    pub enum PinnedBody {
136        /// Empty body variant.
137        Empty,
138        /// Full (in-memory) body variant.
139        Full { data: Option<Bytes> },
140        /// Streaming body variant.
141        Stream { #[pin] inner: Pin<Box<dyn Stream<Item = Result<Bytes, OxiHttpError>> + Send>> },
142    }
143}
144
145impl From<Body> for PinnedBody {
146    fn from(body: Body) -> Self {
147        match body {
148            Body::Empty => PinnedBody::Empty,
149            Body::Full(f) => PinnedBody::Full { data: f.data },
150            Body::Stream(s) => PinnedBody::Stream { inner: s.inner },
151        }
152    }
153}
154
155impl http_body::Body for PinnedBody {
156    type Data = Bytes;
157    type Error = OxiHttpError;
158
159    fn poll_frame(
160        self: Pin<&mut Self>,
161        cx: &mut Context<'_>,
162    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
163        match self.project() {
164            PinnedBodyProj::Empty => Poll::Ready(None),
165            PinnedBodyProj::Full { data } => {
166                let chunk = data.take();
167                match chunk {
168                    Some(d) if !d.is_empty() => Poll::Ready(Some(Ok(Frame::data(d)))),
169                    _ => Poll::Ready(None),
170                }
171            }
172            PinnedBodyProj::Stream { mut inner } => match inner.as_mut().poll_next(cx) {
173                Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(Frame::data(chunk)))),
174                Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
175                Poll::Ready(None) => Poll::Ready(None),
176                Poll::Pending => Poll::Pending,
177            },
178        }
179    }
180
181    fn is_end_stream(&self) -> bool {
182        match self {
183            PinnedBody::Empty => true,
184            PinnedBody::Full { data } => data.is_none(),
185            PinnedBody::Stream { .. } => false,
186        }
187    }
188
189    fn size_hint(&self) -> http_body::SizeHint {
190        match self {
191            PinnedBody::Empty => http_body::SizeHint::with_exact(0),
192            PinnedBody::Full { data } => match data {
193                Some(d) => http_body::SizeHint::with_exact(d.len() as u64),
194                None => http_body::SizeHint::with_exact(0),
195            },
196            PinnedBody::Stream { .. } => http_body::SizeHint::default(),
197        }
198    }
199}