2
0
Fork 0
mirror of https://github.com/leptos-rs/leptos synced 2025-02-03 07:23:26 +00:00

feat: support for custom patch function on stores ()

This commit is contained in:
Michael Scofield 2025-01-17 19:10:44 +01:00 committed by GitHub
parent 035586e5d8
commit 429d1b1262
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 33 deletions
reactive_stores/src
reactive_stores_macro/src

View file

@ -885,6 +885,68 @@ mod tests {
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn patching_only_notifies_changed_field_with_custom_patch() {
#[derive(Debug, Store, Patch, Default)]
struct CustomTodos {
#[patch(|this, new| *this = new)]
user: String,
todos: Vec<CustomTodo>,
}
#[derive(Debug, Store, Patch, Default)]
struct CustomTodo {
label: String,
completed: bool,
}
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(CustomTodos {
user: "Alice".into(),
todos: vec![],
});
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.user().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.patch(CustomTodos {
user: "Bob".into(),
todos: vec![],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
store.patch(CustomTodos {
user: "Carol".into(),
todos: vec![],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
store.patch(CustomTodos {
user: "Carol".into(),
todos: vec![CustomTodo {
label: "First CustomTodo".into(),
completed: false,
}],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[derive(Debug, Store)]
pub struct StructWithOption {
opt_field: Option<Todo>,

View file

@ -1,6 +1,6 @@
use convert_case::{Case, Casing};
use proc_macro2::{Span, TokenStream};
use proc_macro_error2::{abort, abort_call_site, proc_macro_error};
use proc_macro_error2::{abort, abort_call_site, proc_macro_error, OptionExt};
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream, Parser},
@ -19,7 +19,7 @@ pub fn derive_store(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
}
#[proc_macro_error]
#[proc_macro_derive(Patch, attributes(store))]
#[proc_macro_derive(Patch, attributes(store, patch))]
pub fn derive_patch(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
syn::parse_macro_input!(input as PatchModel)
.into_token_stream()
@ -537,33 +537,58 @@ fn variant_to_tokens(
struct PatchModel {
pub name: Ident,
pub generics: Generics,
pub fields: Vec<Field>,
pub ty: PatchModelTy,
}
enum PatchModelTy {
Struct {
fields: Vec<Field>,
},
#[allow(dead_code)]
Enum {
variants: Vec<Variant>,
},
}
impl Parse for PatchModel {
fn parse(input: ParseStream) -> Result<Self> {
let input = syn::DeriveInput::parse(input)?;
let syn::Data::Struct(s) = input.data else {
abort_call_site!("only structs can be used with `Patch`");
};
let ty = match input.data {
syn::Data::Struct(s) => {
let fields = match s.fields {
syn::Fields::Unit => {
abort!(s.semi_token, "unit structs are not supported");
}
syn::Fields::Named(fields) => {
fields.named.into_iter().collect::<Vec<_>>()
}
syn::Fields::Unnamed(fields) => {
fields.unnamed.into_iter().collect::<Vec<_>>()
}
};
let fields = match s.fields {
syn::Fields::Unit => {
abort!(s.semi_token, "unit structs are not supported");
PatchModelTy::Struct { fields }
}
syn::Fields::Named(fields) => {
fields.named.into_iter().collect::<Vec<_>>()
syn::Data::Enum(_e) => {
abort_call_site!("only structs can be used with `Patch`");
// TODO: support enums later on
// PatchModelTy::Enum {
// variants: e.variants.into_iter().collect(),
// }
}
syn::Fields::Unnamed(fields) => {
fields.unnamed.into_iter().collect::<Vec<_>>()
_ => {
abort_call_site!(
"only structs and enums can be used with `Store`"
);
}
};
Ok(Self {
name: input.ident,
generics: input.generics,
fields,
ty,
})
}
}
@ -571,27 +596,75 @@ impl Parse for PatchModel {
impl ToTokens for PatchModel {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let library_path = quote! { reactive_stores };
let PatchModel {
name,
generics,
fields,
} = &self;
let PatchModel { name, generics, ty } = &self;
let fields = fields.iter().enumerate().map(|(idx, field)| {
let field_name = match &field.ident {
Some(ident) => quote! { #ident },
None => quote! { #idx },
};
quote! {
#library_path::PatchField::patch_field(
&mut self.#field_name,
new.#field_name,
&new_path,
notify
);
new_path.replace_last(#idx + 1);
let fields = match ty {
PatchModelTy::Struct { fields } => {
fields.iter().enumerate().map(|(idx, field)| {
let Field {
attrs, ident, ..
} = &field;
let field_name = match &ident {
Some(ident) => quote! { #ident },
None => quote! { #idx },
};
let closure = attrs
.iter()
.find_map(|attr| {
attr.meta.path().is_ident("patch").then(
|| match &attr.meta {
Meta::List(list) => {
match Punctuated::<
ExprClosure,
Comma,
>::parse_terminated
.parse2(list.tokens.clone())
{
Ok(closures) => {
let closure = closures.iter().next().cloned().expect_or_abort("should have ONE closure");
if closure.inputs.len() != 2 {
abort!(closure.inputs, "patch closure should have TWO params as in #[patch(|this, new| ...)]");
}
closure
},
Err(e) => abort!(list, e),
}
}
_ => abort!(attr.meta, "needs to be as `#[patch(|this, new| ...)]`"),
},
)
});
if let Some(closure) = closure {
let params = closure.inputs;
let body = closure.body;
quote! {
if new.#field_name != self.#field_name {
_ = {
let (#params) = (&mut self.#field_name, new.#field_name);
#body
};
notify(&new_path);
}
new_path.replace_last(#idx + 1);
}
} else {
quote! {
#library_path::PatchField::patch_field(
&mut self.#field_name,
new.#field_name,
&new_path,
notify
);
new_path.replace_last(#idx + 1);
}
}
}).collect::<Vec<_>>()
}
});
PatchModelTy::Enum { variants: _ } => {
unreachable!("not implemented currently")
}
};
// read access
tokens.extend(quote! {