Skip to content

Commit

Permalink
use macros for automatic type inference in commands
Browse files Browse the repository at this point in the history
  • Loading branch information
chippers committed May 4, 2021
1 parent 17bf38d commit a6baec5
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 124 deletions.
238 changes: 117 additions & 121 deletions core/tauri-macros/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,64 @@
// SPDX-License-Identifier: MIT

use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use quote::{format_ident, quote};
use syn::{
parse::Parser, punctuated::Punctuated, FnArg, GenericArgument, Ident, ItemFn, Pat, Path,
PathArguments, ReturnType, Token, Type, Visibility,
parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Token, Type,
Visibility,
};

fn fn_wrapper(function: &ItemFn) -> (&Visibility, Ident) {
/// The autogenerated wrapper ident
fn fn_wrapper(function: &Ident) -> Ident {
format_ident!("__cmd__{}", function)
}

/// Automatically insert await when the command we are wrapping is async
fn maybe_await(function: &ItemFn) -> TokenStream {
match function.sig.asyncness {
Some(_) => quote!(.await),
None => Default::default(),
}
}

/// If the autogenerated wrapper needs to be explicitly exported (if the function is pub)
fn maybe_export(function: &ItemFn) -> (&Visibility, TokenStream) {
(
&function.vis,
format_ident!("{}_wrapper", function.sig.ident),
match &function.vis {
Visibility::Public(_) => quote!(#[macro_export]),
_ => Default::default(),
},
)
}

/// Generate a minimal skeleton with all the required items named but unimplemented.
///
/// Prevents extraneous errors when some item inside the macro errors.
fn err(function: ItemFn, error_message: &str) -> TokenStream {
let (vis, wrap) = fn_wrapper(&function);
let wrap = fn_wrapper(&function.sig.ident);
let (vis, maybe_export) = maybe_export(&function);
quote! {
#function

#vis fn #wrap<P: ::tauri::Params>(_message: ::tauri::InvokeMessage<P>) {
compile_error!(#error_message);
unimplemented!()
#maybe_export
macro_rules! #wrap {
($path:path, $invoke:ident) => {{
compile_error!(#error_message);
unimplemented!()
}};
}

#vis use #wrap;
}
}

pub fn generate_command(function: ItemFn) -> TokenStream {
/*let mut params = quote!(::tauri::Params);
let mut fail = true;
if let FnArg::Typed(pat) = window {
if let Type::Path(ty) = &*pat.ty {
let last = match ty.path.segments.last() {
Some(last) => last,
None => {
return err(
function,
"found a type path (expected to be window) without any segments (how?)",
)
}
};
let angle = match &last.arguments {
PathArguments::AngleBracketed(args) => args,
_ => {
return err(
function,
"type path (expected to be window) needs to have an angled generic argument",
)
}
};
if angle.args.len() != 1 {
return err(
function,
"type path (expected to be window) needs to have exactly one generic argument",
);
}
if let Some(GenericArgument::Type(Type::ImplTrait(ty))) = angle.args.first() {
if ty.bounds.len() > 1 {
return err(
function,
"only a single bound is allowed for the window in #[command(with_window)], ::tauri::Params"
);
}
if let Some(bound) = ty.bounds.first() {
params = bound.to_token_stream();
fail = false;
};
}
}
}
if fail {
return err(
function,
"only impl trait is supported for now... this should not have gotten merged",
);
}*/

let fn_name = function.sig.ident.clone();
let fn_name_str = fn_name.to_string();
let (vis, fn_wrapper) = fn_wrapper(&function);
let fn_wrapper = fn_wrapper(&fn_name);
let (vis, maybe_export) = maybe_export(&function);
let maybe_await = maybe_await(&function);

// 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) => {
Expand All @@ -101,73 +76,91 @@ pub fn generate_command(function: ItemFn) -> TokenStream {
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 args: Result<Vec<_>, _> = function
.sig
.inputs
.iter()
.map(|param| {
// todo: clean up this error logic later by wrapping the function in a result and having a
// dedicated error type
let arg = match param {
FnArg::Typed(arg) => match arg.pat.as_ref() {
Pat::Ident(arg) => {
if arg.ident == "self" {
return Err(err(
function.clone(),
"unable to use self as a command function parameter",
));
} else {
arg.ident.clone()
}
}
_ => {
return Err(err(
function.clone(),
"command parameters expected to be a typed pattern",
))
}
},
FnArg::Receiver(_) => {
return Err(err(
function.clone(),
"unable to use self as a command function parameter",
))
}
};

let await_maybe = if function.sig.asyncness.is_some() {
quote!(.await)
} else {
quote!()
Ok(quote!(::tauri::command::CommandArg::from_command(
::tauri::command::CommandItem {
name: stringify!(#fn_name),
key: stringify!(#arg),
message: &message,
}
)))
})
.collect();

// we have no way to recover if we didn't successfully parse the arguments, return compile error
let args = match args {
Ok(args) => args,
Err(err) => return err,
};

// 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 return_value = if returns_result {
quote!(::core::result::Result::Ok(#fn_name(#(#invoke_arg_names),*)#await_maybe?))
quote! {
let result = $path(#(#args?),*);
::core::result::Result::Ok(result #maybe_await?)
}
} else {
quote! { ::core::result::Result::<_, ::tauri::InvokeError>::Ok(#fn_name(#(#invoke_arg_names),*)#await_maybe) }
quote! {
let result = $path(#(#args?),*);
::core::result::Result::<_, ::tauri::InvokeError>::Ok(result #maybe_await)
}
};

// double underscore prefix temporary until underlying scoping issue is fixed (planned)
quote! {
#function
#vis fn #fn_wrapper(invoke: ::tauri::Invoke<impl ::tauri::Params>) {
use ::tauri::command::CommandArg;
let ::tauri::Invoke { message: __message, resolver: __resolver } = invoke;
__resolver.respond_async(async move {
#invoke_args
#return_value
})

#[doc(hidden)]
#maybe_export
macro_rules! #fn_wrapper {
($path:path, $invoke:ident) => {{
#[allow(unused_variables)]
let ::tauri::Invoke { message, resolver } = $invoke;

resolver.respond_async(async move {
#return_value
});
}};
}

#[doc(hidden)]
#vis use #fn_wrapper;
}
}

Expand All @@ -180,21 +173,24 @@ pub fn generate_handler(item: proc_macro::TokenStream) -> TokenStream {
// Get names of functions, used for match statement
let fn_names = paths
.iter()
.map(|p| p.segments.last().unwrap().ident.clone());
.map(|p| p.segments.last().unwrap().ident.to_string());

// 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);
last_segment.ident = fn_wrapper(&last_segment.ident);
func
});

// turn it into an iterator so that `Punctuated` will only output the paths
let paths = paths.iter();

quote! {
move |invoke| {
let cmd = invoke.message.command();
match cmd {
#(stringify!(#fn_names) => #fn_wrappers(invoke),)*
#(#fn_names => #fn_wrappers!(#paths, invoke),)*
_ => {
invoke.resolver.reject(format!("command {} not found", cmd))
},
Expand Down
5 changes: 3 additions & 2 deletions examples/commands/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ <h1>Tauri Commands</h1>

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 },
Expand All @@ -34,7 +35,7 @@ <h1>Tauri Commands</h1>
{ name: 'async_stateful_command_with_result', required: false },
]

for (command of commands) {
for (const command of commands) {
const { name, required } = command
const button = document.createElement('button')
button.innerHTML = `Run ${name}`;
Expand All @@ -52,4 +53,4 @@ <h1>Tauri Commands</h1>
</script>
</body>

</html>
</html>
9 changes: 8 additions & 1 deletion examples/commands/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
windows_subsystem = "1s"
)]

#[derive(Debug)]
Expand All @@ -23,6 +23,12 @@ fn stateful_command(argument: Option<String>, state: tauri::State<'_, MyState>)
println!("{:?} {:?}", argument, state.inner());
}

// ------------------------ Commands using Window ------------------------
#[tauri::command]
fn window_label(window: tauri::Window<impl tauri::Params<Label = String>>) {
println!("window label: {}", window.label());
}

// Async commands

#[tauri::command]
Expand Down Expand Up @@ -78,6 +84,7 @@ fn main() {
label: "Tauri!".into(),
})
.invoke_handler(tauri::generate_handler![
window_label,
simple_command,
stateful_command,
async_simple_command,
Expand Down

0 comments on commit a6baec5

Please sign in to comment.