diff --git a/.changes/add-macros-allow-rename-command.md b/.changes/add-macros-allow-rename-command.md new file mode 100644 index 000000000000..bdda4d3427d6 --- /dev/null +++ b/.changes/add-macros-allow-rename-command.md @@ -0,0 +1,6 @@ +--- +"tauri-macros": minor:feat +"tauri": minor:feat +--- + +Add support for the `rename` attribute in the `tauri::command` macro to allow renaming the command to something other than the function name. diff --git a/crates/tauri-macros/src/command/handler.rs b/crates/tauri-macros/src/command/handler.rs index add699b73da2..33143525a7b3 100644 --- a/crates/tauri-macros/src/command/handler.rs +++ b/crates/tauri-macros/src/command/handler.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use quote::format_ident; +use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, Attribute, Ident, Path, Token, @@ -151,14 +151,30 @@ impl From for proc_macro::TokenStream { ) -> Self { let cmd = format_ident!("__tauri_cmd__"); let invoke = format_ident!("__tauri_invoke__"); - let (paths, attrs): (Vec, Vec>) = command_defs - .into_iter() - .map(|def| (def.path, def.attrs)) - .unzip(); + let mut paths: Vec = Vec::new(); + let mut attrs: Vec> = Vec::new(); + let mut command_name_macros: Vec = Vec::new(); + for (def, command) in command_defs.into_iter().zip(commands) { + let path = def.path; + let attrs_vec = def.attrs; + + let mut command_name_macro_path = path.clone(); + let last = command_name_macro_path + .segments + .last_mut() + .expect("path has at least one segment"); + last.ident = format_ident!("__tauri_command_name_{command}"); + + paths.push(path); + attrs.push(attrs_vec); + // Call the macro to get the command name string literal + command_name_macros.push(quote!(#command_name_macro_path!())); + } + quote::quote!(move |#invoke| { let #cmd = #invoke.message.command(); match #cmd { - #(#(#attrs)* stringify!(#commands) => #wrappers!(#paths, #invoke),)* + #(#(#attrs)* #command_name_macros => #wrappers!(#paths, #invoke),)* _ => { return false; }, diff --git a/crates/tauri-macros/src/command/wrapper.rs b/crates/tauri-macros/src/command/wrapper.rs index 261257d639c7..7eb85510e7ad 100644 --- a/crates/tauri-macros/src/command/wrapper.rs +++ b/crates/tauri-macros/src/command/wrapper.rs @@ -40,6 +40,7 @@ struct WrapperAttributes { root: TokenStream2, execution_context: ExecutionContext, argument_case: ArgumentCase, + rename: RenamePolicy, } impl Parse for WrapperAttributes { @@ -48,6 +49,7 @@ impl Parse for WrapperAttributes { root: quote!(::tauri), execution_context: ExecutionContext::Blocking, argument_case: ArgumentCase::Camel, + rename: RenamePolicy::Keep, }; let attrs = Punctuated::::parse_terminated(input)?; @@ -74,6 +76,19 @@ impl Parse for WrapperAttributes { } }; } + } else if v.path.is_ident("rename") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = v.value + { + let lit = s.value(); + wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit)); + } else { + return Err(syn::Error::new( + v.span(), + "expected string literal for rename", + )); + } } else if v.path.is_ident("root") { if let Expr::Lit(ExprLit { lit: Lit::Str(s), @@ -94,7 +109,7 @@ impl Parse for WrapperAttributes { WrapperAttributeKind::Meta(Meta::Path(_)) => { return Err(syn::Error::new( input.span(), - "unexpected input, expected one of `rename_all`, `root`, `async`", + "unexpected input, expected one of `rename_all`, `rename`, `root`, `async`", )); } WrapperAttributeKind::Async => { @@ -120,6 +135,12 @@ enum ArgumentCase { Camel, } +/// The rename policy for the command. +enum RenamePolicy { + Keep, + Rename(TokenStream2), +} + /// The bindings we attach to `tauri::Invoke`. struct Invoke { message: Ident, @@ -138,9 +159,11 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { attrs.execution_context = ExecutionContext::Async; } - // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`. let maybe_macro_export = match &function.vis { - Visibility::Public(_) | Visibility::Restricted(_) => quote!(#[macro_export]), + Visibility::Public(_) | Visibility::Restricted(_) => { + quote!(#[macro_export]) + } _ => TokenStream2::default(), }; @@ -270,6 +293,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { TokenStream2::default() }; + // Always define a hidden macro that returns the externally invoked command name. + // This lets the handler match on the renamed string while the original function + // identifier remains usable in `generate_handler![original_fn_name]`. + let command_name_macro_ident = format_ident!("__tauri_command_name_{}", function.sig.ident); + let command_name_value = if let RenamePolicy::Rename(ref rename) = attrs.rename { + quote!(#rename) + } else { + let ident = &function.sig.ident; + quote!(stringify!(#ident)) + }; + // Rely on rust 2018 edition to allow importing a macro from a path. quote!( #async_command_check @@ -277,6 +311,17 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { #maybe_allow_unused #function + // Command name macro used by the handler for pattern matching. + // This macro returns the command name string literal (renamed or original). + #maybe_allow_unused + #maybe_macro_export + #[doc(hidden)] + macro_rules! #command_name_macro_ident { + () => { + #command_name_value + }; + } + #maybe_allow_unused #maybe_macro_export #[doc(hidden)] @@ -303,7 +348,7 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { // allow the macro to be resolved with the same path as the command function #[allow(unused_imports)] - #visibility use #wrapper; + #visibility use {#wrapper, #command_name_macro_ident}; ) .into() } @@ -467,11 +512,16 @@ fn parse_arg( } let root = &attributes.root; + let command_name = if let RenamePolicy::Rename(r) = &attributes.rename { + quote!(stringify!(#r)) + } else { + quote!(stringify!(#command)) + }; Ok(quote!(#root::ipc::CommandArg::from_command( #root::ipc::CommandItem { plugin: #plugin_name, - name: stringify!(#command), + name: #command_name, key: #key, message: &#message, acl: &#acl, diff --git a/examples/commands/commands.rs b/examples/commands/commands.rs index 93691c59dca1..4b112e283e7d 100644 --- a/examples/commands/commands.rs +++ b/examples/commands/commands.rs @@ -25,3 +25,8 @@ pub fn simple_command(the_argument: String) { pub fn stateful_command(the_argument: Option, state: State<'_, super::MyState>) { println!("{:?} {:?}", the_argument, state.inner()); } + +#[command(rename = "renamed_command_in_mod_new")] +pub fn renamed_command_in_mod() { + println!("renamed command in mod called"); +} diff --git a/examples/commands/index.html b/examples/commands/index.html index aed95c21ca7d..ef95405fd89a 100644 --- a/examples/commands/index.html +++ b/examples/commands/index.html @@ -63,7 +63,11 @@

Tauri Commands

{ name: 'command_arguments_tuple_struct', args: { inlinePerson: ['ferris', 6] } - } + }, + { name: 'renamed_command' }, + { name: 'renamed_command_new' }, + { name: 'renamed_command_in_mod' }, + { name: 'renamed_command_in_mod_new' } ] for (const command of commands) { diff --git a/examples/commands/main.rs b/examples/commands/main.rs index aa0641de093b..ed846b7f7f4e 100644 --- a/examples/commands/main.rs +++ b/examples/commands/main.rs @@ -6,7 +6,7 @@ // we move some basic commands to a separate module just to show it works mod commands; -use commands::{cmd, invoke, message, resolver}; +use commands::{cmd, invoke, message, renamed_command_in_mod, resolver}; use serde::Deserialize; use tauri::{ @@ -188,6 +188,11 @@ fn command_arguments_wild(_: Window) { println!("we saw the wildcard!") } +#[command(rename = "renamed_command_new")] +fn renamed_command() { + println!("renamed command called") +} + #[derive(Deserialize)] struct Person<'a> { name: &'a str, @@ -246,6 +251,8 @@ fn main() { future_simple_command, async_stateful_command, command_arguments_wild, + renamed_command, + renamed_command_in_mod, command_arguments_struct, simple_command_with_result, async_simple_command_snake,