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    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    fn encode_inline_size(&self) -> usize {
165        if self.pending.is_some() {
166            return 0;
167        }
168        self.get()
169            .expect("Lazy should have a value if pending is None")
170            .encode_inline_size()
171    }
172}
173
174impl<T: Read + Write> Write for Lazy<T> {
175    fn write(&self, buf: &mut impl bytes::BufMut) {
176        if let Some(pending) = &self.pending {
177            // Write raw bytes without length prefix (Bytes::write adds a length prefix)
178            buf.put_slice(&pending.bytes);
179            return;
180        }
181        self.get()
182            .expect("Lazy should have a value if pending is None")
183            .write(buf);
184    }
185
186    fn write_bufs(&self, buf: &mut impl BufsMut) {
187        if let Some(pending) = &self.pending {
188            // Write raw bytes without length prefix (Bytes::write_bufs adds a length prefix)
189            buf.push(pending.bytes.clone());
190            return;
191        }
192        self.get()
193            .expect("Lazy should have a value if pending is None")
194            .write_bufs(buf);
195    }
196}
197
198impl<T: Read + FixedSize> Read for Lazy<T> {
199    type Cfg = T::Cfg;
200
201    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, crate::Error> {
202        // In this case, we can be a bit more helpful, and fail earlier, rather
203        // than deferring this error until later.
204        if buf.remaining() < T::SIZE {
205            return Err(crate::Error::EndOfBuffer);
206        }
207        Ok(Self::deferred(&mut buf.take(T::SIZE), cfg.clone()))
208    }
209}
210
211// # Forwarded Impls
212//
213// We want to provide some convenience functions which might exist on the underlying
214// value in a Lazy. To do so, we really on `get` to access that value.
215
216impl<T: Read + PartialEq> PartialEq for Lazy<T> {
217    fn eq(&self, other: &Self) -> bool {
218        self.get() == other.get()
219    }
220}
221
222impl<T: Read + Eq> Eq for Lazy<T> {}
223
224impl<T: Read + PartialOrd> PartialOrd for Lazy<T> {
225    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
226        self.get().partial_cmp(&other.get())
227    }
228}
229
230impl<T: Read + Ord> Ord for Lazy<T> {
231    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
232        self.get().cmp(&other.get())
233    }
234}
235
236impl<T: Read + Hash> Hash for Lazy<T> {
237    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
238        self.get().hash(state);
239    }
240}
241
242impl<T: Read + core::fmt::Debug> core::fmt::Debug for Lazy<T> {
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        self.get().fmt(f)
245    }
246}
247
248#[cfg(test)]
249mod test {
250    use super::Lazy;
251    use crate::{DecodeExt, Encode, FixedSize, Read, Write};
252    use proptest::prelude::*;
253
254    /// A byte that's always <= 100
255    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
256    struct Small(u8);
257
258    impl FixedSize for Small {
259        const SIZE: usize = 1;
260    }
261
262    impl Write for Small {
263        fn write(&self, buf: &mut impl bytes::BufMut) {
264            self.0.write(buf);
265        }
266    }
267
268    impl Read for Small {
269        type Cfg = ();
270
271        fn read_cfg(buf: &mut impl bytes::Buf, _cfg: &Self::Cfg) -> Result<Self, crate::Error> {
272            let byte = u8::read_cfg(buf, &())?;
273            if byte > 100 {
274                return Err(crate::Error::Invalid("Small", "value > 100"));
275            }
276            Ok(Self(byte))
277        }
278    }
279
280    impl Arbitrary for Small {
281        type Parameters = ();
282        type Strategy = BoxedStrategy<Self>;
283
284        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
285            (0..=100u8).prop_map(Small).boxed()
286        }
287    }
288
289    proptest! {
290        #[test]
291        fn test_lazy_new_eq_deferred(x: Small) {
292            let from_new = Lazy::new(x);
293            let from_deferred = Lazy::deferred(&mut x.encode(), ());
294            prop_assert_eq!(from_new, from_deferred);
295        }
296
297        #[test]
298        fn test_lazy_write_eq_direct(x: Small) {
299            let direct = x.encode();
300            let via_lazy = Lazy::new(x).encode();
301            prop_assert_eq!(direct, via_lazy);
302        }
303
304        #[test]
305        fn test_lazy_encode_consistent_across_construction(x: Small) {
306            let direct = x.encode();
307            let via_new = Lazy::new(x).encode();
308            let via_deferred = Lazy::<Small>::deferred(&mut x.encode(), ()).encode();
309            prop_assert_eq!(&direct, &via_new);
310            prop_assert_eq!(&direct, &via_deferred);
311        }
312
313        #[test]
314        fn test_lazy_read_eq_direct(byte: u8) {
315            let direct: Option<Small> = Small::decode(byte.encode()).ok();
316            let via_lazy: Option<Small> =
317                Lazy::<Small>::decode(byte.encode()).ok().and_then(|l| l.get().copied());
318            prop_assert_eq!(direct, via_lazy);
319        }
320
321        #[test]
322        fn test_lazy_cmp_eq_direct(a: Small, b: Small) {
323            let la = Lazy::new(a);
324            let lb = Lazy::new(b);
325            prop_assert_eq!(a == b, la == lb);
326            prop_assert_eq!(a < b, la < lb);
327            prop_assert_eq!(a >= b, la >= lb);
328        }
329    }
330}