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 (#3449)
This commit is contained in:
parent
035586e5d8
commit
429d1b1262
2 changed files with 168 additions and 33 deletions
|
@ -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>,
|
||||
|
|
|
@ -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! {
|
||||
|
|
Loading…
Reference in a new issue