Stores: Externally tracked state and reactive collections (#4483)
* map signals mutably * boxed signals * remove mutable borrow type from boxed writable * improve map mut example * restore boxed_mut helper * make boxed readable/writable types copy * Move away from the ReadOnlySignal alias in examples * automatically convert Write * Fix some tests * ReadSignal and WriteSignal * move read only signal alias * make maps copy * store prototype * implement derive macro * Handle generics in the derive macro * Make Selector boxable * derive boxed conversion * Get rid of store type * Remove hardcoded type from derive macro * selector -> store * Refactor store implementation * more vec methods * todo mvc store example * Implement Storable for HashMap * fix write * restore sorting logic in todomvc * derive readable and writable * improve todomvc store example * Result store * Option store * collect dead subscribers * fix clippy and formatting * recognize some common foreign types automatically * fix foreign stores in props * slice and array selectors * clean up store example * writable is child owned * box selector * read and write to builtins * re-use the proc macro in the derive macro * Switch to a extension trait based approach * switch to extension traits in the derive macro * transposed method * derive enums * Expose Store::new * remove stores from the prelude * More consistent naming scheme for SelectorScope methods * document hashmap methods * document index extension * document option ext * document result ext * document slice extension * document the vec extension * make more methods borrow the store mutably * Move to impls to have higher priority over the builtin write extension traits * fix deadlock * finish stores documentation * Document the store macro * cleanup docs and organization * finish derive docs * fix typo * fix clippy * fix doc tests * fix derive macro with generics * fix clippy * test and fix empty structs * clean up todomvc store example * default to a boxed store writer to make it easier to type when providing to the context api * fix cargo doc * fix clippy in store example * relax generational box lifetime bounds * relax bounds * relax signal bounds * fix compilation * read store * relax bounds on default store impls * fix enum derive * Convert to readsignal and writesignal automatically * simplify type matching logic * add write store * unsized stores * shrink the path vec type * use more concrete types * remove opaque types * remove copy bounds * use lens as generic name everywhere * more consistent borrow semantics * use a concrete type for the signal metadata and mention references in the map docs * improve todomvc store docs * document store derive macro internals * refactor macro * improve selector node documentation * fix clippy * add btreemap * fix doc tests * more option methods * fix doc * fix clippy * add a bunch more result methods * add a bunch of bounds to the iterators * fix clippy * remove option from subscribers * add store macro to generate extension traits * fix store example * fix deadlock * fix docs * fix doc tests * fix clippy * add stores to public api * add store macro, and then update todomvc_store example --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
5591070e92
commit
58c41f8561
57 files changed
+4958
-436
No files matched your search
@@ -0,0 +1,493 @@
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::{
|
||||
parse_quote, spanned::Spanned, DataEnum, DataStruct, DeriveInput, Field, Fields, Generics,
|
||||
Ident, Index, LitInt,
|
||||
};
|
||||
|
||||
pub(crate) fn derive_store(input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||
let item_name = &input.ident;
|
||||
let extension_trait_name = format_ident!("{}StoreExt", item_name);
|
||||
let transposed_name = format_ident!("{}StoreTransposed", item_name);
|
||||
|
||||
// Create generics for the extension trait and transposed struct. Both items need the original generics
|
||||
// and bounds plus an extra __Lens type used in the store generics
|
||||
let generics = &input.generics;
|
||||
let mut extension_generics = generics.clone();
|
||||
extension_generics.params.insert(0, parse_quote!(__Lens));
|
||||
|
||||
match &input.data {
|
||||
syn::Data::Struct(data_struct) => derive_store_struct(
|
||||
&input,
|
||||
data_struct,
|
||||
extension_trait_name,
|
||||
transposed_name,
|
||||
extension_generics,
|
||||
),
|
||||
syn::Data::Enum(data_enum) => derive_store_enum(
|
||||
&input,
|
||||
data_enum,
|
||||
extension_trait_name,
|
||||
transposed_name,
|
||||
extension_generics,
|
||||
),
|
||||
syn::Data::Union(_) => Err(syn::Error::new(
|
||||
input.span(),
|
||||
"Store macro does not support unions",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// For structs, we derive two items:
|
||||
// - An extension trait with methods to access the fields of the struct as stores and a `transpose` method
|
||||
// - A transposed version of the struct with all fields wrapped in stores
|
||||
fn derive_store_struct(
|
||||
input: &DeriveInput,
|
||||
structure: &DataStruct,
|
||||
extension_trait_name: Ident,
|
||||
transposed_name: Ident,
|
||||
extension_generics: Generics,
|
||||
) -> syn::Result<TokenStream2> {
|
||||
let struct_name = &input.ident;
|
||||
let fields = &structure.fields;
|
||||
let visibility = &input.vis;
|
||||
|
||||
// We don't need to do anything if there are no fields
|
||||
if fields.is_empty() {
|
||||
return Ok(quote! {});
|
||||
}
|
||||
|
||||
let generics = &input.generics;
|
||||
let (_, ty_generics, _) = generics.split_for_impl();
|
||||
let (extension_impl_generics, extension_generics, extension_where_clause) =
|
||||
extension_generics.split_for_impl();
|
||||
|
||||
// We collect the definitions and implementations for the extension trait methods along with the types of the fields in the transposed struct
|
||||
let mut implementations = Vec::new();
|
||||
let mut definitions = Vec::new();
|
||||
let mut transposed_fields = Vec::new();
|
||||
|
||||
for (field_index, field) in fields.iter().enumerate() {
|
||||
generate_field_methods(
|
||||
field_index,
|
||||
field,
|
||||
struct_name,
|
||||
&ty_generics,
|
||||
&mut transposed_fields,
|
||||
&mut definitions,
|
||||
&mut implementations,
|
||||
);
|
||||
}
|
||||
|
||||
// Add a transpose method to turn the stored struct into a struct with all fields as stores
|
||||
// We need the copy bound here because the store will be copied into the selector for each field
|
||||
let definition = quote! {
|
||||
fn transpose(
|
||||
self,
|
||||
) -> #transposed_name #extension_generics where Self: ::std::marker::Copy;
|
||||
};
|
||||
definitions.push(definition);
|
||||
let field_names = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| function_name_from_field(i, field))
|
||||
.collect::<Vec<_>>();
|
||||
// Construct the transposed struct with the fields as stores from the field variables in scope
|
||||
let construct = match &structure.fields {
|
||||
Fields::Named(_) => {
|
||||
quote! { #transposed_name { #(#field_names),* } }
|
||||
}
|
||||
Fields::Unnamed(_) => {
|
||||
quote! { #transposed_name(#(#field_names),*) }
|
||||
}
|
||||
Fields::Unit => {
|
||||
quote! { #transposed_name }
|
||||
}
|
||||
};
|
||||
let implementation = quote! {
|
||||
fn transpose(
|
||||
self,
|
||||
) -> #transposed_name #extension_generics where Self: ::std::marker::Copy {
|
||||
// Convert each field into the corresponding store
|
||||
#(
|
||||
let #field_names = self.#field_names();
|
||||
)*
|
||||
#construct
|
||||
}
|
||||
};
|
||||
implementations.push(implementation);
|
||||
|
||||
// Generate the transposed struct definition
|
||||
let transposed_struct = match &structure.fields {
|
||||
Fields::Named(_) => {
|
||||
quote! { #visibility struct #transposed_name #extension_generics #extension_where_clause {#(#transposed_fields),*} }
|
||||
}
|
||||
Fields::Unnamed(_) => {
|
||||
quote! { #visibility struct #transposed_name #extension_generics (#(#transposed_fields),*) #extension_where_clause; }
|
||||
}
|
||||
Fields::Unit => {
|
||||
quote! {#visibility struct #transposed_name #extension_generics #extension_where_clause;}
|
||||
}
|
||||
};
|
||||
|
||||
// Expand to the extension trait and its implementation for the store alongside the transposed struct
|
||||
Ok(quote! {
|
||||
#visibility trait #extension_trait_name #extension_generics where #extension_where_clause {
|
||||
#(
|
||||
#definitions
|
||||
)*
|
||||
}
|
||||
|
||||
impl #extension_impl_generics #extension_trait_name #extension_generics for dioxus_stores::Store<#struct_name #ty_generics, __Lens> #extension_where_clause {
|
||||
#(
|
||||
#implementations
|
||||
)*
|
||||
}
|
||||
|
||||
#transposed_struct
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_field_methods(
|
||||
field_index: usize,
|
||||
field: &syn::Field,
|
||||
struct_name: &Ident,
|
||||
ty_generics: &syn::TypeGenerics,
|
||||
transposed_fields: &mut Vec<TokenStream2>,
|
||||
definitions: &mut Vec<TokenStream2>,
|
||||
implementations: &mut Vec<TokenStream2>,
|
||||
) {
|
||||
let vis = &field.vis;
|
||||
let field_name = &field.ident;
|
||||
let colon = field.colon_token.as_ref();
|
||||
|
||||
// When we map the field, we need to use either the field name for named fields or the index for unnamed fields.
|
||||
let field_accessor = field_name.as_ref().map_or_else(
|
||||
|| Index::from(field_index).to_token_stream(),
|
||||
|name| name.to_token_stream(),
|
||||
);
|
||||
let function_name = function_name_from_field(field_index, field);
|
||||
let field_type = &field.ty;
|
||||
let store_type = mapped_type(struct_name, ty_generics, field_type);
|
||||
|
||||
transposed_fields.push(quote! { #vis #field_name #colon #store_type });
|
||||
|
||||
// Each field gets its own reactive scope within the child based on the field's index
|
||||
let ordinal = LitInt::new(&field_index.to_string(), field.span());
|
||||
|
||||
let definition = quote! {
|
||||
fn #function_name(
|
||||
self,
|
||||
) -> #store_type;
|
||||
};
|
||||
definitions.push(definition);
|
||||
let implementation = quote! {
|
||||
fn #function_name(
|
||||
self,
|
||||
) -> #store_type {
|
||||
let __map_field: fn(&#struct_name #ty_generics) -> &#field_type = |value| &value.#field_accessor;
|
||||
let __map_mut_field: fn(&mut #struct_name #ty_generics) -> &mut #field_type = |value| &mut value.#field_accessor;
|
||||
// Map the field into a child selector that tracks the field
|
||||
let scope = self.into_selector().child(
|
||||
#ordinal,
|
||||
__map_field,
|
||||
__map_mut_field,
|
||||
);
|
||||
// Convert the selector into a store
|
||||
::std::convert::Into::into(scope)
|
||||
}
|
||||
};
|
||||
implementations.push(implementation);
|
||||
}
|
||||
|
||||
// For enums, we derive two items:
|
||||
// - An extension trait with methods to check if the store is a specific variant and a method
|
||||
// to access the field of that variant if there is only one field
|
||||
// - A transposed version of the enum with all fields wrapped in stores
|
||||
fn derive_store_enum(
|
||||
input: &DeriveInput,
|
||||
structure: &DataEnum,
|
||||
extension_trait_name: Ident,
|
||||
transposed_name: Ident,
|
||||
extension_generics: Generics,
|
||||
) -> syn::Result<TokenStream2> {
|
||||
let enum_name = &input.ident;
|
||||
let variants = &structure.variants;
|
||||
let visibility = &input.vis;
|
||||
|
||||
let generics = &input.generics;
|
||||
let (_, ty_generics, _) = generics.split_for_impl();
|
||||
let (extension_impl_generics, extension_generics, extension_where_clause) =
|
||||
extension_generics.split_for_impl();
|
||||
|
||||
// We collect the definitions and implementations for the extension trait methods along with the types of the fields in the transposed enum
|
||||
// and the match arms for the transposed enum.
|
||||
let mut implementations = Vec::new();
|
||||
let mut definitions = Vec::new();
|
||||
let mut transposed_variants = Vec::new();
|
||||
let mut transposed_match_arms = Vec::new();
|
||||
|
||||
// The generated items that check the variant of the enum need to read the enum which requires these extra bounds
|
||||
let readable_bounds = quote! { __Lens: dioxus_stores::macro_helpers::dioxus_signals::Readable<Target = #enum_name #ty_generics>, #enum_name #ty_generics: 'static };
|
||||
|
||||
for variant in variants {
|
||||
let variant_name = &variant.ident;
|
||||
let snake_case_variant = format_ident!("{}", variant_name.to_string().to_case(Case::Snake));
|
||||
let is_fn = format_ident!("is_{}", snake_case_variant);
|
||||
|
||||
generate_is_variant_method(
|
||||
&is_fn,
|
||||
variant_name,
|
||||
enum_name,
|
||||
readable_bounds.clone(),
|
||||
&mut definitions,
|
||||
&mut implementations,
|
||||
);
|
||||
|
||||
let mut transposed_fields = Vec::new();
|
||||
let mut transposed_field_selectors = Vec::new();
|
||||
let fields = &variant.fields;
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let vis = &field.vis;
|
||||
let field_name = &field.ident;
|
||||
let colon = field.colon_token.as_ref();
|
||||
let field_type = &field.ty;
|
||||
let store_type = mapped_type(enum_name, &ty_generics, field_type);
|
||||
|
||||
// Push the field for the transposed enum
|
||||
transposed_fields.push(quote! { #vis #field_name #colon #store_type });
|
||||
|
||||
// Generate the code to get Store<Field, W> from the enum
|
||||
let select_field = select_enum_variant_field(
|
||||
enum_name,
|
||||
&ty_generics,
|
||||
variant_name,
|
||||
field,
|
||||
i,
|
||||
fields.len(),
|
||||
);
|
||||
|
||||
// If there is only one field, generate a field() -> Option<Store<O, W>> method
|
||||
if fields.len() == 1 {
|
||||
generate_as_variant_method(
|
||||
&is_fn,
|
||||
&snake_case_variant,
|
||||
&select_field,
|
||||
&store_type,
|
||||
&readable_bounds,
|
||||
&mut definitions,
|
||||
&mut implementations,
|
||||
);
|
||||
}
|
||||
|
||||
transposed_field_selectors.push(select_field);
|
||||
}
|
||||
|
||||
// Now that we have the types for the field selectors within the variant,
|
||||
// we can construct the transposed variant and the logic to turn the normal
|
||||
// version of that variant into the store version
|
||||
let field_names = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, field)| function_name_from_field(i, field))
|
||||
.collect::<Vec<_>>();
|
||||
// Turn each field into its store
|
||||
let construct_fields = field_names
|
||||
.iter()
|
||||
.zip(transposed_field_selectors.iter())
|
||||
.map(|(name, selector)| {
|
||||
quote! { let #name = { #selector }; }
|
||||
});
|
||||
// Merge the stores into the variant
|
||||
let construct_variant = match &fields {
|
||||
Fields::Named(_) => {
|
||||
quote! { #transposed_name::#variant_name { #(#field_names),* } }
|
||||
}
|
||||
Fields::Unnamed(_) => {
|
||||
quote! { #transposed_name::#variant_name(#(#field_names),*) }
|
||||
}
|
||||
Fields::Unit => {
|
||||
quote! { #transposed_name::#variant_name }
|
||||
}
|
||||
};
|
||||
let match_arm = quote! {
|
||||
#enum_name::#variant_name { .. } => {
|
||||
#(#construct_fields)*
|
||||
#construct_variant
|
||||
},
|
||||
};
|
||||
transposed_match_arms.push(match_arm);
|
||||
|
||||
// Push the type definition of the variant to the transposed enum
|
||||
let transposed_variant = match &fields {
|
||||
Fields::Named(_) => {
|
||||
quote! { #variant_name {#(#transposed_fields),*} }
|
||||
}
|
||||
Fields::Unnamed(_) => {
|
||||
quote! { #variant_name (#(#transposed_fields),*) }
|
||||
}
|
||||
Fields::Unit => {
|
||||
quote! { #variant_name }
|
||||
}
|
||||
};
|
||||
transposed_variants.push(transposed_variant);
|
||||
}
|
||||
|
||||
let definition = quote! {
|
||||
fn transpose(
|
||||
self,
|
||||
) -> #transposed_name #extension_generics where #readable_bounds, Self: ::std::marker::Copy;
|
||||
};
|
||||
definitions.push(definition);
|
||||
let implementation = quote! {
|
||||
fn transpose(
|
||||
self,
|
||||
) -> #transposed_name #extension_generics where #readable_bounds, Self: ::std::marker::Copy {
|
||||
// We only do a shallow read of the store to get the current variant. We only need to rerun
|
||||
// this match when the variant changes, not when the fields change
|
||||
self.selector().track_shallow();
|
||||
let read = dioxus_stores::macro_helpers::dioxus_signals::ReadableExt::peek(self.selector());
|
||||
match &*read {
|
||||
#(#transposed_match_arms)*
|
||||
// The enum may be #[non_exhaustive]
|
||||
#[allow(unreachable)]
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
};
|
||||
implementations.push(implementation);
|
||||
|
||||
let transposed_enum = quote! { #visibility enum #transposed_name #extension_generics #extension_where_clause {#(#transposed_variants),*} };
|
||||
|
||||
// Expand to the extension trait and its implementation for the store alongside the transposed enum
|
||||
Ok(quote! {
|
||||
#visibility trait #extension_trait_name #extension_generics where #extension_where_clause {
|
||||
#(
|
||||
#definitions
|
||||
)*
|
||||
}
|
||||
|
||||
impl #extension_impl_generics #extension_trait_name #extension_generics for dioxus_stores::Store<#enum_name #ty_generics, __Lens> #extension_where_clause {
|
||||
#(
|
||||
#implementations
|
||||
)*
|
||||
}
|
||||
|
||||
#transposed_enum
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_is_variant_method(
|
||||
is_fn: &Ident,
|
||||
variant_name: &Ident,
|
||||
enum_name: &Ident,
|
||||
readable_bounds: TokenStream2,
|
||||
definitions: &mut Vec<TokenStream2>,
|
||||
implementations: &mut Vec<TokenStream2>,
|
||||
) {
|
||||
// Generate a is_variant method that checks if the store is a specific variant
|
||||
let definition = quote! {
|
||||
fn #is_fn(
|
||||
&self,
|
||||
) -> bool where #readable_bounds;
|
||||
};
|
||||
definitions.push(definition);
|
||||
let implementation = quote! {
|
||||
fn #is_fn(
|
||||
&self,
|
||||
) -> bool where #readable_bounds {
|
||||
// Reading the current variant only tracks the shallow value of the store. Writing to a specific
|
||||
// variant will not cause the variant to change, so we don't need to subscribe deeply
|
||||
self.selector().track_shallow();
|
||||
let ref_self = dioxus_stores::macro_helpers::dioxus_signals::ReadableExt::peek(self.selector());
|
||||
matches!(&*ref_self, #enum_name::#variant_name { .. })
|
||||
}
|
||||
};
|
||||
implementations.push(implementation);
|
||||
}
|
||||
|
||||
/// Generate a method to turn Store<Enum, W> into Option<Store<VariantField, W>> if the variant only has one field.
|
||||
fn generate_as_variant_method(
|
||||
is_fn: &Ident,
|
||||
snake_case_variant: &Ident,
|
||||
select_field: &TokenStream2,
|
||||
store_type: &TokenStream2,
|
||||
readable_bounds: &TokenStream2,
|
||||
definitions: &mut Vec<TokenStream2>,
|
||||
implementations: &mut Vec<TokenStream2>,
|
||||
) {
|
||||
let definition = quote! {
|
||||
fn #snake_case_variant(
|
||||
self,
|
||||
) -> Option<#store_type> where #readable_bounds;
|
||||
};
|
||||
definitions.push(definition);
|
||||
let implementation = quote! {
|
||||
fn #snake_case_variant(
|
||||
self,
|
||||
) -> Option<#store_type> where #readable_bounds {
|
||||
self.#is_fn().then(|| {
|
||||
#select_field
|
||||
})
|
||||
}
|
||||
};
|
||||
implementations.push(implementation);
|
||||
}
|
||||
|
||||
fn select_enum_variant_field(
|
||||
enum_name: &Ident,
|
||||
ty_generics: &syn::TypeGenerics,
|
||||
variant_name: &Ident,
|
||||
field: &Field,
|
||||
field_index: usize,
|
||||
field_count: usize,
|
||||
) -> TokenStream2 {
|
||||
// Generate the match arm for the field
|
||||
let function_name = function_name_from_field(field_index, field);
|
||||
let field_type = &field.ty;
|
||||
let match_field = if field.ident.is_none() {
|
||||
let ignore_before = (0..field_index).map(|_| quote!(_));
|
||||
let ignore_after = (field_index + 1..field_count).map(|_| quote!(_));
|
||||
quote!( ( #(#ignore_before,)* #function_name, #(#ignore_after),* ) )
|
||||
} else {
|
||||
quote!( { #function_name, .. })
|
||||
};
|
||||
let ordinal = LitInt::new(&field_index.to_string(), variant_name.span());
|
||||
quote! {
|
||||
let __map_field: fn(&#enum_name #ty_generics) -> &#field_type = |value| match value {
|
||||
#enum_name::#variant_name #match_field => #function_name,
|
||||
_ => panic!("Selector that was created to match {} read after variant changed", stringify!(#variant_name)),
|
||||
};
|
||||
let __map_mut_field: fn(&mut #enum_name #ty_generics) -> &mut #field_type = |value| match value {
|
||||
#enum_name::#variant_name #match_field => #function_name,
|
||||
_ => panic!("Selector that was created to match {} written after variant changed", stringify!(#variant_name)),
|
||||
};
|
||||
// Each field within the variant gets its own reactive scope. Writing to one field will not notify the enum or
|
||||
// other fields
|
||||
let scope = self.into_selector().child(
|
||||
#ordinal,
|
||||
__map_field,
|
||||
__map_mut_field,
|
||||
);
|
||||
::std::convert::Into::into(scope)
|
||||
}
|
||||
}
|
||||
|
||||
fn function_name_from_field(index: usize, field: &syn::Field) -> Ident {
|
||||
// Generate a function name from the field's identifier or index
|
||||
field
|
||||
.ident
|
||||
.as_ref()
|
||||
.map_or_else(|| format_ident!("field_{index}"), |name| name.clone())
|
||||
}
|
||||
|
||||
fn mapped_type(
|
||||
item: &Ident,
|
||||
ty_generics: &syn::TypeGenerics,
|
||||
field_type: &syn::Type,
|
||||
) -> TokenStream2 {
|
||||
// The zoomed in store type is a MappedMutSignal with function pointers to map the reference to the enum into a reference to the field
|
||||
let write_type = quote! { dioxus_stores::macro_helpers::dioxus_signals::MappedMutSignal<#field_type, __Lens, fn(&#item #ty_generics) -> &#field_type, fn(&mut #item #ty_generics) -> &mut #field_type> };
|
||||
quote! { dioxus_stores::Store<#field_type, #write_type> }
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Parse;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{
|
||||
parse_quote, Ident, ImplItem, ImplItemConst, ImplItemType, ItemImpl, PathArguments, Type,
|
||||
WherePredicate,
|
||||
};
|
||||
|
||||
pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result<TokenStream> {
|
||||
// Extract the type name the store is generic over
|
||||
let store_type = &*input.self_ty;
|
||||
let store = parse_store_type(store_type)?;
|
||||
let store_path = &store.store_path;
|
||||
let item = store.store_generic;
|
||||
let lens_generic = store.store_lens;
|
||||
let visibility = args
|
||||
.visibility
|
||||
.unwrap_or_else(|| syn::Visibility::Inherited);
|
||||
if input.trait_.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
input.trait_.unwrap().1,
|
||||
"The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.",
|
||||
));
|
||||
}
|
||||
|
||||
let extension_name = match args.name {
|
||||
Some(attr) => attr,
|
||||
None => {
|
||||
// Otherwise, generate a name based on the type name
|
||||
let type_name = stringify_type(&item)?;
|
||||
Ident::new(&format!("{}StoreImplExt", type_name), item.span())
|
||||
}
|
||||
};
|
||||
|
||||
// Go through each method in the impl block and add extra bounds to lens as needed
|
||||
let immutable_bounds: WherePredicate = parse_quote!(#lens_generic: dioxus_stores::macro_helpers::dioxus_signals::Readable<Target = #item> + ::std::marker::Copy + 'static);
|
||||
let mutable_bounds: WherePredicate = parse_quote!(#lens_generic: dioxus_stores::macro_helpers::dioxus_signals::Writable<Target = #item> + ::std::marker::Copy + 'static);
|
||||
for item in &mut input.items {
|
||||
let ImplItem::Fn(func) = item else {
|
||||
continue; // Only process function items
|
||||
};
|
||||
// Only add bounds if the function has a self argument
|
||||
let Some(receiver) = func.sig.inputs.iter().find_map(|arg| {
|
||||
if let syn::FnArg::Receiver(receiver) = arg {
|
||||
Some(receiver)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let extra_bounds = match (&receiver.reference, &receiver.mutability) {
|
||||
// The function takes &self
|
||||
(Some(_), None) => &immutable_bounds,
|
||||
// The function takes &mut self
|
||||
(Some(_), Some(_)) => &mutable_bounds,
|
||||
_ => {
|
||||
// If the function doesn't take &self or &mut self, we don't need to add any bounds
|
||||
continue;
|
||||
}
|
||||
};
|
||||
func.sig
|
||||
.generics
|
||||
.make_where_clause()
|
||||
.predicates
|
||||
.push(extra_bounds.clone());
|
||||
}
|
||||
|
||||
// Push a __Lens generic to the impl if it doesn't already exist
|
||||
let contains_lens_generic = input.generics.params.iter().any(|param| {
|
||||
if let syn::GenericParam::Type(ty) = param {
|
||||
ty.ident == lens_generic
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if !contains_lens_generic {
|
||||
input
|
||||
.generics
|
||||
.params
|
||||
.push(parse_quote!(#lens_generic: ::std::marker::Copy + 'static));
|
||||
}
|
||||
|
||||
// quote as the trait definition
|
||||
let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?;
|
||||
|
||||
// Reformat the type to be generic over the lens
|
||||
input.self_ty = Box::new(parse_quote!(#store_path<#item, #lens_generic>));
|
||||
// Change the standalone impl block to a trait impl block
|
||||
let (_, trait_generics, _) = input.generics.split_for_impl();
|
||||
input.trait_ = Some((
|
||||
None,
|
||||
parse_quote!(#extension_name #trait_generics),
|
||||
parse_quote!(for),
|
||||
));
|
||||
|
||||
Ok(quote! {
|
||||
#trait_definition
|
||||
|
||||
#input
|
||||
})
|
||||
}
|
||||
|
||||
fn stringify_type(ty: &Type) -> syn::Result<String> {
|
||||
match ty {
|
||||
Type::Array(type_array) => {
|
||||
let elem = stringify_type(&type_array.elem)?;
|
||||
Ok(format!("Array{elem}"))
|
||||
}
|
||||
Type::Slice(type_slice) => {
|
||||
let elem = stringify_type(&type_slice.elem)?;
|
||||
Ok(format!("Slice{elem}"))
|
||||
}
|
||||
Type::Paren(type_paren) => stringify_type(&type_paren.elem),
|
||||
Type::Path(type_path) => {
|
||||
let last_segment = type_path.path.segments.last().ok_or_else(|| {
|
||||
syn::Error::new_spanned(type_path, "Type path must have at least one segment")
|
||||
})?;
|
||||
let ident = &last_segment.ident;
|
||||
Ok(ident.to_string())
|
||||
}
|
||||
_ => Err(syn::Error::new_spanned(
|
||||
ty,
|
||||
"Unsupported type in store implementation",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_to_trait_body(
|
||||
trait_name: &Ident,
|
||||
item: &ItemImpl,
|
||||
visibility: &syn::Visibility,
|
||||
) -> syn::Result<TokenStream> {
|
||||
let ItemImpl {
|
||||
attrs,
|
||||
defaultness,
|
||||
unsafety,
|
||||
items,
|
||||
..
|
||||
} = item;
|
||||
|
||||
let generics = &item.generics;
|
||||
|
||||
let items = items
|
||||
.iter()
|
||||
.map(item_to_trait_definition)
|
||||
.collect::<syn::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(quote! {
|
||||
#(#attrs)*
|
||||
#visibility #defaultness #unsafety trait #trait_name #generics {
|
||||
#(#items)*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn item_to_trait_definition(item: &syn::ImplItem) -> syn::Result<proc_macro2::TokenStream> {
|
||||
match item {
|
||||
syn::ImplItem::Fn(func) => {
|
||||
let sig = &func.sig;
|
||||
|
||||
Ok(quote! {
|
||||
#sig;
|
||||
})
|
||||
}
|
||||
syn::ImplItem::Const(impl_item_const) => {
|
||||
let ImplItemConst {
|
||||
attrs,
|
||||
const_token,
|
||||
ident,
|
||||
generics,
|
||||
colon_token,
|
||||
ty,
|
||||
semi_token,
|
||||
..
|
||||
} = impl_item_const;
|
||||
|
||||
Ok(quote! {
|
||||
#(#attrs)*
|
||||
#const_token #ident #generics #colon_token #ty #semi_token
|
||||
})
|
||||
}
|
||||
syn::ImplItem::Type(impl_item_type) => {
|
||||
let ImplItemType {
|
||||
attrs,
|
||||
type_token,
|
||||
ident,
|
||||
generics,
|
||||
eq_token,
|
||||
ty,
|
||||
semi_token,
|
||||
..
|
||||
} = impl_item_type;
|
||||
|
||||
Ok(quote! {
|
||||
#(#attrs)*
|
||||
#type_token #ident #generics #eq_token #ty #semi_token
|
||||
})
|
||||
}
|
||||
_ => Err(syn::Error::new_spanned(item, "Unsupported item type")),
|
||||
}
|
||||
}
|
||||
|
||||
fn argument_as_type(arg: &syn::GenericArgument) -> Option<Type> {
|
||||
if let syn::GenericArgument::Type(ty) = arg {
|
||||
Some(ty.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct StorePath {
|
||||
store_path: syn::Path,
|
||||
store_generic: syn::Type,
|
||||
store_lens: syn::Ident,
|
||||
}
|
||||
|
||||
fn parse_store_type(store_type: &Type) -> syn::Result<StorePath> {
|
||||
if let Type::Path(type_path) = store_type {
|
||||
if let Some(segment) = type_path.path.segments.last() {
|
||||
if let PathArguments::AngleBracketed(args) = &segment.arguments {
|
||||
if let Some(store_generics) = args.args.first().and_then(argument_as_type) {
|
||||
let store_lens = args
|
||||
.args
|
||||
.iter()
|
||||
.nth(1)
|
||||
.and_then(argument_as_type)
|
||||
.unwrap_or_else(|| parse_quote!(__Lens));
|
||||
let store_lens = parse_quote!(#store_lens);
|
||||
let mut path_without_generics = type_path.path.clone();
|
||||
for segment in &mut path_without_generics.segments {
|
||||
segment.arguments = PathArguments::None;
|
||||
}
|
||||
return Ok(StorePath {
|
||||
store_path: path_without_generics,
|
||||
store_generic: store_generics,
|
||||
store_lens,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(syn::Error::new_spanned(
|
||||
store_type,
|
||||
"The implementation must be in the form `impl Store<T> {...}`",
|
||||
))
|
||||
}
|
||||
|
||||
/// The args the `#[store]` attribute macro accepts
|
||||
pub(crate) struct ExtendArgs {
|
||||
/// The name of the extension trait generated
|
||||
name: Option<Ident>,
|
||||
/// The visibility of the extension trait
|
||||
visibility: Option<syn::Visibility>,
|
||||
}
|
||||
|
||||
impl Parse for ExtendArgs {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
// Try to parse visibility if it exists
|
||||
let visibility = if input.peek(syn::Token![pub]) {
|
||||
let vis: syn::Visibility = input.parse()?;
|
||||
Some(vis)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Try to parse name = ident if it exists
|
||||
let name = if input.peek(Ident) && input.peek2(syn::Token![=]) {
|
||||
let ident: Ident = input.parse()?;
|
||||
if ident != "name" {
|
||||
return Err(syn::Error::new_spanned(ident, "Expected `name` argument"));
|
||||
}
|
||||
let _eq_token: syn::Token![=] = input.parse()?;
|
||||
let ident: Ident = input.parse()?;
|
||||
Some(ident)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(ExtendArgs { name, visibility })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{parse_macro_input, DeriveInput, ItemImpl};
|
||||
|
||||
use crate::extend::ExtendArgs;
|
||||
|
||||
mod derive;
|
||||
mod extend;
|
||||
|
||||
/// # `derive(Store)`
|
||||
///
|
||||
/// The `Store` macro is used to create an extension trait for stores that makes it possible to access the fields or variants
|
||||
/// of an item as stores.
|
||||
///
|
||||
/// ## Expansion
|
||||
///
|
||||
/// The macro expands to two different items:
|
||||
/// - An extension trait which is implemented for `Store<YourType, W>` with methods to access fields and variants for your type.
|
||||
/// - A transposed version of your type which contains the fields or variants as stores.
|
||||
///
|
||||
/// ### Structs
|
||||
///
|
||||
/// For structs, the store macro generates methods for each field that returns a store scoped to that field and a `transpose` method that returns a struct with all fields as stores:
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// #[derive(Store)]
|
||||
/// struct TodoItem {
|
||||
/// checked: bool,
|
||||
/// contents: String,
|
||||
/// }
|
||||
///
|
||||
/// let store = use_store(|| TodoItem {
|
||||
/// checked: false,
|
||||
/// contents: "Learn about stores".to_string(),
|
||||
/// });
|
||||
///
|
||||
/// // The store macro creates an extension trait with methods for each field
|
||||
/// // that returns a store scoped to that field.
|
||||
/// let checked: Store<bool, _> = store.checked();
|
||||
/// let contents: Store<String, _> = store.contents();
|
||||
///
|
||||
/// // It also generates a `transpose` method returns a variant of your structure
|
||||
/// // with stores wrapping each of your data types. This can be very useful when destructuring
|
||||
/// // or matching your type
|
||||
/// let TodoItemStoreTransposed { checked, contents } = store.transpose();
|
||||
/// let checked: bool = checked();
|
||||
/// let contents: String = contents();
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ### Enums
|
||||
///
|
||||
/// For enums, the store macro generates methods for each variant that checks if the store is that variant. It also generates a `transpose` method that returns an enum with all fields as stores.
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// #[derive(Store, PartialEq, Clone, Debug)]
|
||||
/// enum Enum {
|
||||
/// Foo(String),
|
||||
/// Bar { foo: i32, bar: String },
|
||||
/// }
|
||||
///
|
||||
/// let store = use_store(|| Enum::Foo("Hello".to_string()));
|
||||
/// // The store macro creates an extension trait with methods for each variant to check
|
||||
/// // if the store is that variant.
|
||||
/// let foo: bool = store.is_foo();
|
||||
/// let bar: bool = store.is_bar();
|
||||
///
|
||||
/// // If there is only one field in the variant, it also generates a method to try
|
||||
/// // to downcast the store to that variant.
|
||||
/// let foo: Option<Store<String, _>> = store.foo();
|
||||
/// if let Some(foo) = foo {
|
||||
/// println!("Foo: {foo}");
|
||||
/// }
|
||||
///
|
||||
/// // It also generates a `transpose` method that returns a variant of your enum where all
|
||||
/// // the fields are stores. You can use this to match your enum
|
||||
/// let transposed = store.transpose();
|
||||
/// use EnumStoreTransposed::*;
|
||||
/// match transposed {
|
||||
/// EnumStoreTransposed::Foo(foo) => println!("Foo: {foo}"),
|
||||
/// EnumStoreTransposed::Bar { foo, bar } => {
|
||||
/// let foo: i32 = foo();
|
||||
/// let bar: String = bar();
|
||||
/// println!("Bar: foo = {foo}, bar = {bar}");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Store)]
|
||||
pub fn derive_store(input: TokenStream) -> TokenStream {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let expanded = match derive::derive_store(input) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
// If there was an error, return it as a compile error
|
||||
return err.to_compile_error().into();
|
||||
}
|
||||
};
|
||||
|
||||
// Hand the output tokens back to the compiler
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
/// # `#[store]`
|
||||
///
|
||||
/// The `store` attribute macro is used to create an extension trait for store implementations. The extension traits lets you add
|
||||
/// methods to the store even though the type is not defined in your crate.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// - `pub`: Makes the generated extension trait public. If not provided, the trait will be private.
|
||||
/// - `name = YourExtensionName`: The name of the extension trait. If not provided, it will be generated based on the type name.
|
||||
///
|
||||
/// ## Bounds
|
||||
///
|
||||
/// The generated extension trait will have bounds on the lens generic parameter to ensure it implements `Readable` or `Writable` as needed.
|
||||
///
|
||||
/// - If a method accepts `&self`, the lens will require `Readable` which lets you read the value of the store.
|
||||
/// - If a method accepts `&mut self`, the lens will require `Writable` which lets you change the value of the store.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// #[derive(Store)]
|
||||
/// struct TodoItem {
|
||||
/// checked: bool,
|
||||
/// contents: String,
|
||||
/// }
|
||||
///
|
||||
/// // You can use the store attribute macro to add methods to your stores
|
||||
/// #[store]
|
||||
/// impl<Lens> Store<TodoItem, Lens> {
|
||||
/// // Since this method takes &mut self, the lens will require Writable automatically. It cannot be used
|
||||
/// // with ReadStore<TodoItem>
|
||||
/// fn toggle_checked(&mut self) {
|
||||
/// self.checked().toggle();
|
||||
/// }
|
||||
///
|
||||
/// // Since this method takes &self, the lens will require Readable automatically
|
||||
/// fn checked_contents(&self) -> Option<String> {
|
||||
/// self.checked().cloned().then(|| self.contents().to_string())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let mut store = use_store(|| TodoItem {
|
||||
/// checked: false,
|
||||
/// contents: "Learn about stores".to_string(),
|
||||
/// });
|
||||
///
|
||||
/// // You can use the methods defined in the extension trait
|
||||
/// store.toggle_checked();
|
||||
/// let contents: Option<String> = store.checked_contents();
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn store(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let args = parse_macro_input!(args as ExtendArgs);
|
||||
let input = parse_macro_input!(input as ItemImpl);
|
||||
|
||||
let expanded = match extend::extend_store(args, input) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
// If there was an error, return it as a compile error
|
||||
return err.to_compile_error().into();
|
||||
}
|
||||
};
|
||||
|
||||
// Hand the output tokens back to the compiler
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
Reference in new issue
Block a user