//! This module exports the [`Lazy`] type. use crate::{Decode, Encode, EncodeSize, FixedSize, Read, Write}; use bytes::{Buf, Bytes}; use core::hash::Hash; #[cfg(feature = "std")] use std::sync::OnceLock; /// A type which can be deserialized lazily. /// /// This is useful when deserializing a value is expensive, and you don't want /// to immediately pay this cost. This type allows you to move this cost to a /// later point in your program, or use parallelism to spread the cost across /// computing cores. /// /// # Usage /// /// Any usage of the type requires that `T` implements [`Read`], because we need /// to know what type [`Read::Cfg`] is. /// /// ## Construction /// /// If you have a `T`, you can use [`Lazy::new`]: /// /// ``` /// # use commonware_codec::types::lazy::Lazy; /// let l = Lazy::new(4000u64); /// ``` /// /// or [`Into`]: /// /// ``` /// # use commonware_codec::types::lazy::Lazy; /// let l: Lazy = 4000u64.into(); /// ``` /// /// If you *don't* have a `T`, then you can instead create a [`Lazy`] using /// bytes and a [`Read::Cfg`]: /// /// ``` /// # use commonware_codec::{Encode, types::lazy::Lazy}; /// let l: Lazy = Lazy::deferred(&mut 4000u64.encode(), ()); /// ``` /// /// ## Consumption /// /// Given a [`Lazy`], use [`Lazy::get`] to access the value: /// /// ``` /// # use commonware_codec::{Encode, types::lazy::Lazy}; /// let l = Lazy::::deferred(&mut 4000u64.encode(), ()); /// assert_eq!(l.get(), Some(&4000u64)); /// // Does not pay the cost of deserializing again /// assert_eq!(l.get(), Some(&4000u64)); /// ``` /// /// This returns an [`Option`], because deserialization might fail. /// /// ## Traits /// /// [`Lazy`] can be serialized and deserialized, implementing [`Read`], [`Write`], /// and [`EncodeSize`], based on the underlying implementation of `T`. /// /// Furthermore, we implement [`Eq`], [`Ord`], [`Hash`] based on the implementation /// of `T` as well. These methods will force deserialization of the value. #[derive(Clone)] pub struct Lazy { /// This should only be `None` if `value` is initialized. pending: Option>, #[cfg(feature = "std")] value: OnceLock>, #[cfg(not(feature = "std"))] value: Option, } #[derive(Clone)] struct Pending { bytes: Bytes, #[cfg_attr(not(feature = "std"), allow(dead_code))] cfg: T::Cfg, } impl Lazy { // I considered calling this "now", but this was too close to "new". /// Create a [`Lazy`] using a value. pub fn new(value: T) -> Self { Self { pending: None, value: Some(value).into(), } } /// Create a [`Lazy`] by deferring decoding of an underlying value. /// /// The only cost incurred when this function is called is that of copying /// some bytes. /// /// Use [`Self::get`] to access the actual value, by decoding these bytes. pub fn deferred(buf: &mut impl Buf, cfg: T::Cfg) -> Self { let bytes = buf.copy_to_bytes(buf.remaining()); cfg_if::cfg_if! { if #[cfg(feature = "std")] { Self { pending: Some(Pending { bytes, cfg }), value: Default::default(), } } else { Self { value: T::decode_cfg(bytes.clone(), &cfg).ok(), pending: Some(Pending { bytes, cfg }), } } } } } impl Lazy { /// Force decoding of the underlying value. /// /// This will return `None` only if decoding the value fails. /// /// This function wil incur the cost of decoding the value only once, /// so there's no need to cache its output. pub fn get(&self) -> Option<&T> { cfg_if::cfg_if! { if #[cfg(feature = "std")] { self.value .get_or_init(|| { let Pending { bytes, cfg } = self .pending .as_ref() .expect("Lazy should have pending if value is not initialized"); T::decode_cfg(bytes.clone(), cfg).ok() }) .as_ref() } else { self.value.as_ref() } } } } impl From for Lazy { fn from(value: T) -> Self { Self::new(value) } } // # Implementing Codec. // // The strategy here is that for writing, we use the underlying bytes stored // in the value, and for reading, we rely on the type having a fixed size. impl EncodeSize for Lazy { fn encode_size(&self) -> usize { if let Some(pending) = &self.pending { return pending.bytes.len(); } self.get() .expect("Lazy should have a value if pending is None") .encode_size() } } impl Write for Lazy { fn write(&self, buf: &mut impl bytes::BufMut) { if let Some(pending) = &self.pending { // Write raw bytes without length prefix (Bytes::write adds a length prefix) buf.put_slice(&pending.bytes); return; } self.get() .expect("Lazy should have a value if pending is None") .write(buf); } } impl Read for Lazy { type Cfg = T::Cfg; fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result { // In this case, we can be a bit more helpful, and fail earlier, rather // than deferring this error until later. if buf.remaining() < T::SIZE { return Err(crate::Error::EndOfBuffer); } Ok(Self::deferred(&mut buf.take(T::SIZE), cfg.clone())) } } // # Forwarded Impls // // We want to provide some convenience functions which might exist on the underlying // value in a Lazy. To do so, we really on `get` to access that value. impl PartialEq for Lazy { fn eq(&self, other: &Self) -> bool { self.get() == other.get() } } impl Eq for Lazy {} impl PartialOrd for Lazy { fn partial_cmp(&self, other: &Self) -> Option { self.get().partial_cmp(&other.get()) } } impl Ord for Lazy { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.get().cmp(&other.get()) } } impl Hash for Lazy { fn hash(&self, state: &mut H) { self.get().hash(state); } } impl core::fmt::Debug for Lazy { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.get().fmt(f) } } #[cfg(test)] mod test { use super::Lazy; use crate::{DecodeExt, Encode, FixedSize, Read, Write}; use proptest::prelude::*; /// A byte that's always <= 100 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] struct Small(u8); impl FixedSize for Small { const SIZE: usize = 1; } impl Write for Small { fn write(&self, buf: &mut impl bytes::BufMut) { self.0.write(buf); } } impl Read for Small { type Cfg = (); fn read_cfg(buf: &mut impl bytes::Buf, _cfg: &Self::Cfg) -> Result { let byte = u8::read_cfg(buf, &())?; if byte > 100 { return Err(crate::Error::Invalid("Small", "value > 100")); } Ok(Self(byte)) } } impl Arbitrary for Small { type Parameters = (); type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { (0..=100u8).prop_map(Small).boxed() } } proptest! { #[test] fn test_lazy_new_eq_deferred(x: Small) { let from_new = Lazy::new(x); let from_deferred = Lazy::deferred(&mut x.encode(), ()); prop_assert_eq!(from_new, from_deferred); } #[test] fn test_lazy_write_eq_direct(x: Small) { let direct = x.encode(); let via_lazy = Lazy::new(x).encode(); prop_assert_eq!(direct, via_lazy); } #[test] fn test_lazy_encode_consistent_across_construction(x: Small) { let direct = x.encode(); let via_new = Lazy::new(x).encode(); let via_deferred = Lazy::::deferred(&mut x.encode(), ()).encode(); prop_assert_eq!(&direct, &via_new); prop_assert_eq!(&direct, &via_deferred); } #[test] fn test_lazy_read_eq_direct(byte: u8) { let direct: Option = Small::decode(byte.encode()).ok(); let via_lazy: Option = Lazy::::decode(byte.encode()).ok().and_then(|l| l.get().copied()); prop_assert_eq!(direct, via_lazy); } #[test] fn test_lazy_cmp_eq_direct(a: Small, b: Small) { let la = Lazy::new(a); let lb = Lazy::new(b); prop_assert_eq!(a == b, la == lb); prop_assert_eq!(a < b, la < lb); prop_assert_eq!(a >= b, la >= lb); } } }