Skip to main content

link_r/
payload.rs

1// Author: support@lituus.io
2// Copyright: lituus-io, all rights reserved
3//
4//! Zero-copy document payloads.
5//!
6//! Mirrors the house `GetPayload` pattern: a fetch result can be a borrowed slice,
7//! a memory-mapped view, an owned [`Bytes`] buffer, or a byte stream — whichever
8//! avoids a copy for the originating source. Consumers that need contiguous bytes
9//! call [`DocPayload::into_bytes`]; link-extraction and keyword distillation read
10//! [`DocPayload::as_slice`] when the payload is already in memory.
11
12use crate::error::Result;
13use bytes::Bytes;
14use futures::Stream;
15use std::fmt;
16use std::pin::Pin;
17
18/// A boxed byte stream. The box wraps the *stream* once (not each item), keeping
19/// the per-chunk path allocation-free; this matches the house `ByteStream` alias.
20pub type ByteStream<'a> = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send + 'a>>;
21
22/// A borrowed view into a memory-mapped region, tied to the lifetime of the owner.
23///
24/// The owner (an mmap or the index file view) guarantees the bytes outlive `'a`;
25/// this newtype simply documents the zero-copy intent at call sites.
26#[derive(Clone, Copy)]
27pub struct MmapView<'a>(&'a [u8]);
28
29impl<'a> MmapView<'a> {
30    /// Wrap a borrowed slice as an mmap view.
31    #[must_use]
32    pub fn new(bytes: &'a [u8]) -> Self {
33        Self(bytes)
34    }
35
36    /// The underlying bytes.
37    #[must_use]
38    pub fn as_slice(&self) -> &'a [u8] {
39        self.0
40    }
41}
42
43impl fmt::Debug for MmapView<'_> {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        f.debug_struct("MmapView")
46            .field("len", &self.0.len())
47            .finish()
48    }
49}
50
51impl AsRef<[u8]> for MmapView<'_> {
52    fn as_ref(&self) -> &[u8] {
53        self.0
54    }
55}
56
57/// The body of a fetched document, in whichever representation avoids a copy.
58pub enum DocPayload<'a> {
59    /// Borrowed directly from the caller's buffer — zero copy.
60    Borrowed(&'a [u8]),
61    /// A memory-mapped view (e.g. a local file) — zero copy.
62    Mapped(MmapView<'a>),
63    /// An owned, reference-counted buffer (e.g. a network fetch).
64    Owned(Bytes),
65    /// A lazily-produced byte stream for large bodies.
66    Stream(ByteStream<'a>),
67}
68
69impl DocPayload<'_> {
70    /// Borrow the bytes if they are already materialized in memory.
71    ///
72    /// Returns `None` for [`DocPayload::Stream`], which must be consumed via
73    /// [`DocPayload::into_bytes`].
74    #[must_use]
75    pub fn as_slice(&self) -> Option<&[u8]> {
76        match self {
77            Self::Borrowed(b) => Some(b),
78            Self::Mapped(m) => Some(m.as_slice()),
79            Self::Owned(b) => Some(b),
80            Self::Stream(_) => None,
81        }
82    }
83
84    /// The byte length if known without consuming a stream.
85    #[must_use]
86    pub fn len(&self) -> Option<usize> {
87        self.as_slice().map(<[u8]>::len)
88    }
89
90    /// Whether an in-memory payload is empty (always `None` for streams).
91    #[must_use]
92    pub fn is_empty(&self) -> Option<bool> {
93        self.as_slice().map(<[u8]>::is_empty)
94    }
95
96    /// Collect the payload into a contiguous [`Bytes`], draining a stream if needed.
97    pub async fn into_bytes(self) -> Result<Bytes> {
98        match self {
99            Self::Borrowed(b) => Ok(Bytes::copy_from_slice(b)),
100            Self::Mapped(m) => Ok(Bytes::copy_from_slice(m.as_slice())),
101            Self::Owned(b) => Ok(b),
102            Self::Stream(mut s) => {
103                use futures::StreamExt;
104                let mut buf = Vec::new();
105                while let Some(chunk) = s.next().await {
106                    buf.extend_from_slice(&chunk?);
107                }
108                Ok(Bytes::from(buf))
109            }
110        }
111    }
112}
113
114impl fmt::Debug for DocPayload<'_> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::Borrowed(b) => f.debug_tuple("Borrowed").field(&b.len()).finish(),
118            Self::Mapped(m) => f.debug_tuple("Mapped").field(&m.as_slice().len()).finish(),
119            Self::Owned(b) => f.debug_tuple("Owned").field(&b.len()).finish(),
120            Self::Stream(_) => f.write_str("Stream(..)"),
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn borrowed_and_owned_expose_slices() {
131        let data = b"hello world";
132        let borrowed = DocPayload::Borrowed(data);
133        assert_eq!(borrowed.as_slice(), Some(&data[..]));
134        assert_eq!(borrowed.len(), Some(11));
135        assert_eq!(borrowed.is_empty(), Some(false));
136
137        let owned = DocPayload::Owned(Bytes::from_static(data));
138        assert_eq!(owned.as_slice(), Some(&data[..]));
139    }
140
141    #[test]
142    fn mapped_view_roundtrips() {
143        let data = [1u8, 2, 3, 4];
144        let view = MmapView::new(&data);
145        assert_eq!(view.as_slice(), &data);
146        assert_eq!(view.as_ref(), &data);
147    }
148
149    #[test]
150    fn stream_has_no_in_memory_slice() {
151        let s: ByteStream<'_> = Box::pin(futures::stream::empty());
152        let payload = DocPayload::Stream(s);
153        assert_eq!(payload.as_slice(), None);
154        assert_eq!(payload.len(), None);
155    }
156
157    #[tokio::test]
158    async fn into_bytes_drains_a_stream() {
159        let chunks = vec![
160            Ok(Bytes::from_static(b"foo")),
161            Ok(Bytes::from_static(b"bar")),
162        ];
163        let s: ByteStream<'_> = Box::pin(futures::stream::iter(chunks));
164        let bytes = DocPayload::Stream(s).into_bytes().await.unwrap();
165        assert_eq!(&bytes[..], b"foobar");
166    }
167
168    #[tokio::test]
169    async fn into_bytes_on_borrowed_copies() {
170        let bytes = DocPayload::Borrowed(b"abc").into_bytes().await.unwrap();
171        assert_eq!(&bytes[..], b"abc");
172    }
173}