From 54ac7ebe1edc7cc2abffa621a87e8f216af288af Mon Sep 17 00:00:00 2001 From: Islam El-Ashi Date: Thu, 26 Jun 2025 15:11:15 +0200 Subject: [PATCH 1/3] docs: improve documentation of `Cell` --- README.md | 2 +- .../introduction/available-data-structures.md | 11 +-- src/cell.rs | 90 +++++++++++++++++-- src/cell/tests.rs | 18 ++++ 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 607da382..fb268cee 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ This library aims to simplify managing data structures directly in stable memory ## Available Data Structures +- [Cell]: A serializable value - [BTreeMap]: A Key-Value store - [BTreeSet]: A set of unique elements - [Vec]: A growable array - [Log]: An append-only list of variable-size entries -- [Cell]: A serializable value - [MinHeap]: A priority queue. ## Tutorials diff --git a/docs/src/introduction/available-data-structures.md b/docs/src/introduction/available-data-structures.md index 64d95b58..ccff120c 100644 --- a/docs/src/introduction/available-data-structures.md +++ b/docs/src/introduction/available-data-structures.md @@ -2,8 +2,9 @@ The library provides several stable data structures: -* **BTreeMap**: A key-value store that maintains keys in sorted order -* **Vec**: A growable array -* **Log**: An append-only list of variable-size entries -* **Cell**: A serializable value -* **MinHeap**: A priority queue +* [Cell](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/cell/struct.Cell.html): For small single values that change rarely +* [BTreeMap](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/btreemap/struct.BTreeMap.html): A key-value store that maintains keys in sorted order +* [BTreeSet](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/btreeset/struct.BTreeSet.html): A set of unique elements +* [Vec](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/vec/struct.Vec.html): A growable array +* [Log](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/log/struct.Log.html): An append-only list of variable-size entries +* [MinHeap](https://docs.rs/ic-stable-structures/latest/ic_stable_structures/min_heap/struct.MinHeap.html): A priority queue diff --git a/src/cell.rs b/src/cell.rs index 33f672dc..0fc60d4f 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -81,14 +81,90 @@ impl From for InitError { } } -/// Represents a serializable value stored in the stable memory. -/// It has semantics similar to "stable variables" in Motoko and share the same limitations. -/// The main difference is that Cell writes its value to the memory on each assignment, not just in -/// upgrade hooks. -/// You should use cells only for small (up to a few MiB) values to keep upgrades safe. +/// Represents a value stored in stable memory. +/// +/// A `Cell` stores a single value directly in stable memory and provides immediate persistence +/// on every write operation. This makes it ideal for configuration values, metadata, or any +/// small state that needs to survive canister upgrades. /// -/// Cell is a good choice for small read-only configuration values set once on canister installation -/// and rarely updated. +/// You should use cells only for small (up to a few MiB) values to keep upgrades safe. For larger +/// values, consider using other data structures like `Vec` or `BTreeMap` instead. +/// +/// # Example +/// +/// ```rust +/// use ic_stable_structures::{Cell, DefaultMemoryImpl, Storable, storable::Bound}; +/// use std::borrow::Cow; +/// use std::cell::RefCell; +/// +/// #[derive(Clone)] +/// struct Config { +/// name: String, +/// version: u32, +/// } +/// +/// // Implement Storable for serialization/deserialization when saving to stable memory. +/// impl Storable for Config { +/// fn to_bytes(&self) -> Cow<'_, [u8]> { +/// # let mut bytes = Vec::new(); +/// // Convert config into bytes... +/// # Cow::Owned(bytes) +/// } +/// +/// fn into_bytes(self) -> Vec { +/// # let mut bytes = Vec::new(); +/// // Convert config into bytes... +/// # bytes +/// } +/// +/// fn from_bytes(bytes: Cow<[u8]>) -> Self { +/// // Convert bytes back to Config +/// # let (name, version) = ("".to_string(), 0); +/// # Config { name, version } +/// } +/// +/// // Types can be bounded or unbounded: +/// // - Use Bound::Unbounded if the size can vary or isn't known in advance (recommended for most cases) +/// // - Use Bound::Bounded if you know the maximum size and want memory optimization +/// const BOUND: Bound = Bound::Unbounded; +/// } +/// +/// // Create a global cell variable +/// thread_local! { +/// static CONFIG: RefCell> = RefCell::new( +/// Cell::init( +/// DefaultMemoryImpl::default(), +/// Config { +/// name: "MyConfig".to_string(), +/// version: 1, +/// } +/// ).expect("failed to initialize config cell") +/// ); +/// } +/// +/// // Read the current configuration +/// fn get_version() -> u32 { +/// CONFIG.with(|c| c.borrow().get().version) +/// } +/// +/// // Update the configuration +/// fn update_version(new_version: u32) -> Result<(), String> { +/// CONFIG.with(|c| { +/// let mut cell = c.borrow_mut(); +/// let mut config = cell.get().clone(); +/// config.version = new_version; +/// cell.set(config).map_err(|e| format!("Failed to update: {:?}", e))?; +/// Ok(()) +/// }) +/// } +/// +/// # // Test to ensure example works as expected. +/// # fn main() { +/// # assert_eq!(get_version(), 1); +/// # update_version(2).unwrap(); +/// # assert_eq!(get_version(), 2); +/// # } +/// ``` pub struct Cell { memory: M, value: T, diff --git a/src/cell/tests.rs b/src/cell/tests.rs index b6d44812..fa84562e 100644 --- a/src/cell/tests.rs +++ b/src/cell/tests.rs @@ -60,3 +60,21 @@ fn test_cell_grow_and_shrink() { let cell = reload(cell); assert_eq!(&[3u8; 5][..], &cell.get()[..]); } + +#[test] +fn can_store_unbounded_type() { + let mem = VectorMemory::default(); + let test_string = "Another string with different content".to_string(); + + let mut cell = Cell::init(mem, test_string.clone()).unwrap(); + assert_eq!(test_string, *cell.get()); + + let new_string = "This is a test string that can be very long and unbounded".to_string(); + cell.set(new_string.clone()).unwrap(); + assert_eq!(new_string, *cell.get()); + + // Test reloading from memory + let cell = reload(cell); + assert_eq!(new_string, *cell.get()); +} + From 950e27d78c8ea9baf09a4f48aaa62593cc395e77 Mon Sep 17 00:00:00 2001 From: Islam El-Ashi Date: Thu, 26 Jun 2025 15:19:44 +0200 Subject: [PATCH 2/3] cargo fmt --- src/cell.rs | 2 +- src/cell/tests.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cell.rs b/src/cell.rs index 0fc60d4f..9987122b 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -82,7 +82,7 @@ impl From for InitError { } /// Represents a value stored in stable memory. -/// +/// /// A `Cell` stores a single value directly in stable memory and provides immediate persistence /// on every write operation. This makes it ideal for configuration values, metadata, or any /// small state that needs to survive canister upgrades. diff --git a/src/cell/tests.rs b/src/cell/tests.rs index fa84562e..2153bb79 100644 --- a/src/cell/tests.rs +++ b/src/cell/tests.rs @@ -65,16 +65,15 @@ fn test_cell_grow_and_shrink() { fn can_store_unbounded_type() { let mem = VectorMemory::default(); let test_string = "Another string with different content".to_string(); - + let mut cell = Cell::init(mem, test_string.clone()).unwrap(); assert_eq!(test_string, *cell.get()); - + let new_string = "This is a test string that can be very long and unbounded".to_string(); cell.set(new_string.clone()).unwrap(); assert_eq!(new_string, *cell.get()); - + // Test reloading from memory let cell = reload(cell); assert_eq!(new_string, *cell.get()); } - From b666283c34395ddeb855d000425c386b114410ec Mon Sep 17 00:00:00 2001 From: Islam El-Ashi Date: Mon, 30 Jun 2025 11:59:21 +0200 Subject: [PATCH 3/3] merge --- src/cell.rs | 11 +++++------ src/cell/tests.rs | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/cell.rs b/src/cell.rs index 9a41fd15..19425b25 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -138,7 +138,7 @@ impl From for InitError { /// name: "MyConfig".to_string(), /// version: 1, /// } -/// ).expect("failed to initialize config cell") +/// ) /// ); /// } /// @@ -148,20 +148,19 @@ impl From for InitError { /// } /// /// // Update the configuration -/// fn update_version(new_version: u32) -> Result<(), String> { +/// fn update_version(new_version: u32) { /// CONFIG.with(|c| { /// let mut cell = c.borrow_mut(); /// let mut config = cell.get().clone(); /// config.version = new_version; -/// cell.set(config).map_err(|e| format!("Failed to update: {:?}", e))?; -/// Ok(()) -/// }) +/// cell.set(config); +/// }); /// } /// /// # // Test to ensure example works as expected. /// # fn main() { /// # assert_eq!(get_version(), 1); -/// # update_version(2).unwrap(); +/// # update_version(2); /// # assert_eq!(get_version(), 2); /// # } /// ``` diff --git a/src/cell/tests.rs b/src/cell/tests.rs index 6a947bf5..b4d1e5bd 100644 --- a/src/cell/tests.rs +++ b/src/cell/tests.rs @@ -60,11 +60,11 @@ fn can_store_unbounded_type() { let mem = VectorMemory::default(); let test_string = "Another string with different content".to_string(); - let mut cell = Cell::init(mem, test_string.clone()).unwrap(); + let mut cell = Cell::init(mem, test_string.clone()); assert_eq!(test_string, *cell.get()); let new_string = "This is a test string that can be very long and unbounded".to_string(); - cell.set(new_string.clone()).unwrap(); + cell.set(new_string.clone()); assert_eq!(new_string, *cell.get()); // Test reloading from memory