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:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-08-05 11:30:21 -07:00
1 parent 5591070e92
commit 58c41f8561
57 files changed
+4958 -436

No files matched your search

+493
View File
@@ -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> }
}
+281
View File
@@ -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 })
}
}
+179
View File
@@ -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)
}