Files
dioxus/packages/stores-macro/src/derive.rs
T
Evan AlmloffandJonathan Kelley 58c41f8561 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>
2025-08-05 11:30:21 -07:00

494 lines
18 KiB
Rust

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> }
}