From a52eedf415d07503daa2e3ffc5f321ec976853b5 Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Fri, 13 Jun 2025 23:36:43 -0400 Subject: [PATCH 1/9] macro? --- Cargo.toml | 19 ++++- dioxus-query-macro/Cargo.toml | 12 +++ dioxus-query-macro/src/lib.rs | 150 ++++++++++++++++++++++++++++++++++ examples/hello_world.rs | 29 ++++--- src/lib.rs | 5 ++ 5 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 dioxus-query-macro/Cargo.toml create mode 100644 dioxus-query-macro/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 893bbf0..f43b0bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,12 @@ keywords = ["dioxus", "async", "state", "synchronization"] categories = ["gui", "asynchronous"] [dependencies] -dioxus-lib = { version = "0.6", default-features = false, features = ["macro", "hooks", "signals"] } +dioxus-query-macro = { path = "./dioxus-query-macro" } +dioxus-lib = { version = "0.6", default-features = false, features = [ + "macro", + "hooks", + "signals", +] } futures-util = "0.3.28" warnings = "0.2.1" tokio = { version = "^1", features = ["sync", "time"] } @@ -20,3 +25,15 @@ tokio = { version = "^1", features = ["sync", "time"] } [dev-dependencies] dioxus = { version = "0.6", features = ["desktop"] } tokio = { version = "^1", features = ["time"] } + +[profile] + +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 + +[profile.server-dev] +inherits = "dev" + +[profile.android-dev] +inherits = "dev" diff --git a/dioxus-query-macro/Cargo.toml b/dioxus-query-macro/Cargo.toml new file mode 100644 index 0000000..a332730 --- /dev/null +++ b/dioxus-query-macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-query-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = { version = "2.0.103", features = ["full"] } diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs new file mode 100644 index 0000000..433847b --- /dev/null +++ b/dioxus-query-macro/src/lib.rs @@ -0,0 +1,150 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; + +/// Derive macro for automatically implementing QueryCapability +/// +/// # Example +/// ```rust +/// #[derive(Query)] +/// struct GetUserName { +/// client: FancyClient, +/// } +/// +/// impl GetUserName { +/// async fn run(&self, user_id: &usize) -> Result { +/// // Your async logic here +/// } +/// } +/// ``` +#[proc_macro_derive(Query, attributes(query))] +pub fn derive_query(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Extract the struct fields to understand the captured context + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return syn::Error::new_spanned( + &input, + "Query derive macro only supports structs with named fields", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(&input, "Query derive macro only supports structs") + .to_compile_error() + .into(); + } + }; + + // Find the key type from attributes or default to usize + let key_type = extract_key_type(&input.attrs).unwrap_or_else(|| quote! { usize }); + let ok_type = extract_ok_type(&input.attrs).unwrap_or_else(|| quote! { String }); + let err_type = extract_err_type(&input.attrs).unwrap_or_else(|| quote! { () }); + + // Generate the captured fields initialization + let captured_fields = generate_captured_fields(fields); + + let expanded = quote! { + impl ::dioxus_query::query::QueryCapability for #name { + type Ok = #ok_type; + type Err = #err_type; + type Keys = #key_type; + + async fn run(&self, key: &Self::Keys) -> Result { + self.run(key).await + } + } + + impl ::std::clone::Clone for #name { + fn clone(&self) -> Self { + Self { + #captured_fields + } + } + } + + impl ::std::cmp::PartialEq for #name { + fn eq(&self, other: &Self) -> bool { + true // For simplicity, consider all instances equal + } + } + + impl ::std::cmp::Eq for #name {} + + impl ::std::hash::Hash for #name { + fn hash(&self, state: &mut H) { + stringify!(#name).hash(state); + } + } + }; + + TokenStream::from(expanded) +} + +fn extract_key_type(attrs: &[Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("query") { + if let Ok(meta) = attr.parse_args::() { + if let syn::Meta::NameValue(nv) = meta { + if nv.path.is_ident("key") { + if let syn::Expr::Path(path) = nv.value { + return Some(quote! { #path }); + } + } + } + } + } + } + None +} + +fn extract_ok_type(attrs: &[Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("query") { + if let Ok(meta) = attr.parse_args::() { + if let syn::Meta::NameValue(nv) = meta { + if nv.path.is_ident("ok") { + if let syn::Expr::Path(path) = nv.value { + return Some(quote! { #path }); + } + } + } + } + } + } + None +} + +fn extract_err_type(attrs: &[Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("query") { + if let Ok(meta) = attr.parse_args::() { + if let syn::Meta::NameValue(nv) = meta { + if nv.path.is_ident("err") { + if let syn::Expr::Path(path) = nv.value { + return Some(quote! { #path }); + } + } + } + } + } + } + None +} + +fn generate_captured_fields( + fields: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let field_clones = fields.iter().map(|field| { + let field_name = &field.ident; + quote! { #field_name: self.#field_name.clone() } + }); + + quote! { #(#field_clones),* } +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index bbfc424..aa0e594 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -22,19 +22,19 @@ impl FancyClient { } } -#[derive(Clone, PartialEq, Hash, Eq)] -struct GetUserName(Captured); - -impl QueryCapability for GetUserName { - type Ok = String; - type Err = (); - type Keys = usize; +// NEW: Most ergonomic derive syntax! +#[derive(Query)] +#[query(ok = String, err = (), key = usize)] +struct GetUserName { + client: FancyClient, +} - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserName { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching name of user {user_id}"); sleep(Duration::from_millis(650)).await; match user_id { - 0 => Ok(self.0.name().to_string()), + 0 => Ok(self.client.name().to_string()), _ => Err(()), } } @@ -43,7 +43,12 @@ impl QueryCapability for GetUserName { #[allow(non_snake_case)] #[component] fn User(id: usize) -> Element { - let user_name = use_query(Query::new(id, GetUserName(Captured(FancyClient)))); + let user_name = use_query(Query::new( + id, + GetUserName { + client: FancyClient, + }, + )); println!("Rendering user {id}"); @@ -60,6 +65,8 @@ fn app() -> Element { rsx!( User { id: 0 } User { id: 0 } - button { onclick: refresh, label { "Refresh" } } + button { onclick: refresh, + label { "Refresh" } + } ) } diff --git a/src/lib.rs b/src/lib.rs index 18871ed..11f36bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,13 @@ pub mod captured; pub mod mutation; pub mod query; +// Re-export the derive macro +pub use dioxus_query_macro::Query; + pub mod prelude { pub use crate::captured::*; pub use crate::mutation::*; pub use crate::query::*; + // Re-export the derive macro in prelude too + pub use dioxus_query_macro::Query; } From 0d1671f2cc276aa51b54c9b8e0752e3e713e3fe1 Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Fri, 13 Jun 2025 23:43:01 -0400 Subject: [PATCH 2/9] automatically inferred types --- dioxus-query-macro/src/lib.rs | 5 +++-- examples/hello_world.rs | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs index 433847b..5022195 100644 --- a/dioxus-query-macro/src/lib.rs +++ b/dioxus-query-macro/src/lib.rs @@ -3,6 +3,7 @@ use quote::quote; use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; /// Derive macro for automatically implementing QueryCapability +/// Now automatically infers ok, err, and key types from the run method signature! /// /// # Example /// ```rust @@ -13,7 +14,7 @@ use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; /// /// impl GetUserName { /// async fn run(&self, user_id: &usize) -> Result { -/// // Your async logic here +/// // Types are automatically inferred: key=usize, ok=String, err=() /// } /// } /// ``` @@ -42,7 +43,7 @@ pub fn derive_query(input: TokenStream) -> TokenStream { } }; - // Find the key type from attributes or default to usize + // For now, use defaults - we'll enhance this to parse the impl block later let key_type = extract_key_type(&input.attrs).unwrap_or_else(|| quote! { usize }); let ok_type = extract_ok_type(&input.attrs).unwrap_or_else(|| quote! { String }); let err_type = extract_err_type(&input.attrs).unwrap_or_else(|| quote! { () }); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index aa0e594..d653d44 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -22,9 +22,7 @@ impl FancyClient { } } -// NEW: Most ergonomic derive syntax! #[derive(Query)] -#[query(ok = String, err = (), key = usize)] struct GetUserName { client: FancyClient, } From 13340bd4577869eba418a894eb1ceb2e93bafe24 Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 00:09:24 -0400 Subject: [PATCH 3/9] Revert "automatically inferred types" This reverts commit 0d1671f2cc276aa51b54c9b8e0752e3e713e3fe1. --- dioxus-query-macro/src/lib.rs | 5 ++--- examples/hello_world.rs | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs index 5022195..433847b 100644 --- a/dioxus-query-macro/src/lib.rs +++ b/dioxus-query-macro/src/lib.rs @@ -3,7 +3,6 @@ use quote::quote; use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; /// Derive macro for automatically implementing QueryCapability -/// Now automatically infers ok, err, and key types from the run method signature! /// /// # Example /// ```rust @@ -14,7 +13,7 @@ use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; /// /// impl GetUserName { /// async fn run(&self, user_id: &usize) -> Result { -/// // Types are automatically inferred: key=usize, ok=String, err=() +/// // Your async logic here /// } /// } /// ``` @@ -43,7 +42,7 @@ pub fn derive_query(input: TokenStream) -> TokenStream { } }; - // For now, use defaults - we'll enhance this to parse the impl block later + // Find the key type from attributes or default to usize let key_type = extract_key_type(&input.attrs).unwrap_or_else(|| quote! { usize }); let ok_type = extract_ok_type(&input.attrs).unwrap_or_else(|| quote! { String }); let err_type = extract_err_type(&input.attrs).unwrap_or_else(|| quote! { () }); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d653d44..aa0e594 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -22,7 +22,9 @@ impl FancyClient { } } +// NEW: Most ergonomic derive syntax! #[derive(Query)] +#[query(ok = String, err = (), key = usize)] struct GetUserName { client: FancyClient, } From d479d2412eb7c05cfc50bcaee019218d3a7ae2a0 Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 10:57:18 -0400 Subject: [PATCH 4/9] mutation macro --- dioxus-query-macro/src/lib.rs | 279 ++++++++++++++++++++++++--------- examples/mutate_hello_world.rs | 72 +++++---- src/lib.rs | 2 + 3 files changed, 250 insertions(+), 103 deletions(-) diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs index 433847b..821aead 100644 --- a/dioxus-query-macro/src/lib.rs +++ b/dioxus-query-macro/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; +use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, Lit, Meta, MetaNameValue}; /// Derive macro for automatically implementing QueryCapability /// @@ -19,36 +19,22 @@ use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields}; /// ``` #[proc_macro_derive(Query, attributes(query))] pub fn derive_query(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = &input.ident; - - // Extract the struct fields to understand the captured context - let fields = match &input.data { - Data::Struct(data) => match &data.fields { - Fields::Named(fields) => &fields.named, - _ => { - return syn::Error::new_spanned( - &input, - "Query derive macro only supports structs with named fields", - ) - .to_compile_error() - .into(); - } - }, - _ => { - return syn::Error::new_spanned(&input, "Query derive macro only supports structs") - .to_compile_error() - .into(); - } + let derive_input = parse_macro_input!(input as DeriveInput); + let (name, fields) = match extract_name_and_fields(&derive_input) { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), }; - // Find the key type from attributes or default to usize - let key_type = extract_key_type(&input.attrs).unwrap_or_else(|| quote! { usize }); - let ok_type = extract_ok_type(&input.attrs).unwrap_or_else(|| quote! { String }); - let err_type = extract_err_type(&input.attrs).unwrap_or_else(|| quote! { () }); + let DeriveAttributeValues { + key_type, + ok_type, + err_type, + } = match extract_attribute_values(&derive_input.attrs, "query", quote! {String}) { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), + }; - // Generate the captured fields initialization - let captured_fields = generate_captured_fields(fields); + let captured_fields = generate_captured_fields(&fields); let expanded = quote! { impl ::dioxus_query::query::QueryCapability for #name { @@ -87,64 +73,213 @@ pub fn derive_query(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } -fn extract_key_type(attrs: &[Attribute]) -> Option { - for attr in attrs { - if attr.path().is_ident("query") { - if let Ok(meta) = attr.parse_args::() { - if let syn::Meta::NameValue(nv) = meta { - if nv.path.is_ident("key") { - if let syn::Expr::Path(path) = nv.value { - return Some(quote! { #path }); - } - } +fn generate_captured_fields( + fields: &syn::punctuated::Punctuated, +) -> proc_macro2::TokenStream { + let field_clones = fields.iter().map(|field| { + let field_name = &field.ident; + quote! { #field_name: self.#field_name.clone() } + }); + + quote! { #(#field_clones),* } +} + +#[proc_macro_derive(Mutation, attributes(mutation))] +pub fn derive_mutation(input: TokenStream) -> TokenStream { + let derive_input = parse_macro_input!(input as DeriveInput); + let (name, fields) = match extract_name_and_fields(&derive_input) { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), + }; + + let DeriveAttributeValues { + key_type, + ok_type, + err_type, + } = match extract_attribute_values(&derive_input.attrs, "mutation", quote! {()}) { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), + }; + + let captured_fields = generate_captured_fields(&fields); + + let expanded = quote! { + impl ::dioxus_query::mutation::MutationCapability for #name { + type Ok = #ok_type; + type Err = #err_type; + type Keys = #key_type; + + async fn run(&self, key: &Self::Keys) -> Result { + self.run(key).await + } + + // Add forwarding for on_settled + async fn on_settled(&self, keys: &Self::Keys, result: &Result) { + // This assumes the user has an inherent method `on_settled` with the same signature. + // If not, this will cause a compile error, which is a way to enforce the contract. + // A more advanced macro could check for the method's existence and provide a true default if not found. + self.on_settled(keys, result).await + } + } + + impl ::std::clone::Clone for #name { + fn clone(&self) -> Self { + Self { + #captured_fields } } } - } - None + + impl ::std::cmp::PartialEq for #name { + fn eq(&self, other: &Self) -> bool { + // TODO: Compare fields if they are PartialEq + // For now, to ensure proper re-rendering on state change in captured values, + // we should compare the captured fields if possible. + // However, the original Query derive had `true`, so we'll start there. + // This might need refinement based on how Captured's PartialEq works. + true + } + } + + impl ::std::cmp::Eq for #name {} + + impl ::std::hash::Hash for #name { + fn hash(&self, state: &mut H) { + stringify!(#name).hash(state); + // TODO: Hash fields if they are Hash + } + } + }; + + TokenStream::from(expanded) } -fn extract_ok_type(attrs: &[Attribute]) -> Option { - for attr in attrs { - if attr.path().is_ident("query") { - if let Ok(meta) = attr.parse_args::() { - if let syn::Meta::NameValue(nv) = meta { - if nv.path.is_ident("ok") { - if let syn::Expr::Path(path) = nv.value { - return Some(quote! { #path }); - } - } - } +// Helper function to extract struct name and fields +fn extract_name_and_fields( + input: &DeriveInput, +) -> Result< + ( + &syn::Ident, + &syn::punctuated::Punctuated, + ), + syn::Error, +> { + let name = &input.ident; + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => { + return Err(syn::Error::new_spanned( + input, + "This derive macro only supports structs with named fields", + )); } + }, + _ => { + return Err(syn::Error::new_spanned( + input, + "This derive macro only supports structs", + )); } - } - None + }; + Ok((name, fields)) } -fn extract_err_type(attrs: &[Attribute]) -> Option { +struct DeriveAttributeValues { + key_type: proc_macro2::TokenStream, + ok_type: proc_macro2::TokenStream, + err_type: proc_macro2::TokenStream, +} + +// Helper function to extract attribute values (key, ok, err) +fn extract_attribute_values( + attrs: &[syn::Attribute], + attribute_name: &str, // "query" or "mutation" + default_ok_type: proc_macro2::TokenStream, +) -> Result { + let mut key_type = quote! { usize }; + let mut ok_type = default_ok_type; + let mut err_type = quote! { () }; + for attr in attrs { - if attr.path().is_ident("query") { - if let Ok(meta) = attr.parse_args::() { - if let syn::Meta::NameValue(nv) = meta { - if nv.path.is_ident("err") { - if let syn::Expr::Path(path) = nv.value { - return Some(quote! { #path }); + if attr.path().is_ident(attribute_name) { + match attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + Ok(meta_list) => { + for meta_item in meta_list { + if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta_item { + let ident_name = path.get_ident().map(|i| i.to_string()); + match ident_name.as_deref() { + Some("key") => { + if let syn::Expr::Path(expr_path) = value { + key_type = quote! { #expr_path }; + } else if let syn::Expr::Lit(lit) = value { + if let Lit::Str(lit_str) = lit.lit { + let type_ident: syn::Type = + syn::parse_str(&lit_str.value()).map_err(|e| { + syn::Error::new_spanned( + lit_str, + format!( + "Failed to parse key type string: {}", + e + ), + ) + })?; + key_type = quote! { #type_ident }; + } + } + } + Some("ok") => { + if let syn::Expr::Path(expr_path) = value { + ok_type = quote! { #expr_path }; + } else if let syn::Expr::Lit(lit) = value { + if let Lit::Str(lit_str) = lit.lit { + let type_ident: syn::Type = + syn::parse_str(&lit_str.value()).map_err(|e| { + syn::Error::new_spanned( + lit_str, + format!( + "Failed to parse ok type string: {}", + e + ), + ) + })?; + ok_type = quote! { #type_ident }; + } + } + } + Some("err") => { + if let syn::Expr::Path(expr_path) = value { + err_type = quote! { #expr_path }; + } else if let syn::Expr::Lit(lit) = value { + if let Lit::Str(lit_str) = lit.lit { + let type_ident: syn::Type = + syn::parse_str(&lit_str.value()).map_err(|e| { + syn::Error::new_spanned( + lit_str, + format!( + "Failed to parse err type string: {}", + e + ), + ) + })?; + err_type = quote! { #type_ident }; + } + } + } + _ => {} + } } } } + Err(e) => return Err(e), } } } - None -} - -fn generate_captured_fields( - fields: &syn::punctuated::Punctuated, -) -> proc_macro2::TokenStream { - let field_clones = fields.iter().map(|field| { - let field_name = &field.ident; - quote! { #field_name: self.#field_name.clone() } - }); - - quote! { #(#field_clones),* } + Ok(DeriveAttributeValues { + key_type, + ok_type, + err_type, + }) } diff --git a/examples/mutate_hello_world.rs b/examples/mutate_hello_world.rs index 8b1b715..1f2a96a 100644 --- a/examples/mutate_hello_world.rs +++ b/examples/mutate_hello_world.rs @@ -34,46 +34,46 @@ impl FancyClient { } } -#[derive(Clone, PartialEq, Hash, Eq)] -struct SetUserAge(Captured); - -impl MutationCapability for SetUserAge { - type Ok = i32; - type Err = (); - type Keys = usize; +// NEW: Most ergonomic derive syntax! +#[derive(Query)] // Clone, PartialEq, Eq, Hash are derived by Query +#[query(ok = i32, err = (), key = usize)] +struct GetUserAge { + client: Captured, +} - async fn run(&self, user_id: &Self::Keys) -> Result { - println!("Updating age of user {user_id}"); - sleep(Duration::from_millis(400)).await; - let curr_age = self.0.age(); - self.0.set_age(curr_age + 1); +impl GetUserAge { + async fn run(&self, user_id: &usize) -> Result { + println!("Fetching age of user {user_id}"); + sleep(Duration::from_millis(1000)).await; match user_id { - 0 => Ok(self.0.age()), + 0 => Ok(self.client.age()), // Corrected: No .0 needed due to Deref on Captured _ => Err(()), } } - - async fn on_settled(&self, user_id: &Self::Keys, _result: &Result) { - QueriesStorage::::invalidate_matching(*user_id).await; - } } -#[derive(Clone, PartialEq, Hash, Eq)] -struct GetUserAge(Captured); - -impl QueryCapability for GetUserAge { - type Ok = i32; - type Err = (); - type Keys = usize; +#[derive(Mutation)] +#[mutation(ok = i32, err = (), key = usize)] +struct SetUserAge { + client: Captured, +} - async fn run(&self, user_id: &Self::Keys) -> Result { - println!("Fetching age of user {user_id}"); - sleep(Duration::from_millis(1000)).await; +// User still defines the run logic and any specific lifecycle methods +impl SetUserAge { + async fn run(&self, user_id: &usize) -> Result { + println!("Updating age of user {user_id}"); + sleep(Duration::from_millis(400)).await; + let curr_age = self.client.age(); + self.client.set_age(curr_age + 1); match user_id { - 0 => Ok(self.0.age()), + 0 => Ok(self.client.age()), _ => Err(()), } } + + async fn on_settled(&self, user_id: &usize, _result: &Result) { + QueriesStorage::::invalidate_matching(*user_id).await; + } } #[allow(non_snake_case)] @@ -82,7 +82,13 @@ fn User(id: usize) -> Element { let fancy_client = use_context::(); let user_age = use_query( - Query::new(id, GetUserAge(Captured(fancy_client))).stale_time(Duration::from_secs(4)), + Query::new( + id, + GetUserAge { + client: Captured(fancy_client.clone()), + }, + ) + .stale_time(Duration::from_secs(4)), ); println!("Rendering user {id}"); @@ -95,7 +101,9 @@ fn User(id: usize) -> Element { fn app() -> Element { let fancy_client = use_context_provider(FancyClient::default); - let set_user_age = use_mutation(Mutation::new(SetUserAge(Captured(fancy_client)))); + let set_user_age = use_mutation(Mutation::new(SetUserAge { + client: Captured(fancy_client.clone()), + })); let increase_age = move |_| async move { set_user_age.mutate_async(0).await; @@ -104,6 +112,8 @@ fn app() -> Element { rsx!( User { id: 0 } User { id: 0 } - button { onclick: increase_age, label { "Increse age" } } + button { onclick: increase_age, + label { "Increse age" } + } ) } diff --git a/src/lib.rs b/src/lib.rs index 11f36bd..c0d3e42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod mutation; pub mod query; // Re-export the derive macro +pub use dioxus_query_macro::Mutation; pub use dioxus_query_macro::Query; pub mod prelude { @@ -12,5 +13,6 @@ pub mod prelude { pub use crate::mutation::*; pub use crate::query::*; // Re-export the derive macro in prelude too + pub use dioxus_query_macro::Mutation; pub use dioxus_query_macro::Query; } From 3b0cd8575ec9d93b706bccbbfe4555ed0d0d210b Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 11:20:43 -0400 Subject: [PATCH 5/9] small cleanup and updating interval & extended --- dioxus-query-macro/src/lib.rs | 140 +++++++++++++++++-------------- examples/extended_hello_world.rs | 30 ++++--- examples/interval.rs | 16 ++-- 3 files changed, 99 insertions(+), 87 deletions(-) diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs index 821aead..55069d6 100644 --- a/dioxus-query-macro/src/lib.rs +++ b/dioxus-query-macro/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, Lit, Meta, MetaNameValue}; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaNameValue}; /// Derive macro for automatically implementing QueryCapability /// @@ -34,7 +34,7 @@ pub fn derive_query(input: TokenStream) -> TokenStream { Err(err) => return err.to_compile_error().into(), }; - let captured_fields = generate_captured_fields(&fields); + let (_, clone_impl) = generate_clone_implementation(&name, fields); let expanded = quote! { impl ::dioxus_query::query::QueryCapability for #name { @@ -47,13 +47,7 @@ pub fn derive_query(input: TokenStream) -> TokenStream { } } - impl ::std::clone::Clone for #name { - fn clone(&self) -> Self { - Self { - #captured_fields - } - } - } + #clone_impl impl ::std::cmp::PartialEq for #name { fn eq(&self, other: &Self) -> bool { @@ -73,15 +67,78 @@ pub fn derive_query(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } -fn generate_captured_fields( - fields: &syn::punctuated::Punctuated, -) -> proc_macro2::TokenStream { - let field_clones = fields.iter().map(|field| { - let field_name = &field.ident; - quote! { #field_name: self.#field_name.clone() } - }); +fn extract_name_and_fields( + input: &DeriveInput, +) -> Result< + ( + &syn::Ident, + Option, // Changed to return Fields directly + ), + syn::Error, +> { + let name = &input.ident; + match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => Ok((name, Some(Fields::Named(fields.clone())))), + Fields::Unnamed(fields) => Ok((name, Some(Fields::Unnamed(fields.clone())))), // Handle unnamed fields + Fields::Unit => Ok((name, None)), + }, + _ => Err(syn::Error::new_spanned( + input, + "This derive macro only supports structs", + )), + } +} - quote! { #(#field_clones),* } +fn generate_clone_implementation( + name: &syn::Ident, + fields_option: Option, +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + match fields_option { + Some(Fields::Named(fields)) => { + let field_clones = fields.named.iter().map(|field| { + let field_name = &field.ident; + quote! { #field_name: self.#field_name.clone() } + }); + let captured_fields = quote! { #(#field_clones),* }; + let clone_impl = quote! { + impl ::std::clone::Clone for #name { + fn clone(&self) -> Self { + Self { + #captured_fields + } + } + } + }; + (captured_fields, clone_impl) + } + Some(Fields::Unnamed(fields)) => { + let field_clones = fields.unnamed.iter().enumerate().map(|(i, _field)| { + let index = syn::Index::from(i); + quote! { self.#index.clone() } + }); + let captured_fields = quote! { #(#field_clones),* }; + let clone_impl = quote! { + impl ::std::clone::Clone for #name { + fn clone(&self) -> Self { + Self(#captured_fields) + } + } + }; + (captured_fields, clone_impl) + } + Some(Fields::Unit) | None => { + let captured_fields = quote! {}; + let clone_impl = quote! { + impl ::std::clone::Clone for #name { + fn clone(&self) -> Self { + Self + } + } + }; + (captured_fields, clone_impl) + } + } } #[proc_macro_derive(Mutation, attributes(mutation))] @@ -101,7 +158,7 @@ pub fn derive_mutation(input: TokenStream) -> TokenStream { Err(err) => return err.to_compile_error().into(), }; - let captured_fields = generate_captured_fields(&fields); + let (_, clone_impl) = generate_clone_implementation(&name, fields); let expanded = quote! { impl ::dioxus_query::mutation::MutationCapability for #name { @@ -122,22 +179,11 @@ pub fn derive_mutation(input: TokenStream) -> TokenStream { } } - impl ::std::clone::Clone for #name { - fn clone(&self) -> Self { - Self { - #captured_fields - } - } - } + #clone_impl impl ::std::cmp::PartialEq for #name { fn eq(&self, other: &Self) -> bool { - // TODO: Compare fields if they are PartialEq - // For now, to ensure proper re-rendering on state change in captured values, - // we should compare the captured fields if possible. - // However, the original Query derive had `true`, so we'll start there. - // This might need refinement based on how Captured's PartialEq works. - true + true // For simplicity, consider all instances equal } } @@ -146,7 +192,6 @@ pub fn derive_mutation(input: TokenStream) -> TokenStream { impl ::std::hash::Hash for #name { fn hash(&self, state: &mut H) { stringify!(#name).hash(state); - // TODO: Hash fields if they are Hash } } }; @@ -154,37 +199,6 @@ pub fn derive_mutation(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } -// Helper function to extract struct name and fields -fn extract_name_and_fields( - input: &DeriveInput, -) -> Result< - ( - &syn::Ident, - &syn::punctuated::Punctuated, - ), - syn::Error, -> { - let name = &input.ident; - let fields = match &input.data { - Data::Struct(data) => match &data.fields { - Fields::Named(fields) => &fields.named, - _ => { - return Err(syn::Error::new_spanned( - input, - "This derive macro only supports structs with named fields", - )); - } - }, - _ => { - return Err(syn::Error::new_spanned( - input, - "This derive macro only supports structs", - )); - } - }; - Ok((name, fields)) -} - struct DeriveAttributeValues { key_type: proc_macro2::TokenStream, ok_type: proc_macro2::TokenStream, diff --git a/examples/extended_hello_world.rs b/examples/extended_hello_world.rs index a16a7f5..c22eb53 100644 --- a/examples/extended_hello_world.rs +++ b/examples/extended_hello_world.rs @@ -26,15 +26,12 @@ impl FancyClient { } } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = String, err = (), key = usize)] struct GetUserName(Captured); -impl QueryCapability for GetUserName { - type Ok = String; - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserName { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching name of user {user_id}"); sleep(Duration::from_millis(650)).await; match user_id { @@ -44,15 +41,12 @@ impl QueryCapability for GetUserName { } } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = u8, err = (), key = usize)] struct GetUserAge(Captured); -impl QueryCapability for GetUserAge { - type Ok = u8; - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserAge { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching age of user {user_id}"); sleep(Duration::from_millis(1000)).await; match user_id { @@ -97,8 +91,12 @@ fn app() -> Element { }; rsx!( - button { onclick: new_replica, label { "New replica" } } - button { onclick: refresh, label { "Refresh" } } + button { onclick: new_replica, + label { "New replica" } + } + button { onclick: refresh, + label { "Refresh" } + } for i in 0..replicas() { User { key: "{i}", id: 0 } } diff --git a/examples/interval.rs b/examples/interval.rs index 01f8d4f..7d536da 100644 --- a/examples/interval.rs +++ b/examples/interval.rs @@ -13,15 +13,13 @@ fn main() { launch(app); } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] // Added Query derive and other necessary derives +#[query(ok = String, err = (), key = usize)] // Added query attribute struct GetUserName; -impl QueryCapability for GetUserName { - type Ok = String; - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +// User still defines the run logic +impl GetUserName { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching name of user {user_id}"); sleep(Duration::from_millis(650)).await; match user_id { @@ -55,7 +53,9 @@ fn app() -> Element { }; rsx!( - button { onclick: new_replica, label { "New replica" } } + button { onclick: new_replica, + label { "New replica" } + } for i in 0..replicas() { User { key: "{i}", id: 0 } } From e7d20c1284de76f15b8cdb3a2405d9a9c42a04db Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 11:24:25 -0400 Subject: [PATCH 6/9] more example update --- examples/composable.rs | 22 ++++++++-------------- examples/direct_invalidation.rs | 11 ++++------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/examples/composable.rs b/examples/composable.rs index aac9704..60b9750 100644 --- a/examples/composable.rs +++ b/examples/composable.rs @@ -26,15 +26,12 @@ impl FancyClient { } } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = String, err = (), key = usize)] struct GetUserName(Captured); -impl QueryCapability for GetUserName { - type Ok = String; - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserName { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching name of user {user_id}"); sleep(Duration::from_millis(650)).await; match user_id { @@ -44,15 +41,12 @@ impl QueryCapability for GetUserName { } } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = "(String, u8)", err = (), key = usize)] struct GetUserInfo(Captured); -impl QueryCapability for GetUserInfo { - type Ok = (String, u8); - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserInfo { + async fn run(&self, user_id: &usize) -> Result<(String, u8), ()> { let name = QueriesStorage::get( GetQuery::new(*user_id, GetUserName(self.0.clone())) .stale_time(Duration::from_secs(30)) diff --git a/examples/direct_invalidation.rs b/examples/direct_invalidation.rs index 299d7fd..a114d90 100644 --- a/examples/direct_invalidation.rs +++ b/examples/direct_invalidation.rs @@ -12,15 +12,12 @@ fn main() { launch(app); } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = SystemTime, err = (), key = "()")] struct GetTime; -impl QueryCapability for GetTime { - type Ok = SystemTime; - type Err = (); - type Keys = (); - - async fn run(&self, _: &Self::Keys) -> Result { +impl GetTime { + async fn run(&self, _: &()) -> Result { Ok(SystemTime::now()) } } From b7550c7a03bbbd4d0577df5b3effbdf5e403cece Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 11:25:56 -0400 Subject: [PATCH 7/9] suspense example update --- examples/composable.rs | 4 +++- examples/suspense.rs | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/composable.rs b/examples/composable.rs index 60b9750..b00d954 100644 --- a/examples/composable.rs +++ b/examples/composable.rs @@ -87,6 +87,8 @@ fn app() -> Element { rsx!( User { id: 0 } User { id: 0 } - button { onclick: refresh, label { "Refresh" } } + button { onclick: refresh, + label { "Refresh" } + } ) } diff --git a/examples/suspense.rs b/examples/suspense.rs index ce40f06..b8b419f 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -22,15 +22,12 @@ impl FancyClient { } } -#[derive(Clone, PartialEq, Hash, Eq)] +#[derive(Query)] +#[query(ok = String, err = (), key = usize)] struct GetUserName(Captured); -impl QueryCapability for GetUserName { - type Ok = String; - type Err = (); - type Keys = usize; - - async fn run(&self, user_id: &Self::Keys) -> Result { +impl GetUserName { + async fn run(&self, user_id: &usize) -> Result { println!("Fetching name of user {user_id}"); sleep(Duration::from_millis(650)).await; match user_id { From 00b0638c39610b81c21814ef8d59c9582de62536 Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 12:07:26 -0400 Subject: [PATCH 8/9] macro cleanup --- dioxus-query-macro/src/lib.rs | 219 ++++++++++++++++------------------ 1 file changed, 101 insertions(+), 118 deletions(-) diff --git a/dioxus-query-macro/src/lib.rs b/dioxus-query-macro/src/lib.rs index 55069d6..a8f9ab5 100644 --- a/dioxus-query-macro/src/lib.rs +++ b/dioxus-query-macro/src/lib.rs @@ -19,6 +19,59 @@ use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaNameValue /// ``` #[proc_macro_derive(Query, attributes(query))] pub fn derive_query(input: TokenStream) -> TokenStream { + derive_capability(input, CapabilityType::Query) +} + +#[proc_macro_derive(Mutation, attributes(mutation))] +pub fn derive_mutation(input: TokenStream) -> TokenStream { + derive_capability(input, CapabilityType::Mutation) +} + +#[derive(Clone, Copy)] +enum CapabilityType { + Query, + Mutation, +} + +impl CapabilityType { + fn attribute_name(&self) -> &'static str { + match self { + CapabilityType::Query => "query", + CapabilityType::Mutation => "mutation", + } + } + + fn default_ok_type(&self) -> proc_macro2::TokenStream { + match self { + CapabilityType::Query => quote! { String }, + CapabilityType::Mutation => quote! { () }, + } + } + + fn trait_path(&self) -> proc_macro2::TokenStream { + match self { + CapabilityType::Query => quote! { ::dioxus_query::query::QueryCapability }, + CapabilityType::Mutation => quote! { ::dioxus_query::mutation::MutationCapability }, + } + } + + fn additional_methods(&self) -> proc_macro2::TokenStream { + match self { + CapabilityType::Query => quote! {}, + CapabilityType::Mutation => quote! { + // Add forwarding for on_settled + async fn on_settled(&self, keys: &Self::Keys, result: &Result) { + // This assumes the user has an inherent method `on_settled` with the same signature. + // If not, this will cause a compile error, which is a way to enforce the contract. + // A more advanced macro could check for the method's existence and provide a true default if not found. + self.on_settled(keys, result).await + } + }, + } + } +} + +fn derive_capability(input: TokenStream, capability_type: CapabilityType) -> TokenStream { let derive_input = parse_macro_input!(input as DeriveInput); let (name, fields) = match extract_name_and_fields(&derive_input) { Ok(val) => val, @@ -29,15 +82,22 @@ pub fn derive_query(input: TokenStream) -> TokenStream { key_type, ok_type, err_type, - } = match extract_attribute_values(&derive_input.attrs, "query", quote! {String}) { + } = match extract_attribute_values( + &derive_input.attrs, + capability_type.attribute_name(), + capability_type.default_ok_type(), + ) { Ok(val) => val, Err(err) => return err.to_compile_error().into(), }; let (_, clone_impl) = generate_clone_implementation(&name, fields); + let trait_path = capability_type.trait_path(); + let additional_methods = capability_type.additional_methods(); + let common_trait_impls = generate_common_trait_impls(&name); let expanded = quote! { - impl ::dioxus_query::query::QueryCapability for #name { + impl #trait_path for #name { type Ok = #ok_type; type Err = #err_type; type Keys = #key_type; @@ -45,10 +105,20 @@ pub fn derive_query(input: TokenStream) -> TokenStream { async fn run(&self, key: &Self::Keys) -> Result { self.run(key).await } + + #additional_methods } #clone_impl + #common_trait_impls + }; + + TokenStream::from(expanded) +} +/// Generate common trait implementations (PartialEq, Eq, Hash) for both Query and Mutation +fn generate_common_trait_impls(name: &syn::Ident) -> proc_macro2::TokenStream { + quote! { impl ::std::cmp::PartialEq for #name { fn eq(&self, other: &Self) -> bool { true // For simplicity, consider all instances equal @@ -62,9 +132,7 @@ pub fn derive_query(input: TokenStream) -> TokenStream { stringify!(#name).hash(state); } } - }; - - TokenStream::from(expanded) + } } fn extract_name_and_fields( @@ -141,63 +209,7 @@ fn generate_clone_implementation( } } -#[proc_macro_derive(Mutation, attributes(mutation))] -pub fn derive_mutation(input: TokenStream) -> TokenStream { - let derive_input = parse_macro_input!(input as DeriveInput); - let (name, fields) = match extract_name_and_fields(&derive_input) { - Ok(val) => val, - Err(err) => return err.to_compile_error().into(), - }; - let DeriveAttributeValues { - key_type, - ok_type, - err_type, - } = match extract_attribute_values(&derive_input.attrs, "mutation", quote! {()}) { - Ok(val) => val, - Err(err) => return err.to_compile_error().into(), - }; - - let (_, clone_impl) = generate_clone_implementation(&name, fields); - - let expanded = quote! { - impl ::dioxus_query::mutation::MutationCapability for #name { - type Ok = #ok_type; - type Err = #err_type; - type Keys = #key_type; - - async fn run(&self, key: &Self::Keys) -> Result { - self.run(key).await - } - - // Add forwarding for on_settled - async fn on_settled(&self, keys: &Self::Keys, result: &Result) { - // This assumes the user has an inherent method `on_settled` with the same signature. - // If not, this will cause a compile error, which is a way to enforce the contract. - // A more advanced macro could check for the method's existence and provide a true default if not found. - self.on_settled(keys, result).await - } - } - - #clone_impl - - impl ::std::cmp::PartialEq for #name { - fn eq(&self, other: &Self) -> bool { - true // For simplicity, consider all instances equal - } - } - - impl ::std::cmp::Eq for #name {} - - impl ::std::hash::Hash for #name { - fn hash(&self, state: &mut H) { - stringify!(#name).hash(state); - } - } - }; - - TokenStream::from(expanded) -} struct DeriveAttributeValues { key_type: proc_macro2::TokenStream, @@ -225,63 +237,9 @@ fn extract_attribute_values( if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta_item { let ident_name = path.get_ident().map(|i| i.to_string()); match ident_name.as_deref() { - Some("key") => { - if let syn::Expr::Path(expr_path) = value { - key_type = quote! { #expr_path }; - } else if let syn::Expr::Lit(lit) = value { - if let Lit::Str(lit_str) = lit.lit { - let type_ident: syn::Type = - syn::parse_str(&lit_str.value()).map_err(|e| { - syn::Error::new_spanned( - lit_str, - format!( - "Failed to parse key type string: {}", - e - ), - ) - })?; - key_type = quote! { #type_ident }; - } - } - } - Some("ok") => { - if let syn::Expr::Path(expr_path) = value { - ok_type = quote! { #expr_path }; - } else if let syn::Expr::Lit(lit) = value { - if let Lit::Str(lit_str) = lit.lit { - let type_ident: syn::Type = - syn::parse_str(&lit_str.value()).map_err(|e| { - syn::Error::new_spanned( - lit_str, - format!( - "Failed to parse ok type string: {}", - e - ), - ) - })?; - ok_type = quote! { #type_ident }; - } - } - } - Some("err") => { - if let syn::Expr::Path(expr_path) = value { - err_type = quote! { #expr_path }; - } else if let syn::Expr::Lit(lit) = value { - if let Lit::Str(lit_str) = lit.lit { - let type_ident: syn::Type = - syn::parse_str(&lit_str.value()).map_err(|e| { - syn::Error::new_spanned( - lit_str, - format!( - "Failed to parse err type string: {}", - e - ), - ) - })?; - err_type = quote! { #type_ident }; - } - } - } + Some("key") => key_type = parse_type_value(value)?, + Some("ok") => ok_type = parse_type_value(value)?, + Some("err") => err_type = parse_type_value(value)?, _ => {} } } @@ -297,3 +255,28 @@ fn extract_attribute_values( err_type, }) } + +/// Parse a type value from either a path expression or a string literal +fn parse_type_value(value: syn::Expr) -> Result { + match value { + syn::Expr::Path(expr_path) => Ok(quote! { #expr_path }), + syn::Expr::Tuple(tuple_expr) => { + // Handle unit type () and tuple types + Ok(quote! { #tuple_expr }) + } + syn::Expr::Lit(lit) => { + if let Lit::Str(lit_str) = lit.lit { + let type_ident: syn::Type = syn::parse_str(&lit_str.value()).map_err(|e| { + syn::Error::new_spanned(lit_str, format!("Failed to parse type string: {}", e)) + })?; + Ok(quote! { #type_ident }) + } else { + Err(syn::Error::new_spanned( + lit, + "Expected string literal for type", + )) + } + } + _ => Err(syn::Error::new_spanned(value, "Expected path, tuple, or string literal")), + } +} From 87ee0f4e573af6585cb2303bd36f938a7ea132be Mon Sep 17 00:00:00 2001 From: Sabin Regmi Date: Sat, 14 Jun 2025 12:09:17 -0400 Subject: [PATCH 9/9] clean up dx gen --- Cargo.toml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f43b0bd..ddf4c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,15 +25,3 @@ tokio = { version = "^1", features = ["sync", "time"] } [dev-dependencies] dioxus = { version = "0.6", features = ["desktop"] } tokio = { version = "^1", features = ["time"] } - -[profile] - -[profile.wasm-dev] -inherits = "dev" -opt-level = 1 - -[profile.server-dev] -inherits = "dev" - -[profile.android-dev] -inherits = "dev"