diff --git a/docs-test-gen/src/main.rs b/docs-test-gen/src/main.rs index 82a70b1f..09280a4d 100644 --- a/docs-test-gen/src/main.rs +++ b/docs-test-gen/src/main.rs @@ -12,6 +12,8 @@ static TEMPLATES: phf::Map<&'static str, &'static str> = phf_map! { "ibc-channel" => include_str!("../templates/ibc-channel.tpl"), "ibc-packet" => include_str!("../templates/ibc-packet.tpl"), "storage" => include_str!("../templates/storage.tpl"), + "storey-container-impl" => include_str!("../templates/storey-container-impl.tpl"), + "storey-container-impl-iter" => include_str!("../templates/storey-container-impl-iter.tpl"), "sylvia-storey-contract" => include_str!("../templates/sylvia/storey_contract.tpl"), "sylvia-cw-storage-contract" => include_str!("../templates/sylvia/cw_storage_contract.tpl"), "sylvia-empty" => include_str!("../templates/sylvia/empty.tpl"), diff --git a/docs-test-gen/templates/storey-container-impl-iter.tpl b/docs-test-gen/templates/storey-container-impl-iter.tpl new file mode 100644 index 00000000..1629c394 --- /dev/null +++ b/docs-test-gen/templates/storey-container-impl-iter.tpl @@ -0,0 +1,225 @@ +#![allow( + unexpected_cfgs, + dead_code, + unused_variables, + unused_imports, + clippy::new_without_default, +)] +use cosmwasm_schema::cw_serde; +use cosmwasm_std::*; +use storey::containers::{IterableStorable, NonTerminal, Storable}; +use storey::storage::{IntoStorage, StorageBranch}; + +mod users { + use super::*; + + use cw_storage_plus::{index_list, IndexedMap, MultiIndex, UniqueIndex}; + + pub type Handle = String; + + #[cw_serde] + pub struct User { + pub handle: String, + pub country: String, + } + + pub struct ExampleUsers { + pub alice: User, + pub bob: User, + } + + pub fn example_users() -> ExampleUsers { + ExampleUsers { + alice: User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + bob: User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + } + } + + #[index_list(User)] + pub struct UserIndexes<'a> { + pub handle_ix: UniqueIndex<'a, Handle, User, Addr>, + pub country_ix: MultiIndex<'a, String, User, Addr>, + } + + pub fn user_indexes() -> UserIndexes<'static> { + user_indexes_custom("u", "uh", "uc") + } + + pub fn user_indexes_custom( + ns: &'static str, + handle_prefix: &'static str, + country_prefix: &'static str, + ) -> UserIndexes<'static> { + UserIndexes { + handle_ix: UniqueIndex::new(|user| user.handle.clone(), handle_prefix), + country_ix: MultiIndex::new(|_pk, user| user.country.clone(), ns, country_prefix), + } + } +} + +fn advance_height(env: &mut Env, blocks: u64) { + env.block.height += blocks; +} + +pub struct MyMap { + prefix: u8, + phantom: std::marker::PhantomData, +} + +impl MyMap +where + V: Storable, +{ + pub const fn new(prefix: u8) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } + + pub fn access(&self, storage: F) -> MyMapAccess> + where + (F,): IntoStorage, + { + let storage = (storage,).into_storage(); + Self::access_impl(StorageBranch::new(storage, vec![self.prefix])) + } +} + +pub struct MyMapAccess { + storage: S, + phantom: std::marker::PhantomData, +} + +impl Storable for MyMap +where + V: Storable, +{ + type Kind = NonTerminal; + type Accessor = MyMapAccess; + + fn access_impl(storage: S) -> MyMapAccess { + MyMapAccess { + storage, + phantom: std::marker::PhantomData, + } + } +} + +impl MyMapAccess +where + V: Storable, +{ + pub fn entry(&self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&self.storage, key)) + } + + pub fn entry_mut(&mut self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&mut self.storage, key)) + } +} + +use storey::containers::IterableAccessor; +use storey::storage::IterableStorage; + +impl IterableStorable for MyMap +where + V: IterableStorable, + ::KeyDecodeError: std::fmt::Display, +{ + type Key = (u32, V::Key); + type KeyDecodeError = String; + type Value = V::Value; + type ValueDecodeError = V::ValueDecodeError; + + fn decode_key(key: &[u8]) -> Result { + if key.len() < 4 { + return Err(String::from("Key too short")); + } + + let key_arr = key[0..4].try_into().map_err(|e| format!("Invalid key: {}", e))?; + let this_key = u32::from_be_bytes(key_arr); + + let rest = V::decode_key(&key[4..]).map_err(|e| e.to_string())?; + + Ok((this_key, rest)) + } + + fn decode_value(value: &[u8]) -> Result { + V::decode_value(value) + } +} + +impl IterableAccessor for MyMapAccess +where + V: IterableStorable, + S: IterableStorage, +{ + type Storable = MyMap; + type Storage = S; + + fn storage(&self) -> &Self::Storage { + &self.storage + } +} + +#[test] +fn doctest() { + #[allow(unused_variables, unused_mut)] + let mut storage = cosmwasm_std::testing::MockStorage::new(); + #[allow(unused_mut)] + let mut env = cosmwasm_std::testing::mock_env(); + + let users = cw_storage_plus::IndexedMap::::new( + "uu", + users::user_indexes_custom("uu", "uuh", "uuc"), + ); + + let users_data = [ + ( + Addr::unchecked("aaa"), + users::User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + ), + ( + Addr::unchecked("bbb"), + users::User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + ), + ( + Addr::unchecked("ccc"), + users::User { + handle: "carol".to_string(), + country: "UK".to_string(), + }, + ), + ( + Addr::unchecked("ddd"), + users::User { + handle: "dave".to_string(), + country: "USA".to_string(), + }, + ), + ]; + + for (addr, user) in users_data { + users.save(&mut storage, addr, &user).unwrap(); + } + + #[rustfmt::skip] + {{code}} +} diff --git a/docs-test-gen/templates/storey-container-impl.tpl b/docs-test-gen/templates/storey-container-impl.tpl new file mode 100644 index 00000000..247b7cf1 --- /dev/null +++ b/docs-test-gen/templates/storey-container-impl.tpl @@ -0,0 +1,181 @@ +#![allow( + unexpected_cfgs, + dead_code, + unused_variables, + unused_imports, + clippy::new_without_default +)] +use cosmwasm_schema::cw_serde; +use cosmwasm_std::*; +use storey::containers::{IterableStorable, NonTerminal, Storable}; +use storey::storage::{IntoStorage, StorageBranch}; + +mod users { + use super::*; + + use cw_storage_plus::{index_list, IndexedMap, MultiIndex, UniqueIndex}; + + pub type Handle = String; + + #[cw_serde] + pub struct User { + pub handle: String, + pub country: String, + } + + pub struct ExampleUsers { + pub alice: User, + pub bob: User, + } + + pub fn example_users() -> ExampleUsers { + ExampleUsers { + alice: User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + bob: User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + } + } + + #[index_list(User)] + pub struct UserIndexes<'a> { + pub handle_ix: UniqueIndex<'a, Handle, User, Addr>, + pub country_ix: MultiIndex<'a, String, User, Addr>, + } + + pub fn user_indexes() -> UserIndexes<'static> { + user_indexes_custom("u", "uh", "uc") + } + + pub fn user_indexes_custom( + ns: &'static str, + handle_prefix: &'static str, + country_prefix: &'static str, + ) -> UserIndexes<'static> { + UserIndexes { + handle_ix: UniqueIndex::new(|user| user.handle.clone(), handle_prefix), + country_ix: MultiIndex::new(|_pk, user| user.country.clone(), ns, country_prefix), + } + } +} + +fn advance_height(env: &mut Env, blocks: u64) { + env.block.height += blocks; +} + +#[test] +fn doctest() { + pub struct MyMap { + prefix: u8, + phantom: std::marker::PhantomData, + } + + impl MyMap + where + V: Storable, + { + pub const fn new(prefix: u8) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } + + pub fn access(&self, storage: F) -> MyMapAccess> + where + (F,): IntoStorage, + { + let storage = (storage,).into_storage(); + Self::access_impl(StorageBranch::new(storage, vec![self.prefix])) + } + } + + pub struct MyMapAccess { + storage: S, + phantom: std::marker::PhantomData, + } + + impl Storable for MyMap + where + V: Storable, + { + type Kind = NonTerminal; + type Accessor = MyMapAccess; + + fn access_impl(storage: S) -> MyMapAccess { + MyMapAccess { + storage, + phantom: std::marker::PhantomData, + } + } + } + + impl MyMapAccess + where + V: Storable, + { + pub fn entry(&self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&self.storage, key)) + } + + pub fn entry_mut(&mut self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&mut self.storage, key)) + } + } + + #[allow(unused_variables, unused_mut)] + let mut storage = cosmwasm_std::testing::MockStorage::new(); + #[allow(unused_mut)] + let mut env = cosmwasm_std::testing::mock_env(); + + let users = cw_storage_plus::IndexedMap::::new( + "uu", + users::user_indexes_custom("uu", "uuh", "uuc"), + ); + + let users_data = [ + ( + Addr::unchecked("aaa"), + users::User { + handle: "alice".to_string(), + country: "Wonderland".to_string(), + }, + ), + ( + Addr::unchecked("bbb"), + users::User { + handle: "bob".to_string(), + country: "USA".to_string(), + }, + ), + ( + Addr::unchecked("ccc"), + users::User { + handle: "carol".to_string(), + country: "UK".to_string(), + }, + ), + ( + Addr::unchecked("ddd"), + users::User { + handle: "dave".to_string(), + country: "USA".to_string(), + }, + ), + ]; + + for (addr, user) in users_data { + users.save(&mut storage, addr, &user).unwrap(); + } + + #[rustfmt::skip] + {{code}} +} diff --git a/src/pages/storey/container-impl.mdx b/src/pages/storey/container-impl.mdx index 18c21131..df9eafa6 100644 --- a/src/pages/storey/container-impl.mdx +++ b/src/pages/storey/container-impl.mdx @@ -5,3 +5,14 @@ tags: ["storey"] import { Callout } from "nextra/components"; # Implementing new containers + +Storey provides a set of built-in containers, but you're not limited to just those. For special +needs, you can go ahead and create your own containers that will play along nicely with the rest of +the Storey "ecosystem". If it's appropriate, your container could for example allow for nesting +other containers - like [`Map`]. + +In this section, you will find examples of custom containers with their full code. + +# Guides + +- [MyMap](container-impl/my-map) diff --git a/src/pages/storey/container-impl/_meta.js b/src/pages/storey/container-impl/_meta.js new file mode 100644 index 00000000..77f1e226 --- /dev/null +++ b/src/pages/storey/container-impl/_meta.js @@ -0,0 +1,3 @@ +export default { + "my-map": "MyMap", +}; diff --git a/src/pages/storey/container-impl/my-map.mdx b/src/pages/storey/container-impl/my-map.mdx new file mode 100644 index 00000000..99930669 --- /dev/null +++ b/src/pages/storey/container-impl/my-map.mdx @@ -0,0 +1,366 @@ +--- +tags: ["storey"] +--- + +import { Callout } from "nextra/components"; + +# MyMap + +Let's build our own version of the [`Map`] container. We'll call it `MyMap`. + +This one will be a little limited. It only accepts `u32` keys. + +First, let's create our struct. + +```rust template="storage" +pub struct MyMap { + prefix: u8, + phantom: std::marker::PhantomData, +} + +impl MyMap { + pub const fn new(prefix: u8) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } +} +``` + +No magic here. The `prefix` field is used when this collection is a top-level collection - it's a +single-byte key that creates a subspace for this collection's internal data. + +The `phantom` field allows us to use the type parameter `V` without actually storing any values of +that type. + +The `V` type parameter is the type of the **container** inside the map. If you've followed the +[`Map`] documentation, you'll know that `Map` is a composable container - it holds another container +inside. + +The constructor is simple - it just initializes the fields. + +The next step is to set up an accessor. Hold on tight! This will be a ride. + +```rust template="storage" {1-2, 10-11, 20-26, 29-46} +use storey::containers::{NonTerminal, Storable}; +use storey::storage::{IntoStorage, StorageBranch}; + +pub struct MyMap { + prefix: u8, + phantom: std::marker::PhantomData, +} + +impl MyMap +where + V: Storable, +{ + pub const fn new(prefix: u8) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } + + pub fn access(&self, storage: F) -> MyMapAccess> + where + (F,): IntoStorage, + { + let storage = (storage,).into_storage(); + Self::access_impl(StorageBranch::new(storage, vec![self.prefix])) + } +} + +pub struct MyMapAccess { + storage: S, + phantom: std::marker::PhantomData, +} + +impl Storable for MyMap +where + V: Storable, +{ + type Kind = NonTerminal; + type Accessor = MyMapAccess; + + fn access_impl(storage: S) -> MyMapAccess { + MyMapAccess { + storage, + phantom: std::marker::PhantomData, + } + } +} +``` + +Whew. Let's break this down. + +The `MyMapAccess` struct is our accessor. It's a facade that's used to actually access the data in +the collection given a `Storage` instance - this is usually a subspace of the "root" storage +backend. + +The [`Storable`] trait is the main trait a container must implement. The associated types tell the +framework: + +| Associated type | Details | +| --------------- | ------------------------------------------------------------------------------------------------------------ | +| `Kind` | We put `NonTerminal` here to signify our container creates subkeys rather than just saving data at the root. | +| `Accessor` | The accessor type. `MyMapAccess` in our case. | + +The method `access_impl` produces an accessor given a storage abstraction (usually representing a +"slice" of the underlying storage.) + +`MyMap::access` is an access method in cases where you're using the container as a top-level +container. + +There's one thing we're missing for this to actually by useful. We need some methods for the +accessor. + +```rust template="storage" {49-64} +use storey::containers::{NonTerminal, Storable}; +use storey::storage::{IntoStorage, StorageBranch}; + +pub struct MyMap { + prefix: u8, + phantom: std::marker::PhantomData, +} + +impl MyMap +where + V: Storable, +{ + pub const fn new(prefix: u8) -> Self { + Self { + prefix, + phantom: std::marker::PhantomData, + } + } + + pub fn access(&self, storage: F) -> MyMapAccess> + where + (F,): IntoStorage, + { + let storage = (storage,).into_storage(); + Self::access_impl(StorageBranch::new(storage, vec![self.prefix])) + } +} + +pub struct MyMapAccess { + storage: S, + phantom: std::marker::PhantomData, +} + +impl Storable for MyMap +where + V: Storable, +{ + type Kind = NonTerminal; + type Accessor = MyMapAccess; + + fn access_impl(storage: S) -> MyMapAccess { + MyMapAccess { + storage, + phantom: std::marker::PhantomData, + } + } +} + +impl MyMapAccess +where + V: Storable, +{ + pub fn entry(&self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&self.storage, key)) + } + + pub fn entry_mut(&mut self, key: u32) -> V::Accessor> { + let key = key.to_be_bytes().to_vec(); + + V::access_impl(StorageBranch::new(&mut self.storage, key)) + } +} +``` + +What does this new code do? It provides a way to create accessors for the inner container based on +the `u32` key. The [`StorageBranch`] type is a helper that creates a "subspace" of the storage +backend. It's used to create a "slice" of the storage granted to `MyMap`. + +Time to see this in action. + +```rust template="storey-container-impl" +use cw_storey::containers::{Item}; + +const MAP_IX: u8 = 0; + +let my_map: MyMap> = MyMap::new(MAP_IX); +let mut access = my_map.access(&mut storage); + +access.entry_mut(1).set(&100).unwrap(); +access.entry_mut(2).set(&200).unwrap(); + +assert_eq!(access.entry(1).get().unwrap(), Some(100)); +assert_eq!(access.entry(2).get().unwrap(), Some(200)); +``` + +# Iteration + +While we have a functional collection by now, we can't perform iteration yet. Instead of trying to +implement iteration on our own, there are benefits to using abstractions `storey` provides. This +means implementing `IterableStorable` for `MyMap`. + +```rust template="storey-container-impl" +impl IterableStorable for MyMap +where + V: IterableStorable, + V::KeyDecodeError: std::fmt::Display, +{ + type Key = (u32, V::Key); + type KeyDecodeError = String; + type Value = V::Value; + type ValueDecodeError = V::ValueDecodeError; + + fn decode_key(key: &[u8]) -> Result { + if key.len() < 4 { + return Err(String::from("Key too short")); + } + + let key_arr = key[0..4].try_into().map_err(|e| format!("Invalid key: {}", e))?; + let this_key = u32::from_be_bytes(key_arr); + + let rest = V::decode_key(&key[4..]).map_err(|e| e.to_string())?; + + Ok((this_key, rest)) + } + + fn decode_value(value: &[u8]) -> Result { + V::decode_value(value) + } +} +``` + +Alright. Let's dive into the trait items! + +| Associated type | Details | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `Key` | The key type to be returned by iterators. Here a tuple of `u32` and the key type of the inner container. | +| `KeyDecodeError` | The error to be returned on invalid key data. Here we don't care, so we use `()`. In production, use a proper error type. | +| `Value` | The value type to be returned by iterators. Here it's delegated to the inner container. | +| `ValueDecodeError` | The error type for invalid value data. Delegated to the inner container. | + +| Method | Function | +| -------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `decode_key` | This is how the framework knows how to decode keys given raw bytes. | +| `decode_value` | This is how the framework knows how to decode values given raw bytes. In this case, we delegate to the inner collection. | + +We still need to implement a trait for the accessor. + +```rust template="storey-container-impl" {1-2, 32-44} +use storey::containers::IterableAccessor; +use storey::storage::IterableStorage; + +impl IterableStorable for MyMap +where + V: IterableStorable, + V::KeyDecodeError: std::fmt::Display, +{ + type Key = (u32, V::Key); + type KeyDecodeError = String; + type Value = V::Value; + type ValueDecodeError = V::ValueDecodeError; + + fn decode_key(key: &[u8]) -> Result { + if key.len() < 4 { + return Err(String::from("Key too short")); + } + + let key_arr = key[0..4].try_into().map_err(|e| format!("Invalid key: {}", e))?; + let this_key = u32::from_be_bytes(key_arr); + + let rest = V::decode_key(&key[4..]).map_err(|e| e.to_string())?; + + Ok((this_key, rest)) + } + + fn decode_value(value: &[u8]) -> Result { + V::decode_value(value) + } +} + +impl IterableAccessor for MyMapAccess +where + V: IterableStorable, + S: IterableStorage, +{ + type Storable = MyMap; + type Storage = S; + + fn storage(&self) -> &Self::Storage { + &self.storage + } +} +``` + +Now that we've implemented the thing, let's use it! + +```rust template="storey-container-impl-iter" +use cw_storey::containers::{Item}; +use storey::containers::IterableAccessor as _; + +const MAP_IX: u8 = 1; + +let my_map: MyMap> = MyMap::new(MAP_IX); +let mut access = my_map.access(&mut storage); + +access.entry_mut(1).set(&100).unwrap(); +access.entry_mut(2).set(&200).unwrap(); +access.entry_mut(3).set(&300).unwrap(); + +let result: Result, _> = access.values().collect(); +assert_eq!(result.unwrap(), vec![100, 200, 300]); +``` + +This isn't all. What we've also enabled is the ability to iterate over any containers that nest +`MyMap` inside. + +Let's create a regular `Map`, nest our `MyMap` inside it, and see what we can do! + +```rust template="storey-container-impl-iter" +use cw_storey::containers::{Item, Map}; +use storey::containers::IterableAccessor as _; + +const MAP_IX: u8 = 1; + +let map: Map>> = Map::new(MAP_IX); +let mut access = map.access(&mut storage); + +access.entry_mut("alice").entry_mut(1).set(&100).unwrap(); +access.entry_mut("alice").entry_mut(2).set(&200).unwrap(); +access.entry_mut("bob").entry_mut(1).set(&1100).unwrap(); +access.entry_mut("bob").entry_mut(3).set(&1300).unwrap(); + +let result: Result, _> = access.pairs().collect(); +assert_eq!(result.unwrap(), vec![ + (("bob".to_string(), (1, ())), 1100), + (("bob".to_string(), (3, ())), 1300), + (("alice".to_string(), (1, ())), 100), + (("alice".to_string(), (2, ())), 200) +]); +``` + +We can iterate over everything. Note we didn't have to write any special logic for this nesting in +our custom container's implementation. It's well integrated with Storey simply by implementing a few +traits. + + + We know the parenthesized, nested keys are ugly. We'll eventually try to make this + prettier. + + For now, remember this ugliness allows unlimited depth of nested keys, which is pretty cool! Recursion rocks. Sometimes. + + + +[`Item`]: /storey/containers/item +[`Map`]: /storey/containers/map +[`Storable`]: https://docs.rs/storey/latest/storey/containers/trait.Storable.html +[`StorageBranch`]: https://docs.rs/storey/latest/storey/storage/struct.StorageBranch.html