diff --git a/Cargo.toml b/Cargo.toml index ace363f..5f2b235 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"] } 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..a8f9ab5 --- /dev/null +++ b/dioxus-query-macro/src/lib.rs @@ -0,0 +1,282 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, MetaNameValue}; + +/// 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 { + 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, + Err(err) => return err.to_compile_error().into(), + }; + + let DeriveAttributeValues { + key_type, + ok_type, + err_type, + } = 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 #trait_path 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 + } + + #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 + } + } + + impl ::std::cmp::Eq for #name {} + + impl ::std::hash::Hash for #name { + fn hash(&self, state: &mut H) { + stringify!(#name).hash(state); + } + } + } +} + +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", + )), + } +} + +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) + } + } +} + + + +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(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") => key_type = parse_type_value(value)?, + Some("ok") => ok_type = parse_type_value(value)?, + Some("err") => err_type = parse_type_value(value)?, + _ => {} + } + } + } + } + Err(e) => return Err(e), + } + } + } + Ok(DeriveAttributeValues { + key_type, + ok_type, + 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")), + } +} diff --git a/examples/composable.rs b/examples/composable.rs index aac9704..b00d954 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)) @@ -93,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/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()) } } 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/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/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 } } 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/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 { diff --git a/src/lib.rs b/src/lib.rs index 18871ed..c0d3e42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,15 @@ pub mod captured; 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 { 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::Mutation; + pub use dioxus_query_macro::Query; }