Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add NewTypeKey derive macro #88

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

## [2.1.0] - 2024-09-25
- Add derive macro for `NewTypeKey`

## [2.0.0] - 2024-03-14

## [2.0.0-rc.0] - 2024-02-09
Expand Down
68 changes: 57 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ criterion = { version = "0.3", features = ["html_reports"] }
# access to an entropy souce via getrandom such as wasm32-unknown-unknown
rand = { version = "0.8", default-features = false }
rand_xoshiro = { version = "0.6.0", default-features = false }
derive_more = {version = "1.0.0", features = ["full"]}

# We don't use the following dependencies directly. They're dependencies of our dependencies.
# We specify them to tighten their version requirements so that builds with `-Zminimal-versions` work.
Expand Down
2 changes: 2 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ documentation = "https://docs.cosmwasm.com"
proc-macro = true

[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.37"
syn = { version = "2", features = ["full"] }
10 changes: 10 additions & 0 deletions macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ struct TestIndexes<'a> {
addr: UniqueIndex<'a, Addr, TestStruct, String>,
}
```

Auto generate the required impls to use a newtype as a key

```rust
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[derive(NewTypeKey)] // <- Add this line right here.
struct TestKey(u64);

// You can now use `TestKey` as a key in `Map`
```
36 changes: 36 additions & 0 deletions macros/src/index_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use proc_macro::TokenStream;
use syn::{
Ident,
__private::{quote::quote, Span},
parse_macro_input, ItemStruct,
};

pub fn index_list(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);

let ty = Ident::new(&attr.to_string(), Span::call_site());
let struct_ty = input.ident.clone();

let names = input
.fields
.clone()
.into_iter()
.map(|e| {
let name = e.ident.unwrap();
quote! { &self.#name }
})
.collect::<Vec<_>>();

let expanded = quote! {
#input

impl cw_storage_plus::IndexList<#ty> for #struct_ty<'_> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn cw_storage_plus::Index<#ty>> + '_> {
let v: Vec<&dyn cw_storage_plus::Index<#ty>> = vec![#(#names),*];
Box::new(v.into_iter())
}
}
};

TokenStream::from(expanded)
}
43 changes: 11 additions & 32 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,22 @@
Procedural macros helper for interacting with cw-storage-plus and cosmwasm-storage.

For more information on this package, please check out the
[README](https://github.com/CosmWasm/cw-plus/blob/main/packages/storage-macro/README.md).
[README](https://github.com/CosmWasm/cw-storage-plus/blob/main/macros/README.md).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

*/

mod index_list;
mod newtype;

use proc_macro::TokenStream;
use syn::{
Ident,
__private::{quote::quote, Span},
parse_macro_input, ItemStruct,
};

// Re-export the procedural macro functions

#[proc_macro_attribute]
pub fn index_list(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);

let ty = Ident::new(&attr.to_string(), Span::call_site());
let struct_ty = input.ident.clone();

let names = input
.fields
.clone()
.into_iter()
.map(|e| {
let name = e.ident.unwrap();
quote! { &self.#name }
})
.collect::<Vec<_>>();

let expanded = quote! {
#input

impl cw_storage_plus::IndexList<#ty> for #struct_ty<'_> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn cw_storage_plus::Index<#ty>> + '_> {
let v: Vec<&dyn cw_storage_plus::Index<#ty>> = vec![#(#names),*];
Box::new(v.into_iter())
}
}
};
index_list::index_list(attr, item)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks for moving this to a separate module

}

TokenStream::from(expanded)
#[proc_macro_derive(NewTypeKey)]
pub fn cw_storage_newtype_key_derive(input: TokenStream) -> TokenStream {
newtype::cw_storage_newtype_key_derive(input)
}
82 changes: 82 additions & 0 deletions macros/src/newtype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

pub fn cw_storage_newtype_key_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if you can parse it directly as ItemStruct (or whatever the type is called in syn). Could save some of the pattern matching later.


let expanded = impl_newtype(&input);

TokenStream::from(expanded)
}

fn impl_newtype(input: &DeriveInput) -> proc_macro2::TokenStream {
// Extract the struct name
let name = &input.ident;

// Extract the inner type
let inner_type = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => Some(&fields.unnamed[0].ty),
_ => None,
},
_ => None,
};

let inner_type = inner_type
.expect("NewTypeKey can only be derived for newtypes (tuple structs with one field)");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are ways to display macro errors similarly to other rust compiler errors, with source code being highlighted and all. I don't think it's a must, but if you're interested: https://docs.rs/syn/latest/syn/struct.Error.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can probably find an example later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I don't have a lot of experience writing macros, happy to learn better techniques. Reminding myself to address this and other comments early next week if not later today :)


// Implement PrimaryKey
let impl_primary_key = quote! {
impl<'a> cw_storage_plus::PrimaryKey<'a> for #name
where
#inner_type: cw_storage_plus::PrimaryKey<'a>,
{
type Prefix = ();
type SubPrefix = ();
type Suffix = Self;
type SuperSuffix = Self;

fn key(&self) -> Vec<cw_storage_plus::Key> {
self.0.key()
}
}
};

// Implement Prefixer
let impl_prefixer = quote! {
impl<'a> cw_storage_plus::Prefixer<'a> for #name
where
#inner_type: cw_storage_plus::Prefixer<'a>,
{
fn prefix(&self) -> Vec<cw_storage_plus::Key> {
self.0.prefix()
}
}
};

// Implement KeyDeserialize
let impl_key_deserialize = quote! {
impl cw_storage_plus::KeyDeserialize for #name
where
#inner_type: cw_storage_plus::KeyDeserialize<Output = #inner_type>,
{
type Output = #name;
const KEY_ELEMS: u16 = 1;

#[inline(always)]
fn from_vec(value: Vec<u8>) -> cosmwasm_std::StdResult<Self::Output> {
<#inner_type as cw_storage_plus::KeyDeserialize>::from_vec(value).map(#name)
}
}
};

// Combine all implementations
let expanded = quote! {
#impl_primary_key
#impl_prefixer
#impl_key_deserialize
};

expanded
}
17 changes: 17 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,20 @@ extern crate cw_storage_macro;
/// ```
///
pub use cw_storage_macro::index_list;

#[cfg(all(feature = "iterator", feature = "macro"))]
/// Auto generate the required impls to use a newtype as a key
/// # Example
///
/// ```rust
/// use cw_storage_plus::NewTypeKey;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
/// #[derive(NewTypeKey)] // <- Add this line right here.
/// struct TestKey(u64);
///
/// // You can now use `TestKey` as a key in `Map`
/// ```
///
pub use cw_storage_macro::NewTypeKey;
Loading