Skip to main content

commonware_codec/types/
lazy.rs

1//! This module exports the [`Lazy`] type.
2
3use crate::{BufsMut, Decode, Encode, EncodeSize, FixedSize, Read, Write};
4use bytes::{Buf, Bytes};
5use core::hash::Hash;
6#[cfg(feature = "std")]
7use std::sync::OnceLock;
8
9/// A type which can be deserialized lazily.
10///
11/// This is useful when deserializing a value is expensive, and you don't want
12/// to immediately pay this cost. This type allows you to move this cost to a
13/// later point in your program, or use parallelism to spread the cost across
14/// computing cores.
15///
16/// # Usage
17///
18/// Any usage of the type requires that `T` implements [`Read`], because we need
19/// to know what type [`Read::Cfg`] is.
20///
21/// ## Construction
22///
23/// If you have a `T`, you can use [`Lazy::new`]:
24///
25/// ```
26/// # use commonware_codec::types::lazy::Lazy;
27/// let l = Lazy::new(4000u64);
28/// ```
29///
30/// or [`Into`]:
31///
32/// ```
33/// # use commonware_codec::types::lazy::Lazy;
34/// let l: Lazy<u64> = 4000u64.into();
35/// ```
36///
37/// If you *don't* have a `T`, then you can instead create a [`Lazy`] using
38/// bytes and a [`Read::Cfg`]:
39///
40/// ```
41/// # use commonware_codec::{Encode, types::lazy::Lazy};
42/// let l: Lazy<u64> = Lazy::deferred(&mut 4000u64.encode(), ());
43/// ```
44///
45/// ## Consumption
46///
47/// Given a [`Lazy`], use [`Lazy::get`] to access the value:
48///
49/// ```
50/// # use commonware_codec::{Encode, types::lazy::Lazy};
51/// let l = Lazy::<u64>::deferred(&mut 4000u64.encode(), ());
52/// assert_eq!(l.get(), Some(&4000u64));
53/// // Does not pay the cost of deserializing again
54/// assert_eq!(l.get(), Some(&4000u64));
55/// ```
56///
57/// This returns an [`Option`], because deserialization might fail.
58///
59/// ## Traits
60///
61/// [`Lazy`] can be serialized and deserialized, implementing [`Read`], [`Write`],
62/// and [`EncodeSize`], based on the underlying implementation of `T`.
63///
64/// Furthermore, we implement [`Eq`], [`Ord`], [`Hash`] based on the implementation
65/// of `T` as well. These methods will force deserialization of the value.
66#[derive(Clone)]
67pub struct Lazy<T: Read> {
68    /// This should only be `None` if `value` is initialized.
69    pending: Option<Pending<T>>,
70    #[cfg(feature = "std")]
71    value: OnceLock<Option<T>>,
72    #[cfg(not(feature = "std"))]
73    value: Option<T>,
74}
75
76#[derive(Clone)]
77struct Pending<T: Read> {
78    bytes: Bytes,
79    #[cfg_attr(not(feature = "std"), allow(dead_code))]
80    cfg: T::Cfg,
81}
82
83impl<T: Read> Lazy<T> {
84    // I considered calling this "now", but this was too close to "new".
85    /// Create a [`Lazy`] using a value.
86    #[cfg(feature = "std")]
87    pub fn new(value: T) -> Self {
88        Self {
89            pending: None,
90            value: OnceLock::from(Some(value)),
91        }
92    }
93
94    /// Create a [`Lazy`] using a value.
95    #[cfg(not(feature = "std"))]
96    pub const fn new(value: T) -> Self {
97        Self {
98            pending: None,
99            value: Some(value),
100        }
101    }
102
103    /// Create a [`Lazy`] by deferring decoding of an underlying value.
104    ///
105    /// The only cost incurred when this function is called is that of copying
106    /// some bytes.
107    ///
108    /// Use [`Self::get`] to access the actual value, by decoding these bytes.
109    pub fn deferred(buf: &mut impl Buf, cfg: T::Cfg) -> Self {
110        let bytes = buf.copy_to_bytes(buf.remaining());
111        cfg_if::cfg_if! {
112            if #[cfg(feature = "std")] {
113                Self {
114                    pending: Some(Pending { bytes, cfg }),
115                    value: Default::default(),
116                }
117            } else {
118                Self {
119                    value: T::decode_cfg(bytes.as_ref(), &cfg).ok(),
120                    pending: Some(Pending { bytes, cfg }),
121                }
122            }
123        }
124    }
125}
126
127impl<T: Read> Lazy<T> {
128    /// Force decoding of the underlying value.
129    ///
130    /// This will return `None` only if decoding the value fails.
131    ///
132    /// This function wil incur the cost of decoding the value only once,
133    /// so there's no need to cache its output.
134    #[cfg(feature = "std")]
135    pub fn get(&self) -> Option<&T> {
136        self.value
137            .get_or_init(|| {
138                let Pending { bytes, cfg } = self
139                    .pending
140                    .as_ref()
141                    .expect("Lazy should have pending if value is not initialized");
142                T::decode_cfg(bytes.as_ref(), cfg).ok()
143            })
144            .as_ref()
145    }
146
147    /// Returns a reference to the underlying value, or `None` if decoding failed.
148    #[cfg(not(feature = "std"))]
149    pub const fn get(&self) -> Option<&T> {
150        self.value.as_ref()
151    }
152}
153
154impl<T: Read + Encode> From<T> for Lazy<T> {
155    fn from(value: T) -> Self {
156        Self::new(value)
157    }
158}
159
160// # Implementing Codec.
161//
162// The strategy here is that for writing, we use the underlying bytes stored
163// in the value, and for reading, we rely on the type having a fixed size.
164
165impl<T: Read + EncodeSize> EncodeSize for Lazy<T> {
166    fn encode_size(&self) -> usize {
167        if let Some(pending) = &self.pending {
168            return pending.bytes.len();
169        }
170        self.get()
171            .expect("Lazy should have a value if pending is None")
172            .encode_size()
173    }
174
175    fn encode_inline_size(&self) -> usize {
176        if self.pending.is_some() {
177            return 0;
178        }
179        self.get()
180            .expect("Lazy should have a value if pending is None")
181            .encode_inline_size()
182    }
183}
184
185impl<T: Read + Write> Write for Lazy<T> {
186    fn write(&self, buf: &mut impl bytes::BufMut) {
187        if let Some(pending) = &self.pending {
188            // Write raw bytes without length prefix (Bytes::write adds a length prefix)
189            buf.put_slice(&pending.bytes);
190            return;
191        }
192        self.get()
193            .expect("Lazy should have a value if pending is None")
194            .write(buf);
195    }
196
197    fn write_bufs(&self, buf: &mut impl BufsMut) {
198        if let Some(pending) = &self.pending {
199            // Write raw bytes without length prefix (Bytes::write_bufs adds a length prefix)
200            buf.push(pending.bytes.clone());
201            return;
202        }
203        self.get()
204            .expect("Lazy should have a value if pending is None")
205            .write_bufs(buf);
206    }
207}
208
209impl<T: Read + FixedSize> Read for Lazy<T> {
210    type Cfg = T::Cfg;
211
212    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, crate::Error> {
213        // In this case, we can be a bit more helpful, and fail earlier, rather
214        // than deferring this error until later.
215        if buf.remaining() < T::SIZE {
216            return Err(crate::Error::EndOfBuffer);
217        }
218        Ok(Self::deferred(&mut buf.take(T::SIZE), cfg.clone()))
219    }
220}
221
222// # Forwarded Impls
223//
224// We want to provide some convenience functions which might exist on the underlying
225// value in a Lazy. To do so, we really on `get` to access that value.
226
227impl<T: Read + PartialEq> PartialEq for Lazy<T> {
228    fn eq(&self, other: &Self) -> bool {
229        self.get() == other.get()
230    }
231}
232
233impl<T: Read + Eq> Eq for Lazy<T> {}
234
235impl<T: Read + PartialOrd> PartialOrd for Lazy<T> {
236    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
237        self.get().partial_cmp(&other.get())
238    }
239}
240
241impl<T: Read + Ord> Ord for Lazy<T> {
242    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
243        self.get().cmp(&other.get())
244    }
245}
246
247impl<T: Read + Hash> Hash for Lazy<T> {
248    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
249        self.get().hash(state);
250    }
251}
252
253impl<T: Read + core::fmt::Debug> core::fmt::Debug for Lazy<T> {
254    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
255        self.get().fmt(f)
256    }
257}
258
259#[cfg(test)]
260mod test {
261    use super::Lazy;
262    use crate::{DecodeExt, Encode, FixedSize, Read, Write};
263    use proptest::prelude::*;
264
265    /// A byte that's always <= 100
266    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
267    struct Small(u8);
268
269    impl FixedSize for Small {
270        const SIZE: usize = 1;
271    }
272
273    impl Write for Small {
274        fn write(&self, buf: &mut impl bytes::BufMut) {
275            self.0.write(buf);
276        }
277    }
278
279    impl Read for Small {
280        type Cfg = ();
281
282        fn read_cfg(buf: &mut impl bytes::Buf, _cfg: &Self::Cfg) -> Result<Self, crate::Error> {
283            let byte = u8::read_cfg(buf, &())?;
284            if byte > 100 {
285                return Err(crate::Error::Invalid("Small", "value > 100"));
286            }
287            Ok(Self(byte))
288        }
289    }
290
291    impl Arbitrary for Small {
292        type Parameters = ();
293        type Strategy = BoxedStrategy<Self>;
294
295        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
296            (0..=100u8).prop_map(Small).boxed()
297        }
298    }
299
300    proptest! {
301        #[test]
302        fn test_lazy_new_eq_deferred(x: Small) {
303            let from_new = Lazy::new(x);
304            let from_deferred = Lazy::deferred(&mut x.encode(), ());
305            prop_assert_eq!(from_new, from_deferred);
306        }
307
308        #[test]
309        fn test_lazy_write_eq_direct(x: Small) {
310            let direct = x.encode();
311            let via_lazy = Lazy::new(x).encode();
312            prop_assert_eq!(direct, via_lazy);
313        }
314
315        #[test]
316        fn test_lazy_encode_consistent_across_construction(x: Small) {
317            let direct = x.encode();
318            let via_new = Lazy::new(x).encode();
319            let via_deferred = Lazy::<Small>::deferred(&mut x.encode(), ()).encode();
320            prop_assert_eq!(&direct, &via_new);
321            prop_assert_eq!(&direct, &via_deferred);
322        }
323
324        #[test]
325        fn test_lazy_read_eq_direct(byte: u8) {
326            let direct: Option<Small> = Small::decode(byte.encode()).ok();
327            let via_lazy: Option<Small> =
328                Lazy::<Small>::decode(byte.encode()).ok().and_then(|l| l.get().copied());
329            prop_assert_eq!(direct, via_lazy);
330        }
331
332        #[test]
333        fn test_lazy_cmp_eq_direct(a: Small, b: Small) {
334            let la = Lazy::new(a);
335            let lb = Lazy::new(b);
336            prop_assert_eq!(a == b, la == lb);
337            prop_assert_eq!(a < b, la < lb);
338            prop_assert_eq!(a >= b, la >= lb);
339        }
340    }
341}