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> }
|
||||
}
|
||||
Reference in new issue
Block a user