Skip to main content

commonware_codec/types/
lazy.rs

1//! This module exports the [`Lazy`] type.
2
3use crate::{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    pub fn new(value: T) -> Self {
87        Self {
88            pending: None,
89            value: Some(value).into(),
90        }
91    }
92
93    /// Create a [`Lazy`] by deferring decoding of an underlying value.
94    ///
95    /// The only cost incurred when this function is called is that of copying
96    /// some bytes.
97    ///
98    /// Use [`Self::get`] to access the actual value, by decoding these bytes.
99    pub fn deferred(buf: &mut impl Buf, cfg: T::Cfg) -> Self {
100        let bytes = buf.copy_to_bytes(buf.remaining());
101        cfg_if::cfg_if! {
102            if #[cfg(feature = "std")] {
103                Self {
104                    pending: Some(Pending { bytes, cfg }),
105                    value: Default::default(),
106                }
107            } else {
108                Self {
109                    value: T::decode_cfg(bytes.clone(), &cfg).ok(),
110                    pending: Some(Pending { bytes, cfg }),
111                }
112            }
113        }
114    }
115}
116
117impl<T: Read> Lazy<T> {
118    /// Force decoding of the underlying value.
119    ///
120    /// This will return `None` only if decoding the value fails.
121    ///
122    /// This function wil incur the cost of decoding the value only once,
123    /// so there's no need to cache its output.
124    pub fn get(&self) -> Option<&T> {
125        cfg_if::cfg_if! {
126            if #[cfg(feature = "std")] {
127                self.value
128                    .get_or_init(|| {
129                        let Pending { bytes, cfg } = self
130                            .pending
131                            .as_ref()
132                            .expect("Lazy should have pending if value is not initialized");
133                        T::decode_cfg(bytes.clone(), cfg).ok()
134                    })
135                    .as_ref()
136            } else {
137                self.value.as_ref()
138            }
139        }
140    }
141}
142
143impl<T: Read + Encode> From<T> for Lazy<T> {
144    fn from(value: T) -> Self {
145        Self::new(value)
146    }
147}
148
149// # Implementing Codec.
150//
151// The strategy here is that for writing, we use the underlying bytes stored
152// in the value, and for reading, we rely on the type having a fixed size.
153
154impl<T: Read + EncodeSize> EncodeSize for Lazy<T> {
155    fn encode_size(&self) -> usize {
156        if let Some(pending) = &self.pending {
157            return pending.bytes.len();
158        }
159        self.get()
160            .expect("Lazy should have a value if pending is None")
161            .encode_size()
162    }
163}
164
165impl<T: Read + Write> Write for Lazy<T> {
166    fn write(&self, buf: &mut impl bytes::BufMut) {
167        if let Some(pending) = &self.pending {
168            // Write raw bytes without length prefix (Bytes::write adds a length prefix)
169            buf.put_slice(&pending.bytes);
170            return;
171        }
172        self.get()
173            .expect("Lazy should have a value if pending is None")
174            .write(buf);
175    }
176}
177
178impl<T: Read + FixedSize> Read for Lazy<T> {
179    type Cfg = T::Cfg;
180
181    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, crate::Error> {
182        // In this case, we can be a bit more helpful, and fail earlier, rather
183        // than deferring this error until later.
184        if buf.remaining() < T::SIZE {
185            return Err(crate::Error::EndOfBuffer);
186        }
187        Ok(Self::deferred(&mut buf.take(T::SIZE), cfg.clone()))
188    }
189}
190
191// # Forwarded Impls
192//
193// We want to provide some convenience functions which might exist on the underlying
194// value in a Lazy. To do so, we really on `get` to access that value.
195
196impl<T: Read + PartialEq> PartialEq for Lazy<T> {
197    fn eq(&self, other: &Self) -> bool {
198        self.get() == other.get()
199    }
200}
201
202impl<T: Read + Eq> Eq for Lazy<T> {}
203
204impl<T: Read + PartialOrd> PartialOrd for Lazy<T> {
205    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
206        self.get().partial_cmp(&other.get())
207    }
208}
209
210impl<T: Read + Ord> Ord for Lazy<T> {
211    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
212        self.get().cmp(&other.get())
213    }
214}
215
216impl<T: Read + Hash> Hash for Lazy<T> {
217    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
218        self.get().hash(state);
219    }
220}
221
222impl<T: Read + core::fmt::Debug> core::fmt::Debug for Lazy<T> {
223    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
224        self.get().fmt(f)
225    }
226}
227
228#[cfg(test)]
229mod test {
230    use super::Lazy;
231    use crate::{DecodeExt, Encode, FixedSize, Read, Write};
232    use proptest::prelude::*;
233
234    /// A byte that's always <= 100
235    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
236    struct Small(u8);
237
238    impl FixedSize for Small {
239        const SIZE: usize = 1;
240    }
241
242    impl Write for Small {
243        fn write(&self, buf: &mut impl bytes::BufMut) {
244            self.0.write(buf);
245        }
246    }
247
248    impl Read for Small {
249        type Cfg = ();
250
251        fn read_cfg(buf: &mut impl bytes::Buf, _cfg: &Self::Cfg) -> Result<Self, crate::Error> {
252            let byte = u8::read_cfg(buf, &())?;
253            if byte > 100 {
254                return Err(crate::Error::Invalid("Small", "value > 100"));
255            }
256            Ok(Self(byte))
257        }
258    }
259
260    impl Arbitrary for Small {
261        type Parameters = ();
262        type Strategy = BoxedStrategy<Self>;
263
264        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
265            (0..=100u8).prop_map(Small).boxed()
266        }
267    }
268
269    proptest! {
270        #[test]
271        fn test_lazy_new_eq_deferred(x: Small) {
272            let from_new = Lazy::new(x);
273            let from_deferred = Lazy::deferred(&mut x.encode(), ());
274            prop_assert_eq!(from_new, from_deferred);
275        }
276
277        #[test]
278        fn test_lazy_write_eq_direct(x: Small) {
279            let direct = x.encode();
280            let via_lazy = Lazy::new(x).encode();
281            prop_assert_eq!(direct, via_lazy);
282        }
283
284        #[test]
285        fn test_lazy_encode_consistent_across_construction(x: Small) {
286            let direct = x.encode();
287            let via_new = Lazy::new(x).encode();
288            let via_deferred = Lazy::<Small>::deferred(&mut x.encode(), ()).encode();
289            prop_assert_eq!(&direct, &via_new);
290            prop_assert_eq!(&direct, &via_deferred);
291        }
292
293        #[test]
294        fn test_lazy_read_eq_direct(byte: u8) {
295            let direct: Option<Small> = Small::decode(byte.encode()).ok();
296            let via_lazy: Option<Small> =
297                Lazy::<Small>::decode(byte.encode()).ok().and_then(|l| l.get().copied());
298            prop_assert_eq!(direct, via_lazy);
299        }
300
301        #[test]
302        fn test_lazy_cmp_eq_direct(a: Small, b: Small) {
303            let la = Lazy::new(a);
304            let lb = Lazy::new(b);
305            prop_assert_eq!(a == b, la == lb);
306            prop_assert_eq!(a < b, la < lb);
307            prop_assert_eq!(a >= b, la >= lb);
308        }
309    }
310}