use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseBuffer},
parse_macro_input,
spanned::Spanned,
FnArg, Ident, ItemFn, Pat, Token, Visibility,
};
enum ExecutionContext {
Async,
Blocking,
}
impl Parse for ExecutionContext {
fn parse(input: &ParseBuffer) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self::Blocking);
}
input
.parse::<Token![async]>()
.map(|_| Self::Async)
.map_err(|_| {
syn::Error::new(
input.span(),
"only a single item `async` is currently allowed",
)
})
}
}
struct Invoke {
message: Ident,
resolver: Ident,
}
pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn);
let wrapper = super::format_command_wrapper(&function.sig.ident);
let visibility = &function.vis;
let maybe_macro_export = match &function.vis {
Visibility::Public(_) => quote!(#[macro_export]),
_ => Default::default(),
};
let invoke = Invoke {
message: format_ident!("__tauri_message__"),
resolver: format_ident!("__tauri_resolver__"),
};
let body = syn::parse::<ExecutionContext>(attributes)
.map(|context| match function.sig.asyncness {
Some(_) => ExecutionContext::Async,
None => context,
})
.and_then(|context| match context {
ExecutionContext::Async => body_async(&function, &invoke),
ExecutionContext::Blocking => body_blocking(&function, &invoke),
})
.unwrap_or_else(syn::Error::into_compile_error);
let Invoke { message, resolver } = invoke;
quote!(
#function
#maybe_macro_export
macro_rules! #wrapper {
($path:path, $invoke:ident) => {{
#[allow(unused_variables)]
let ::tauri::Invoke { message: #message, resolver: #resolver } = $invoke;
#body
}};
}
#[allow(unused_imports)]
#visibility use #wrapper;
)
.into()
}
fn body_async(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
parse_args(function, message).map(|args| {
quote! {
#[allow(unused_imports)]
use ::tauri::command::private::*;
#resolver.respond_async_serialized(async move {
let result = $path(#(#args?),*);
let kind = (&result).async_kind();
kind.future(result).await
});
}
})
}
fn body_blocking(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
let args = parse_args(function, message)?;
let match_body = quote!({
Ok(arg) => arg,
Err(err) => return #resolver.invoke_error(err),
});
Ok(quote! {
#[allow(unused_imports)]
use ::tauri::command::private::*;
let result = $path(#(match #args #match_body),*);
let kind = (&result).blocking_kind();
kind.block(result, #resolver);
})
}
fn parse_args(function: &ItemFn, message: &Ident) -> syn::Result<Vec<TokenStream2>> {
function
.sig
.inputs
.iter()
.map(|arg| parse_arg(&function.sig.ident, arg, message))
.collect()
}
fn parse_arg(command: &Ident, arg: &FnArg, message: &Ident) -> syn::Result<TokenStream2> {
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",
))
}
};
let mut 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",
))
}
};
if key == "self" {
return Err(syn::Error::new(
key.span(),
"unable to use self as a command function parameter",
));
}
if key.as_str().contains('_') {
key = snake_case_to_camel_case(key.as_str());
}
Ok(quote!(::tauri::command::CommandArg::from_command(
::tauri::command::CommandItem {
name: stringify!(#command),
key: #key,
message: &#message,
}
)))
}
fn snake_case_to_camel_case(key: &str) -> String {
let mut camel = String::with_capacity(key.len());
let mut to_upper = false;
for c in key.chars() {
match c {
'_' => to_upper = true,
c if std::mem::take(&mut to_upper) => camel.push(c.to_ascii_uppercase()),
c => camel.push(c),
}
}
camel
}