-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
wrapper.rs
215 lines (188 loc) · 6.5 KB
/
wrapper.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
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,
};
/// The execution context of the command.
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",
)
})
}
}
/// The bindings we attach to `tauri::Invoke`.
struct Invoke {
message: Ident,
resolver: Ident,
}
/// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
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;
// macros used with `pub use my_macro;` need to be exported with `#[macro_export]`
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__"),
};
// body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
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;
// Rely on rust 2018 edition to allow importing a macro from a path.
quote!(
#function
#maybe_macro_export
#[doc(hidden)]
macro_rules! #wrapper {
// double braces because the item is expected to be a block expression
($path:path, $invoke:ident) => {{
#[allow(unused_imports)]
use ::tauri::command::private::*;
// prevent warnings when the body is a `compile_error!` or if the command has no arguments
#[allow(unused_variables)]
let ::tauri::Invoke { message: #message, resolver: #resolver } = $invoke;
#body
}};
}
// allow the macro to be resolved with the same path as the command function
#[allow(unused_imports)]
#visibility use #wrapper;
)
.into()
}
/// Generates an asynchronous command response from the arguments and return value of a function.
///
/// See the [`tauri::command`] module for all the items and traits that make this possible.
///
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
fn body_async(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
parse_args(function, message).map(|args| {
quote! {
#resolver.respond_async_serialized(async move {
let result = $path(#(#args?),*);
let kind = (&result).async_kind();
kind.future(result).await
});
}
})
}
/// Generates a blocking command response from the arguments and return value of a function.
///
/// See the [`tauri::command`] module for all the items and traits that make this possible.
///
/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
fn body_blocking(function: &ItemFn, invoke: &Invoke) -> syn::Result<TokenStream2> {
let Invoke { message, resolver } = invoke;
let args = parse_args(function, message)?;
// the body of a `match` to early return any argument that wasn't successful in parsing.
let match_body = quote!({
Ok(arg) => arg,
Err(err) => return #resolver.invoke_error(err),
});
Ok(quote! {
let result = $path(#(match #args #match_body),*);
let kind = (&result).blocking_kind();
kind.block(result, #resolver);
})
}
/// Parse all arguments for the command wrapper to use from the signature of the command function.
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()
}
/// Transform a [`FnArg`] into a command argument.
fn parse_arg(command: &Ident, arg: &FnArg, message: &Ident) -> syn::Result<TokenStream2> {
// 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 that allow us to extract some sort of keyed identifier
let mut key = match &mut arg {
Pat::Ident(arg) => arg.ident.to_string(),
Pat::Wild(_) => "".into(), // we always convert to camelCase, so "_" will end up empty anyways
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",
));
}
// snake_case -> camelCase
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,
}
)))
}
/// Convert a snake_case string into camelCase, no underscores will be left.
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
}