diff --git a/.changes/command-generics.md b/.changes/command-generics.md new file mode 100644 index 000000000..0eeba6879 --- /dev/null +++ b/.changes/command-generics.md @@ -0,0 +1,6 @@ +--- +"tauri-macros": patch +--- + +`#[command]` now generates a macro instead of a function to allow passing through `Params` and other generics. +`generate_handler!` has been changed to consume the generated `#[command]` macro diff --git a/core/tauri-macros/src/command.rs b/core/tauri-macros/src/command.rs deleted file mode 100644 index 74afbc330..000000000 --- a/core/tauri-macros/src/command.rs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2019-2021 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use proc_macro2::TokenStream; -use quote::{format_ident, quote, TokenStreamExt}; -use syn::{ - parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Token, Type, - Visibility, -}; - -fn fn_wrapper(function: &ItemFn) -> (&Visibility, Ident) { - ( - &function.vis, - format_ident!("{}_wrapper", function.sig.ident), - ) -} - -fn err(function: ItemFn, error_message: &str) -> TokenStream { - let (vis, wrap) = fn_wrapper(&function); - quote! { - #function - - #vis fn #wrap(_message: ::tauri::InvokeMessage

) { - compile_error!(#error_message); - unimplemented!() - } - } -} - -pub fn generate_command(function: ItemFn) -> TokenStream { - let fn_name = function.sig.ident.clone(); - let fn_name_str = fn_name.to_string(); - let (vis, fn_wrapper) = fn_wrapper(&function); - let returns_result = match function.sig.output { - ReturnType::Type(_, ref ty) => match &**ty { - Type::Path(type_path) => { - type_path - .path - .segments - .first() - .map(|seg| seg.ident.to_string()) - == Some("Result".to_string()) - } - _ => false, - }, - ReturnType::Default => false, - }; - - let mut invoke_arg_names: Vec = Default::default(); - let mut invoke_arg_types: Vec = Default::default(); - let mut invoke_args: TokenStream = Default::default(); - - for param in &function.sig.inputs { - let mut arg_name = None; - let mut arg_type = None; - if let FnArg::Typed(arg) = param { - if let Pat::Ident(ident) = arg.pat.as_ref() { - arg_name = Some(ident.ident.clone()); - } - if let Type::Path(path) = arg.ty.as_ref() { - arg_type = Some(path.path.clone()); - } - } - - let arg_name_ = arg_name.unwrap(); - let arg_name_s = arg_name_.to_string(); - - let arg_type = match arg_type { - Some(arg_type) => arg_type, - None => { - return err( - function.clone(), - &format!("invalid type for arg: {}", arg_name_), - ) - } - }; - - let item = quote!(::tauri::command::CommandItem { - name: #fn_name_str, - key: #arg_name_s, - message: &__message, - }); - - invoke_args.append_all(quote!(let #arg_name_ = <#arg_type>::from_command(#item)?;)); - invoke_arg_names.push(arg_name_); - invoke_arg_types.push(arg_type); - } - - let await_maybe = if function.sig.asyncness.is_some() { - quote!(.await) - } else { - quote!() - }; - - // if the command handler returns a Result, - // we just map the values to the ones expected by Tauri - // otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse - // note that all types must implement `serde::Serialize`. - let return_value = if returns_result { - quote!(::core::result::Result::Ok(#fn_name(#(#invoke_arg_names),*)#await_maybe?)) - } else { - quote! { ::core::result::Result::<_, ::tauri::InvokeError>::Ok(#fn_name(#(#invoke_arg_names),*)#await_maybe) } - }; - - // double underscore prefix temporary until underlying scoping issue is fixed (planned) - quote! { - #function - - #vis fn #fn_wrapper(invoke: ::tauri::Invoke

) { - use ::tauri::command::CommandArg; - let ::tauri::Invoke { message: __message, resolver: __resolver } = invoke; - __resolver.respond_async(async move { - #invoke_args - #return_value - }) - } - } -} - -pub fn generate_handler(item: proc_macro::TokenStream) -> TokenStream { - // Get paths of functions passed to macro - let paths = >::parse_terminated - .parse(item) - .expect("generate_handler!: Failed to parse list of command functions"); - - // Get names of functions, used for match statement - let fn_names = paths - .iter() - .map(|p| p.segments.last().unwrap().ident.clone()); - - // Get paths to wrapper functions - let fn_wrappers = paths.iter().map(|func| { - let mut func = func.clone(); - let mut last_segment = func.segments.last_mut().unwrap(); - last_segment.ident = format_ident!("{}_wrapper", last_segment.ident); - func - }); - - quote! { - move |invoke| { - let cmd = invoke.message.command(); - match cmd { - #(stringify!(#fn_names) => #fn_wrappers(invoke),)* - _ => { - invoke.resolver.reject(format!("command {} not found", cmd)) - }, - } - } - } -} diff --git a/core/tauri-macros/src/command/handler.rs b/core/tauri-macros/src/command/handler.rs new file mode 100644 index 000000000..24e1f1644 --- /dev/null +++ b/core/tauri-macros/src/command/handler.rs @@ -0,0 +1,65 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use syn::{ + parse::{Parse, ParseBuffer}, + Ident, Path, Token, +}; + +/// The items parsed from [`generate_handle!`](crate::generate_handle). +pub struct Handler { + paths: Vec, + commands: Vec, + wrappers: Vec, +} + +impl Parse for Handler { + fn parse(input: &ParseBuffer) -> syn::Result { + let paths = input.parse_terminated::(Path::parse)?; + + // parse the command names and wrappers from the passed paths + let (commands, wrappers) = paths + .iter() + .map(|path| { + let mut wrapper = path.clone(); + let last = super::path_to_command(&mut wrapper); + + // the name of the actual command function + let command = last.ident.clone(); + + // set the path to the command function wrapper + last.ident = super::format_command_wrapper(&command); + + (command, wrapper) + }) + .unzip(); + + Ok(Self { + paths: paths.into_iter().collect(), // remove punctuation separators + commands, + wrappers, + }) + } +} + +impl From for proc_macro::TokenStream { + fn from( + Handler { + paths, + commands, + wrappers, + }: Handler, + ) -> Self { + quote::quote!(move |invoke| { + let cmd = invoke.message.command(); + match cmd { + #(stringify!(#commands) => #wrappers!(#paths, invoke),)* + _ => { + invoke.resolver.reject(format!("command {} not found", cmd)) + }, + } + }) + .into() + } +} diff --git a/core/tauri-macros/src/command/mod.rs b/core/tauri-macros/src/command/mod.rs new file mode 100644 index 000000000..b8abe4997 --- /dev/null +++ b/core/tauri-macros/src/command/mod.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::Ident; +use syn::{Path, PathSegment}; + +pub use self::{ + handler::Handler, + wrapper::{Wrapper, WrapperBody}, +}; + +mod handler; +mod wrapper; + +/// The autogenerated wrapper ident. +fn format_command_wrapper(function: &Ident) -> Ident { + quote::format_ident!("__cmd__{}", function) +} + +/// This function will panic if the passed [`syn::Path`] does not have any segments. +fn path_to_command(path: &mut Path) -> &mut PathSegment { + path + .segments + .last_mut() + .expect("parsed syn::Path has no segment") +} diff --git a/core/tauri-macros/src/command/wrapper.rs b/core/tauri-macros/src/command/wrapper.rs new file mode 100644 index 000000000..bb8a84e5e --- /dev/null +++ b/core/tauri-macros/src/command/wrapper.rs @@ -0,0 +1,186 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use std::convert::TryFrom; +use syn::{spanned::Spanned, FnArg, Ident, ItemFn, Pat, ReturnType, Type, Visibility}; + +/// The command wrapper created for a function marked with `#[command]`. +pub struct Wrapper { + function: ItemFn, + visibility: Visibility, + maybe_export: TokenStream, + wrapper: Ident, + body: syn::Result, +} + +impl Wrapper { + /// Create a new [`Wrapper`] from the function and the generated code parsed from the function. + pub fn new(function: ItemFn, body: syn::Result) -> Self { + // macros used with `pub use my_macro;` need to be exported with `#[macro_export]` + let maybe_export = match &function.vis { + Visibility::Public(_) => quote!(#[macro_export]), + _ => Default::default(), + }; + + let visibility = function.vis.clone(); + let wrapper = super::format_command_wrapper(&function.sig.ident); + + Self { + function, + visibility, + maybe_export, + wrapper, + body, + } + } +} + +impl From for proc_macro::TokenStream { + fn from( + Wrapper { + function, + maybe_export, + wrapper, + body, + visibility, + }: Wrapper, + ) -> Self { + // either use the successful body or a `compile_error!` of the error occurred while parsing it. + let body = body + .as_ref() + .map(ToTokens::to_token_stream) + .unwrap_or_else(syn::Error::to_compile_error); + + // we `use` the macro so that other modules can resolve the with the same path as the function. + // this is dependent on rust 2018 edition. + quote!( + #function + #maybe_export + macro_rules! #wrapper { ($path:path, $invoke:ident) => {{ #body }}; } + #visibility use #wrapper; + ) + .into() + } +} + +/// Body of the wrapper that maps the command parameters into callable arguments from [`Invoke`]. +/// +/// This is possible because we require the command parameters to be [`CommandArg`] and use type +/// inference to put values generated from that trait into the arguments of the called command. +/// +/// [`CommandArg`]: https://docs.rs/tauri/*/tauri/command/trait.CommandArg.html +/// [`Invoke`]: https://docs.rs/tauri/*/tauri/struct.Invoke.html +pub struct WrapperBody(TokenStream); + +impl TryFrom<&ItemFn> for WrapperBody { + type Error = syn::Error; + + fn try_from(function: &ItemFn) -> syn::Result { + // the name of the #[command] function is the name of the command to handle + let command = function.sig.ident.clone(); + + // automatically append await when the #[command] function is async + let maybe_await = match function.sig.asyncness { + Some(_) => quote!(.await), + None => Default::default(), + }; + + // todo: detect command return types automatically like params, removes parsing type name + let returns_result = match function.sig.output { + ReturnType::Type(_, ref ty) => match &**ty { + Type::Path(type_path) => type_path + .path + .segments + .first() + .map(|seg| seg.ident == "Result") + .unwrap_or_default(), + _ => false, + }, + ReturnType::Default => false, + }; + + let mut args = Vec::new(); + for param in &function.sig.inputs { + args.push(parse_arg(&command, param)?); + } + + // todo: change this to automatically detect result returns (see above result todo) + // if the command handler returns a Result, + // we just map the values to the ones expected by Tauri + // otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse + // note that all types must implement `serde::Serialize`. + let result = if returns_result { + quote! { + let result = $path(#(#args?),*); + ::core::result::Result::Ok(result #maybe_await?) + } + } else { + quote! { + let result = $path(#(#args?),*); + ::core::result::Result::<_, ::tauri::InvokeError>::Ok(result #maybe_await) + } + }; + + Ok(Self(result)) + } +} + +impl ToTokens for WrapperBody { + fn to_tokens(&self, tokens: &mut TokenStream) { + let body = &self.0; + + // we #[allow(unused_variables)] because a command with no arguments will not use message. + tokens.append_all(quote!( + #[allow(unused_variables)] + let ::tauri::Invoke { message, resolver } = $invoke; + resolver.respond_async(async move { #body }); + )) + } +} + +/// Transform a [`FnArg`] into a command argument. Expects borrowable binding `message` to exist. +fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result { + // we have no use for self arguments + let mut arg = match arg { + FnArg::Typed(arg) => arg.pat.as_ref().clone(), + FnArg::Receiver(arg) => { + return Err(syn::Error::new( + arg.span(), + "unable to use self as a command function parameter", + )) + } + }; + + // we only support patterns supported as arguments to a `ItemFn`. + let key = match &mut arg { + Pat::Ident(arg) => arg.ident.to_string(), + Pat::Wild(_) => "_".into(), + Pat::Struct(s) => super::path_to_command(&mut s.path).ident.to_string(), + Pat::TupleStruct(s) => super::path_to_command(&mut s.path).ident.to_string(), + err => { + return Err(syn::Error::new( + err.span(), + "only named, wildcard, struct, and tuple struct arguments allowed", + )) + } + }; + + // also catch self arguments that use FnArg::Typed syntax + if key == "self" { + return Err(syn::Error::new( + key.span(), + "unable to use self as a command function parameter", + )); + } + + Ok(quote!(::tauri::command::CommandArg::from_command( + ::tauri::command::CommandItem { + name: stringify!(#command), + key: #key, + message: &message, + } + ))) +} diff --git a/core/tauri-macros/src/lib.rs b/core/tauri-macros/src/lib.rs index 8893cc7a3..e279f7c80 100644 --- a/core/tauri-macros/src/lib.rs +++ b/core/tauri-macros/src/lib.rs @@ -5,6 +5,7 @@ extern crate proc_macro; use crate::context::ContextItems; use proc_macro::TokenStream; +use std::convert::TryFrom; use syn::{parse_macro_input, ItemFn}; mod command; @@ -15,14 +16,13 @@ mod context; #[proc_macro_attribute] pub fn command(_attrs: TokenStream, item: TokenStream) -> TokenStream { let function = parse_macro_input!(item as ItemFn); - let gen = command::generate_command(function); - gen.into() + let body = command::WrapperBody::try_from(&function); + command::Wrapper::new(function, body).into() } #[proc_macro] pub fn generate_handler(item: TokenStream) -> TokenStream { - let gen = command::generate_handler(item); - gen.into() + parse_macro_input!(item as command::Handler).into() } /// Reads a Tauri config file and generates a `::tauri::Context` based on the content. diff --git a/examples/commands/public/index.html b/examples/commands/public/index.html index 7c018b348..f9a180ff1 100644 --- a/examples/commands/public/index.html +++ b/examples/commands/public/index.html @@ -24,6 +24,7 @@ const container = document.querySelector('#container') const commands = [ + { name: 'window_label', required: true }, { name: 'simple_command', required: true }, { name: 'stateful_command', required: false }, { name: 'async_simple_command', required: true }, @@ -32,14 +33,18 @@ { name: 'stateful_command_with_result', required: false }, { name: 'async_simple_command_with_result', required: true }, { name: 'async_stateful_command_with_result', required: false }, + { name: 'command_arguments_wild', required: true }, + { name: 'command_arguments_struct', required: true, args: { "Person": { "name": "ferris", age: 6 } } }, + { name: 'command_arguments_tuple_struct', required: true, args: { "InlinePerson": [ "ferris", 6 ] } }, ] - for (command of commands) { + for (const command of commands) { const { name, required } = command + const args = command.args ?? { argument: 'value' } const button = document.createElement('button') button.innerHTML = `Run ${name}`; button.addEventListener("click", function () { - runCommand(name, { argument: 'value' }) + runCommand(name, args) if (!required) { setTimeout(() => { runCommand(name, {}) @@ -52,4 +57,4 @@ - \ No newline at end of file + diff --git a/examples/commands/src-tauri/src/commands.rs b/examples/commands/src-tauri/src/commands.rs new file mode 100644 index 000000000..407a7bb80 --- /dev/null +++ b/examples/commands/src-tauri/src/commands.rs @@ -0,0 +1,13 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[tauri::command] +pub fn simple_command(argument: String) { + println!("{}", argument); +} + +#[tauri::command] +pub fn stateful_command(argument: Option, state: tauri::State<'_, super::MyState>) { + println!("{:?} {:?}", argument, state.inner()); +} diff --git a/examples/commands/src-tauri/src/main.rs b/examples/commands/src-tauri/src/main.rs index 5e438dcdc..cf86e679b 100644 --- a/examples/commands/src-tauri/src/main.rs +++ b/examples/commands/src-tauri/src/main.rs @@ -7,31 +7,33 @@ windows_subsystem = "windows" )] +// we move some basic commands to a separate module just to show it works +mod commands; + +use serde::Deserialize; +use tauri::{command, Params, State, Window}; + #[derive(Debug)] -struct MyState { +pub struct MyState { value: u64, label: String, } -#[tauri::command] -fn simple_command(argument: String) { - println!("{}", argument); -} - -#[tauri::command] -fn stateful_command(argument: Option, state: tauri::State<'_, MyState>) { - println!("{:?} {:?}", argument, state.inner()); +// ------------------------ Commands using Window ------------------------ +#[command] +fn window_label(window: Window>) { + println!("window label: {}", window.label()); } // Async commands -#[tauri::command] +#[command] async fn async_simple_command(argument: String) { println!("{}", argument); } -#[tauri::command] -async fn async_stateful_command(argument: Option, state: tauri::State<'_, MyState>) { +#[command] +async fn async_stateful_command(argument: Option, state: State<'_, MyState>) { println!("{:?} {:?}", argument, state.inner()); } @@ -39,16 +41,16 @@ async fn async_stateful_command(argument: Option, state: tauri::State<'_ type Result = std::result::Result; -#[tauri::command] +#[command] fn simple_command_with_result(argument: String) -> Result { println!("{}", argument); (!argument.is_empty()).then(|| argument).ok_or(()) } -#[tauri::command] +#[command] fn stateful_command_with_result( argument: Option, - state: tauri::State<'_, MyState>, + state: State<'_, MyState>, ) -> Result { println!("{:?} {:?}", argument, state.inner()); argument.ok_or(()) @@ -56,21 +58,47 @@ fn stateful_command_with_result( // Async commands -#[tauri::command] +#[command] async fn async_simple_command_with_result(argument: String) -> Result { println!("{}", argument); Ok(argument) } -#[tauri::command] +#[command] async fn async_stateful_command_with_result( argument: Option, - state: tauri::State<'_, MyState>, + state: State<'_, MyState>, ) -> Result { println!("{:?} {:?}", argument, state.inner()); Ok(argument.unwrap_or_else(|| "".to_string())) } +// Non-Ident command function arguments + +#[command] +fn command_arguments_wild(_: Window

) { + println!("we saw the wildcard!") +} + +#[derive(Deserialize)] +struct Person<'a> { + name: &'a str, + age: u8, +} + +#[command] +fn command_arguments_struct(Person { name, age }: Person) { + println!("received person struct with name: {} | age: {}", name, age) +} + +#[derive(Deserialize)] +struct InlinePerson<'a>(&'a str, u8); + +#[command] +fn command_arguments_tuple_struct(InlinePerson(name, age): InlinePerson) { + println!("received person tuple with name: {} | age: {}", name, age) +} + fn main() { tauri::Builder::default() .manage(MyState { @@ -78,14 +106,18 @@ fn main() { label: "Tauri!".into(), }) .invoke_handler(tauri::generate_handler![ - simple_command, - stateful_command, + window_label, + commands::simple_command, + commands::stateful_command, async_simple_command, async_stateful_command, + command_arguments_wild, + command_arguments_struct, simple_command_with_result, stateful_command_with_result, + command_arguments_tuple_struct, async_simple_command_with_result, - async_stateful_command_with_result + async_stateful_command_with_result, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/examples/splashscreen/src-tauri/src/main.rs b/examples/splashscreen/src-tauri/src/main.rs index 776bf72de..7458eeee6 100644 --- a/examples/splashscreen/src-tauri/src/main.rs +++ b/examples/splashscreen/src-tauri/src/main.rs @@ -53,6 +53,7 @@ mod ui { #[tauri::command] fn close_splashscreen( + _: Window

, // force inference of P splashscreen: State<'_, SplashscreenWindow

>, main: State<'_, MainWindow

>, ) {