Skip to content

Commit 20fa760

Browse files
mscofield0zakstucke
authored andcommitted
feat: support for custom patch function on stores (leptos-rs#3449)
1 parent 6c48b97 commit 20fa760

File tree

2 files changed

+168
-33
lines changed

2 files changed

+168
-33
lines changed

reactive_stores/src/lib.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,68 @@ mod tests {
885885
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
886886
}
887887

888+
#[tokio::test]
889+
async fn patching_only_notifies_changed_field_with_custom_patch() {
890+
#[derive(Debug, Store, Patch, Default)]
891+
struct CustomTodos {
892+
#[patch(|this, new| *this = new)]
893+
user: String,
894+
todos: Vec<CustomTodo>,
895+
}
896+
897+
#[derive(Debug, Store, Patch, Default)]
898+
struct CustomTodo {
899+
label: String,
900+
completed: bool,
901+
}
902+
903+
_ = any_spawner::Executor::init_tokio();
904+
905+
let combined_count = Arc::new(AtomicUsize::new(0));
906+
907+
let store = Store::new(CustomTodos {
908+
user: "Alice".into(),
909+
todos: vec![],
910+
});
911+
912+
Effect::new_sync({
913+
let combined_count = Arc::clone(&combined_count);
914+
move |prev: Option<()>| {
915+
if prev.is_none() {
916+
println!("first run");
917+
} else {
918+
println!("next run");
919+
}
920+
println!("{:?}", *store.user().read());
921+
combined_count.fetch_add(1, Ordering::Relaxed);
922+
}
923+
});
924+
tick().await;
925+
tick().await;
926+
store.patch(CustomTodos {
927+
user: "Bob".into(),
928+
todos: vec![],
929+
});
930+
tick().await;
931+
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
932+
store.patch(CustomTodos {
933+
user: "Carol".into(),
934+
todos: vec![],
935+
});
936+
tick().await;
937+
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
938+
939+
store.patch(CustomTodos {
940+
user: "Carol".into(),
941+
todos: vec![CustomTodo {
942+
label: "First CustomTodo".into(),
943+
completed: false,
944+
}],
945+
});
946+
tick().await;
947+
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
948+
}
949+
888950
#[derive(Debug, Store)]
889951
pub struct StructWithOption {
890952
opt_field: Option<Todo>,

reactive_stores_macro/src/lib.rs

Lines changed: 106 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use convert_case::{Case, Casing};
22
use proc_macro2::{Span, TokenStream};
3-
use proc_macro_error2::{abort, abort_call_site, proc_macro_error};
3+
use proc_macro_error2::{abort, abort_call_site, proc_macro_error, OptionExt};
44
use quote::{quote, ToTokens};
55
use syn::{
66
parse::{Parse, ParseStream, Parser},
@@ -19,7 +19,7 @@ pub fn derive_store(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1919
}
2020

2121
#[proc_macro_error]
22-
#[proc_macro_derive(Patch, attributes(store))]
22+
#[proc_macro_derive(Patch, attributes(store, patch))]
2323
pub fn derive_patch(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
2424
syn::parse_macro_input!(input as PatchModel)
2525
.into_token_stream()
@@ -537,61 +537,134 @@ fn variant_to_tokens(
537537
struct PatchModel {
538538
pub name: Ident,
539539
pub generics: Generics,
540-
pub fields: Vec<Field>,
540+
pub ty: PatchModelTy,
541+
}
542+
543+
enum PatchModelTy {
544+
Struct {
545+
fields: Vec<Field>,
546+
},
547+
#[allow(dead_code)]
548+
Enum {
549+
variants: Vec<Variant>,
550+
},
541551
}
542552

543553
impl Parse for PatchModel {
544554
fn parse(input: ParseStream) -> Result<Self> {
545555
let input = syn::DeriveInput::parse(input)?;
546556

547-
let syn::Data::Struct(s) = input.data else {
548-
abort_call_site!("only structs can be used with `Patch`");
549-
};
557+
let ty = match input.data {
558+
syn::Data::Struct(s) => {
559+
let fields = match s.fields {
560+
syn::Fields::Unit => {
561+
abort!(s.semi_token, "unit structs are not supported");
562+
}
563+
syn::Fields::Named(fields) => {
564+
fields.named.into_iter().collect::<Vec<_>>()
565+
}
566+
syn::Fields::Unnamed(fields) => {
567+
fields.unnamed.into_iter().collect::<Vec<_>>()
568+
}
569+
};
550570

551-
let fields = match s.fields {
552-
syn::Fields::Unit => {
553-
abort!(s.semi_token, "unit structs are not supported");
571+
PatchModelTy::Struct { fields }
554572
}
555-
syn::Fields::Named(fields) => {
556-
fields.named.into_iter().collect::<Vec<_>>()
573+
syn::Data::Enum(_e) => {
574+
abort_call_site!("only structs can be used with `Patch`");
575+
576+
// TODO: support enums later on
577+
// PatchModelTy::Enum {
578+
// variants: e.variants.into_iter().collect(),
579+
// }
557580
}
558-
syn::Fields::Unnamed(fields) => {
559-
fields.unnamed.into_iter().collect::<Vec<_>>()
581+
_ => {
582+
abort_call_site!(
583+
"only structs and enums can be used with `Store`"
584+
);
560585
}
561586
};
562587

563588
Ok(Self {
564589
name: input.ident,
565590
generics: input.generics,
566-
fields,
591+
ty,
567592
})
568593
}
569594
}
570595

571596
impl ToTokens for PatchModel {
572597
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
573598
let library_path = quote! { reactive_stores };
574-
let PatchModel {
575-
name,
576-
generics,
577-
fields,
578-
} = &self;
599+
let PatchModel { name, generics, ty } = &self;
579600

580-
let fields = fields.iter().enumerate().map(|(idx, field)| {
581-
let field_name = match &field.ident {
582-
Some(ident) => quote! { #ident },
583-
None => quote! { #idx },
584-
};
585-
quote! {
586-
#library_path::PatchField::patch_field(
587-
&mut self.#field_name,
588-
new.#field_name,
589-
&new_path,
590-
notify
591-
);
592-
new_path.replace_last(#idx + 1);
601+
let fields = match ty {
602+
PatchModelTy::Struct { fields } => {
603+
fields.iter().enumerate().map(|(idx, field)| {
604+
let Field {
605+
attrs, ident, ..
606+
} = &field;
607+
let field_name = match &ident {
608+
Some(ident) => quote! { #ident },
609+
None => quote! { #idx },
610+
};
611+
let closure = attrs
612+
.iter()
613+
.find_map(|attr| {
614+
attr.meta.path().is_ident("patch").then(
615+
|| match &attr.meta {
616+
Meta::List(list) => {
617+
match Punctuated::<
618+
ExprClosure,
619+
Comma,
620+
>::parse_terminated
621+
.parse2(list.tokens.clone())
622+
{
623+
Ok(closures) => {
624+
let closure = closures.iter().next().cloned().expect_or_abort("should have ONE closure");
625+
if closure.inputs.len() != 2 {
626+
abort!(closure.inputs, "patch closure should have TWO params as in #[patch(|this, new| ...)]");
627+
}
628+
closure
629+
},
630+
Err(e) => abort!(list, e),
631+
}
632+
}
633+
_ => abort!(attr.meta, "needs to be as `#[patch(|this, new| ...)]`"),
634+
},
635+
)
636+
});
637+
638+
if let Some(closure) = closure {
639+
let params = closure.inputs;
640+
let body = closure.body;
641+
quote! {
642+
if new.#field_name != self.#field_name {
643+
_ = {
644+
let (#params) = (&mut self.#field_name, new.#field_name);
645+
#body
646+
};
647+
notify(&new_path);
648+
}
649+
new_path.replace_last(#idx + 1);
650+
}
651+
} else {
652+
quote! {
653+
#library_path::PatchField::patch_field(
654+
&mut self.#field_name,
655+
new.#field_name,
656+
&new_path,
657+
notify
658+
);
659+
new_path.replace_last(#idx + 1);
660+
}
661+
}
662+
}).collect::<Vec<_>>()
593663
}
594-
});
664+
PatchModelTy::Enum { variants: _ } => {
665+
unreachable!("not implemented currently")
666+
}
667+
};
595668

596669
// read access
597670
tokens.extend(quote! {

0 commit comments

Comments
 (0)