unc_sdk/store/lazy_option/
mod.rs

1mod impls;
2
3use borsh::{BorshDeserialize, BorshSerialize};
4use once_cell::unsync::OnceCell;
5use unc_sdk_macros::unc;
6
7use crate::env;
8use crate::store::lazy::{load_and_deserialize, serialize_and_store};
9use crate::utils::{CacheEntry, EntryState};
10use crate::IntoStorageKey;
11
12/// An persistent lazily loaded option, that stores a `value` in the storage when `Some(value)`
13/// is set, and not when `None` is set. `LazyOption` also [`Deref`]s into [`Option`] so we get
14/// all its APIs for free.
15///
16/// This will only write to the underlying store if the value has changed, and will only read the
17/// existing value from storage once.
18///
19/// # Examples
20/// ```
21/// use unc_sdk::store::LazyOption;
22///
23/// let mut a = LazyOption::new(b"a", None);
24/// assert!(a.is_none());
25///
26/// *a = Some("new value".to_owned());
27/// assert_eq!(a.get(), &Some("new value".to_owned()));
28///
29/// // Using Option::replace:
30/// let old_str = a.replace("new new value".to_owned());
31/// assert_eq!(old_str, Some("new value".to_owned()));
32/// assert_eq!(a.get(), &Some("new new value".to_owned()));
33/// ```
34/// [`Deref`]: std::ops::Deref
35#[unc(inside_uncsdk)]
36pub struct LazyOption<T>
37where
38    T: BorshSerialize,
39{
40    /// Key bytes to index the contract's storage.
41    prefix: Box<[u8]>,
42
43    /// Cached value which is lazily loaded and deserialized from storage.
44    #[borsh(skip, bound(deserialize = ""))] // removes `core::default::Default` bound from T
45    cache: OnceCell<CacheEntry<T>>,
46}
47
48impl<T> LazyOption<T>
49where
50    T: BorshSerialize,
51{
52    /// Create a new lazy option with the given `prefix` and the initial value.
53    ///
54    /// This prefix can be anything that implements [`IntoStorageKey`]. The prefix is used when
55    /// storing and looking up values in storage to ensure no collisions with other collections.
56    pub fn new<S>(prefix: S, value: Option<T>) -> Self
57    where
58        S: IntoStorageKey,
59    {
60        let cache = match value {
61            Some(value) => CacheEntry::new_modified(Some(value)),
62            None => CacheEntry::new_cached(None),
63        };
64
65        Self { prefix: prefix.into_storage_key().into_boxed_slice(), cache: OnceCell::from(cache) }
66    }
67
68    /// Updates the value with a new value. This does not load the current value from storage.
69    pub fn set(&mut self, value: Option<T>) {
70        if let Some(v) = self.cache.get_mut() {
71            *v.value_mut() = value;
72        } else {
73            self.cache
74                .set(CacheEntry::new_modified(value))
75                // Cache is checked to not be filled in if statement above
76                .unwrap_or_else(|_| env::abort());
77        }
78    }
79
80    /// Writes any changes to the value to storage. This will automatically be done when the
81    /// value is dropped through [`Drop`] so this should only be used when the changes need to be
82    /// reflected in the underlying storage before then.
83    pub fn flush(&mut self) {
84        if let Some(v) = self.cache.get_mut() {
85            if !v.is_modified() {
86                return;
87            }
88
89            match v.value().as_ref() {
90                Some(value) => serialize_and_store(&self.prefix, value),
91                None => {
92                    env::storage_remove(&self.prefix);
93                }
94            }
95
96            // Replaces cache entry state to cached because the value in memory matches the
97            // stored value. This avoids writing the same value twice.
98            v.replace_state(EntryState::Cached);
99        }
100    }
101}
102
103impl<T> LazyOption<T>
104where
105    T: BorshSerialize + BorshDeserialize,
106{
107    /// Returns a reference to the lazily loaded optional.
108    /// The load from storage only happens once, and if the value is already cached, it will not
109    /// be reloaded.
110    pub fn get(&self) -> &Option<T> {
111        let entry = self.cache.get_or_init(|| load_and_deserialize(&self.prefix));
112        entry.value()
113    }
114
115    /// Returns a reference to the lazily loaded optional.
116    /// The load from storage only happens once, and if the value is already cached, it will not
117    /// be reloaded.
118    pub fn get_mut(&mut self) -> &mut Option<T> {
119        self.cache.get_or_init(|| load_and_deserialize(&self.prefix));
120        let entry = self.cache.get_mut().unwrap_or_else(|| env::abort());
121        entry.value_mut()
122    }
123}
124
125#[cfg(not(target_arch = "wasm32"))]
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    pub fn test_lazy_option() {
132        let mut a = LazyOption::new(b"a", None);
133        assert!(a.is_none());
134        assert!(!env::storage_has_key(b"a"));
135
136        // Check value has been set in via cache:
137        a.set(Some(42u32));
138        assert!(a.is_some());
139        assert_eq!(a.get(), &Some(42));
140
141        // Flushing, then check if storage has been set:
142        a.flush();
143        assert!(env::storage_has_key(b"a"));
144        assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 42);
145
146        // New value is set
147        *a = Some(49u32);
148        assert!(a.is_some());
149        assert_eq!(a.get(), &Some(49));
150
151        // Testing `Option::replace`
152        let old = a.replace(69u32);
153        assert!(a.is_some());
154        assert_eq!(old, Some(49));
155
156        // Testing `Option::take` deletes from internal storage
157        let taken = a.take();
158        assert!(a.is_none());
159        assert_eq!(taken, Some(69));
160
161        // `flush`/`drop` after `Option::take` should remove from storage:
162        drop(a);
163        assert!(!env::storage_has_key(b"a"));
164    }
165
166    #[test]
167    pub fn test_debug() {
168        let mut lazy_option = LazyOption::new(b"m", None);
169        if cfg!(feature = "expensive-debug") {
170            assert_eq!(format!("{:?}", lazy_option), "None");
171        } else {
172            assert_eq!(
173                format!("{:?}", lazy_option),
174                "LazyOption { storage_key: [109], cache: Some(CacheEntry { value: None, state: Cached }) }"
175            );
176        }
177
178        *lazy_option = Some(1u8);
179        if cfg!(feature = "expensive-debug") {
180            assert_eq!(format!("{:?}", lazy_option), "Some(1)");
181        } else {
182            assert_eq!(
183                format!("{:?}", lazy_option),
184                "LazyOption { storage_key: [109], cache: Some(CacheEntry { value: Some(1), state: Modified }) }"
185            );
186        }
187
188        // Serialize and deserialize to simulate storing and loading.
189        let serialized = borsh::to_vec(&lazy_option).unwrap();
190        drop(lazy_option);
191        let lazy_option = LazyOption::<u8>::try_from_slice(&serialized).unwrap();
192        if cfg!(feature = "expensive-debug") {
193            assert_eq!(format!("{:?}", lazy_option), "Some(1)");
194        } else {
195            assert_eq!(
196                format!("{:?}", lazy_option),
197                "LazyOption { storage_key: [109], cache: None }"
198            );
199        }
200    }
201}