feat(core): support generics (especially Param) in #[command] (#1622)

* wip: param argument proof of concept for #[command]

* use macros for automatic type inference in commands

* refactor command for better error handling

* remove redundant ToTokens impl for Wrapper and Handler

* create `StateP` to allow state to use type inference during commands

* wrap State instead of T

* remove accidental edit of attribute

* remove StateP

because we recommend `_: Window<P>` for type inference, the following
function types are now supported:
* Pat::Wild (arg: "_")
* Pat::Struct (arg: final path segment)
* Pat::TupleStruct (arg: final path segment)

* add wildcard, struct, and tuple struct commands to examples

* better unsupported command argument message

* feat(examples): move some commands to a separate module

* add change file

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
chip 2021-05-05 10:32:13 -07:00 committed by GitHub
parent c78db1b399
commit 1453d4bf84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 363 additions and 179 deletions

View File

@ -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

View File

@ -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<P: ::tauri::Params>(_message: ::tauri::InvokeMessage<P>) {
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<Ident> = Default::default();
let mut invoke_arg_types: Vec<Path> = 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<P: ::tauri::Params>(invoke: ::tauri::Invoke<P>) {
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 = <Punctuated<Path, Token![,]>>::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))
},
}
}
}
}

View File

@ -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<Path>,
commands: Vec<Ident>,
wrappers: Vec<Path>,
}
impl Parse for Handler {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
let paths = input.parse_terminated::<Path, Token![,]>(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<Handler> 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()
}
}

View File

@ -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")
}

View File

@ -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<WrapperBody>,
}
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<WrapperBody>) -> 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<Wrapper> 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<Self> {
// 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<TokenStream> {
// 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,
}
)))
}

View File

@ -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.

View File

@ -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 @@
</script>
</body>
</html>
</html>

View File

@ -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<String>, state: tauri::State<'_, super::MyState>) {
println!("{:?} {:?}", argument, state.inner());
}

View File

@ -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<String>, state: tauri::State<'_, MyState>) {
println!("{:?} {:?}", argument, state.inner());
// ------------------------ Commands using Window ------------------------
#[command]
fn window_label(window: Window<impl Params<Label = String>>) {
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<String>, state: tauri::State<'_, MyState>) {
#[command]
async fn async_stateful_command(argument: Option<String>, state: State<'_, MyState>) {
println!("{:?} {:?}", argument, state.inner());
}
@ -39,16 +41,16 @@ async fn async_stateful_command(argument: Option<String>, state: tauri::State<'_
type Result<T> = std::result::Result<T, ()>;
#[tauri::command]
#[command]
fn simple_command_with_result(argument: String) -> Result<String> {
println!("{}", argument);
(!argument.is_empty()).then(|| argument).ok_or(())
}
#[tauri::command]
#[command]
fn stateful_command_with_result(
argument: Option<String>,
state: tauri::State<'_, MyState>,
state: State<'_, MyState>,
) -> Result<String> {
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<String> {
println!("{}", argument);
Ok(argument)
}
#[tauri::command]
#[command]
async fn async_stateful_command_with_result(
argument: Option<String>,
state: tauri::State<'_, MyState>,
state: State<'_, MyState>,
) -> Result<String> {
println!("{:?} {:?}", argument, state.inner());
Ok(argument.unwrap_or_else(|| "".to_string()))
}
// Non-Ident command function arguments
#[command]
fn command_arguments_wild<P: Params>(_: Window<P>) {
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");

View File

@ -53,6 +53,7 @@ mod ui {
#[tauri::command]
fn close_splashscreen<P: Params>(
_: Window<P>, // force inference of P
splashscreen: State<'_, SplashscreenWindow<P>>,
main: State<'_, MainWindow<P>>,
) {