Skip to content

Commit 1f2e7a3

Browse files
nklaymanchipperslucasfernog
authored
feat(core): improved command matching with macros, fixes #1157 (#1301)
Co-authored-by: chip <chip@chip.sh> Co-authored-by: Lucas Nogueira <lucas@tauri.studio> Co-authored-by: Lucas Fernandes Nogueira <lucasfernandesnog@gmail.com>
1 parent 6951cae commit 1f2e7a3

File tree

13 files changed

+210
-88
lines changed

13 files changed

+210
-88
lines changed

.changes/simple-command-matching.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-macros": minor
3+
---
4+
5+
Added new macros to simplify the creation of commands that can be called by the webview.

cli/tauri.js/templates/src-tauri/src/cmd.rs

-10
This file was deleted.

cli/tauri.js/templates/src-tauri/src/main.rs

-18
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,11 @@
33
windows_subsystem = "windows"
44
)]
55

6-
mod cmd;
7-
86
#[derive(tauri::FromTauriContext)]
97
struct Context;
108

119
fn main() {
1210
tauri::AppBuilder::<Context>::new()
13-
.invoke_handler(|_webview, arg| async move {
14-
use cmd::Cmd::*;
15-
match serde_json::from_str(&arg) {
16-
Err(e) => Err(e.into()),
17-
Ok(command) => {
18-
match command {
19-
// definitions for your custom commands from Cmd here
20-
MyCustomCommand { argument } => {
21-
// your command code
22-
println!("{}", argument);
23-
}
24-
}
25-
Ok(().into())
26-
}
27-
}
28-
})
2911
.build()
3012
.unwrap()
3113
.run();

tauri-macros/src/command.rs

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::{
4+
parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Meta, NestedMeta, Pat, Path,
5+
ReturnType, Token, Type,
6+
};
7+
8+
pub fn generate_command(attrs: Vec<NestedMeta>, function: ItemFn) -> TokenStream {
9+
// Check if "with_manager" attr was passed to macro
10+
let uses_manager = attrs.iter().any(|a| {
11+
if let NestedMeta::Meta(Meta::Path(path)) = a {
12+
path
13+
.get_ident()
14+
.map(|i| *i == "with_manager")
15+
.unwrap_or(false)
16+
} else {
17+
false
18+
}
19+
});
20+
21+
let fn_name = function.sig.ident.clone();
22+
let fn_name_str = fn_name.to_string();
23+
let fn_wrapper = format_ident!("{}_wrapper", fn_name);
24+
let returns_result = match function.sig.output {
25+
ReturnType::Type(_, ref ty) => match &**ty {
26+
Type::Path(type_path) => {
27+
type_path
28+
.path
29+
.segments
30+
.first()
31+
.map(|seg| seg.ident.to_string())
32+
== Some("Result".to_string())
33+
}
34+
_ => false,
35+
},
36+
ReturnType::Default => false,
37+
};
38+
39+
// Split function args into names and types
40+
let (mut names, mut types): (Vec<Ident>, Vec<Path>) = function
41+
.sig
42+
.inputs
43+
.iter()
44+
.map(|param| {
45+
let mut arg_name = None;
46+
let mut arg_type = None;
47+
if let FnArg::Typed(arg) = param {
48+
if let Pat::Ident(ident) = arg.pat.as_ref() {
49+
arg_name = Some(ident.ident.clone());
50+
}
51+
if let Type::Path(path) = arg.ty.as_ref() {
52+
arg_type = Some(path.path.clone());
53+
}
54+
}
55+
(
56+
arg_name.clone().unwrap(),
57+
arg_type.unwrap_or_else(|| panic!("Invalid type for arg \"{}\"", arg_name.unwrap())),
58+
)
59+
})
60+
.unzip();
61+
62+
// If function doesn't take the webview manager, wrapper just takes webview manager generically and ignores it
63+
// Otherwise the wrapper uses the specific type from the original function declaration
64+
let mut manager_arg_type = quote!(::tauri::WebviewManager<A>);
65+
let mut application_ext_generic = quote!(<A: ::tauri::ApplicationExt>);
66+
let manager_arg_maybe = match types.first() {
67+
Some(first_type) if uses_manager => {
68+
// Give wrapper specific type
69+
manager_arg_type = quote!(#first_type);
70+
// Generic is no longer needed
71+
application_ext_generic = quote!();
72+
// Remove webview manager arg from list so it isn't expected as arg from JS
73+
types.drain(0..1);
74+
names.drain(0..1);
75+
// Tell wrapper to pass webview manager to original function
76+
quote!(_manager,)
77+
}
78+
// Tell wrapper not to pass webview manager to original function
79+
_ => quote!(),
80+
};
81+
let await_maybe = if function.sig.asyncness.is_some() {
82+
quote!(.await)
83+
} else {
84+
quote!()
85+
};
86+
87+
// if the command handler returns a Result,
88+
// we just map the values to the ones expected by Tauri
89+
// otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse
90+
// note that all types must implement `serde::Serialize`.
91+
let return_value = if returns_result {
92+
quote! {
93+
match #fn_name(#manager_arg_maybe #(parsed_args.#names),*)#await_maybe {
94+
Ok(value) => ::core::result::Result::Ok(value.into()),
95+
Err(e) => ::core::result::Result::Err(tauri::Error::Command(::serde_json::to_value(e)?)),
96+
}
97+
}
98+
} else {
99+
quote! { ::core::result::Result::Ok(#fn_name(#manager_arg_maybe #(parsed_args.#names),*)#await_maybe.into()) }
100+
};
101+
102+
quote! {
103+
#function
104+
pub async fn #fn_wrapper #application_ext_generic(_manager: #manager_arg_type, arg: ::serde_json::Value) -> ::tauri::Result<::tauri::InvokeResponse> {
105+
#[derive(::serde::Deserialize)]
106+
#[serde(rename_all = "camelCase")]
107+
struct ParsedArgs {
108+
#(#names: #types),*
109+
}
110+
let parsed_args: ParsedArgs = ::serde_json::from_value(arg).map_err(|e| ::tauri::Error::InvalidArgs(#fn_name_str, e))?;
111+
#return_value
112+
}
113+
}
114+
}
115+
116+
pub fn generate_handler(item: proc_macro::TokenStream) -> TokenStream {
117+
// Get paths of functions passed to macro
118+
let paths = <Punctuated<Path, Token![,]>>::parse_terminated
119+
.parse(item)
120+
.expect("generate_handler!: Failed to parse list of command functions");
121+
122+
// Get names of functions, used for match statement
123+
let fn_names = paths
124+
.iter()
125+
.map(|p| p.segments.last().unwrap().ident.clone());
126+
127+
// Get paths to wrapper functions
128+
let fn_wrappers = paths.iter().map(|func| {
129+
let mut func = func.clone();
130+
let mut last_segment = func.segments.last_mut().unwrap();
131+
last_segment.ident = format_ident!("{}_wrapper", last_segment.ident);
132+
func
133+
});
134+
135+
quote! {
136+
|webview_manager, arg| async move {
137+
let dispatch: ::std::result::Result<::tauri::DispatchInstructions, ::serde_json::Error> =
138+
::serde_json::from_str(&arg);
139+
match dispatch {
140+
Err(e) => Err(e.into()),
141+
Ok(dispatch) => {
142+
match dispatch.cmd.as_str() {
143+
#(stringify!(#fn_names) => #fn_wrappers(webview_manager, dispatch.args).await,)*
144+
_ => Err(tauri::Error::UnknownApi(None)),
145+
}
146+
}
147+
}
148+
}
149+
}
150+
}

tauri-macros/src/lib.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
extern crate proc_macro;
22
use proc_macro::TokenStream;
3-
use syn::{parse_macro_input, DeriveInput};
3+
use syn::{parse_macro_input, AttributeArgs, DeriveInput, ItemFn};
44

5+
mod command;
56
mod error;
67
mod expand;
78
mod include_dir;
@@ -17,3 +18,17 @@ pub fn load_context(ast: TokenStream) -> TokenStream {
1718
.unwrap_or_else(|e| e.into_compile_error(&name))
1819
.into()
1920
}
21+
22+
#[proc_macro_attribute]
23+
pub fn command(attrs: TokenStream, item: TokenStream) -> TokenStream {
24+
let function = parse_macro_input!(item as ItemFn);
25+
let attrs = parse_macro_input!(attrs as AttributeArgs);
26+
let gen = command::generate_command(attrs, function);
27+
gen.into()
28+
}
29+
30+
#[proc_macro]
31+
pub fn generate_handler(item: TokenStream) -> TokenStream {
32+
let gen = command::generate_handler(item);
33+
gen.into()
34+
}
+10-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
use serde::Deserialize;
2+
use tauri::command;
23

34
#[derive(Debug, Deserialize)]
45
pub struct RequestBody {
56
id: i32,
67
name: String,
78
}
89

9-
#[derive(Deserialize)]
10-
#[serde(tag = "cmd", rename_all = "camelCase")]
11-
pub enum Cmd {
12-
LogOperation {
13-
event: String,
14-
payload: Option<String>,
15-
},
16-
PerformRequest {
17-
endpoint: String,
18-
body: RequestBody,
19-
},
10+
#[command]
11+
pub fn log_operation(event: String, payload: Option<String>) {
12+
println!("{} {:?}", event, payload);
13+
}
14+
15+
#[command]
16+
pub fn perform_request(endpoint: String, body: RequestBody) -> String {
17+
println!("{} {:?}", endpoint, body);
18+
"message response".into()
2019
}

tauri/examples/api/src-tauri/src/main.rs

+4-16
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,10 @@ fn main() {
3131
.expect("failed to emit");
3232
});
3333
})
34-
.invoke_handler(|_webview_manager, arg| async move {
35-
use cmd::Cmd::*;
36-
match serde_json::from_str(&arg) {
37-
Err(e) => Err(e.into()),
38-
Ok(command) => match command {
39-
LogOperation { event, payload } => {
40-
println!("{} {:?}", event, payload);
41-
Ok(().into())
42-
}
43-
PerformRequest { endpoint, body } => {
44-
println!("{} {:?}", endpoint, body);
45-
Ok("message response".into())
46-
}
47-
},
48-
}
49-
})
34+
.invoke_handler(tauri::generate_handler![
35+
cmd::log_operation,
36+
cmd::perform_request
37+
])
5038
.build()
5139
.unwrap()
5240
.run();

tauri/examples/api/src/components/Communication.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
99
function log() {
1010
invoke({
11-
cmd: "logOperation",
11+
cmd: "log_operation",
1212
event: "tauri-click",
1313
payload: "this payload is optional because we used Option in Rust"
1414
});
1515
}
1616
1717
function performRequest() {
1818
invoke({
19-
cmd: "performRequest",
19+
cmd: "perform_request",
2020
endpoint: "dummy endpoint arg",
2121
body: {
2222
id: 5,

tauri/examples/helloworld/src-tauri/src/cmd.rs

-10
This file was deleted.

tauri/examples/helloworld/src-tauri/src/main.rs

+6-18
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,18 @@
33
windows_subsystem = "windows"
44
)]
55

6-
mod cmd;
7-
86
#[derive(tauri::FromTauriContext)]
97
#[config_path = "examples/helloworld/src-tauri/tauri.conf.json"]
108
struct Context;
119

10+
#[tauri::command]
11+
fn my_custom_command(argument: String) {
12+
println!("{}", argument);
13+
}
14+
1215
fn main() {
1316
tauri::AppBuilder::<Context>::new()
14-
.invoke_handler(|_webview, arg| async move {
15-
use cmd::Cmd::*;
16-
match serde_json::from_str(&arg) {
17-
Err(e) => Err(e.into()),
18-
Ok(command) => {
19-
match command {
20-
// definitions for your custom commands from Cmd here
21-
MyCustomCommand { argument } => {
22-
// your command code
23-
println!("{}", argument);
24-
}
25-
}
26-
Ok(().into())
27-
}
28-
}
29-
})
17+
.invoke_handler(tauri::generate_handler![my_custom_command])
3018
.build()
3119
.unwrap()
3220
.run();

tauri/src/app.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use futures::future::BoxFuture;
2-
use serde::Serialize;
2+
use serde::{Deserialize, Serialize};
33
use serde_json::Value as JsonValue;
44
use tauri_api::{config::Config, private::AsTauriContext};
55

@@ -63,6 +63,15 @@ impl<T: Serialize> From<T> for InvokeResponse {
6363
}
6464
}
6565

66+
#[derive(Deserialize)]
67+
#[allow(missing_docs)]
68+
#[serde(tag = "cmd", rename_all = "camelCase")]
69+
pub struct DispatchInstructions {
70+
pub cmd: String,
71+
#[serde(flatten)]
72+
pub args: JsonValue,
73+
}
74+
6675
/// The application runner.
6776
pub struct App<A: ApplicationExt> {
6877
/// The JS message handler.

tauri/src/error.rs

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ pub enum Error {
4343
/// API not whitelisted on tauri.conf.json
4444
#[error("'{0}' not on the allowlist (https://tauri.studio/docs/api/config#tauri.allowlist)")]
4545
ApiNotAllowlisted(String),
46+
/// Command error (userland).
47+
#[error("{0}")]
48+
Command(serde_json::Value),
49+
/// Invalid args when running a command.
50+
#[error("invalid args for command `{0}`: {1}")]
51+
InvalidArgs(&'static str, serde_json::Error),
4652
}
4753

4854
impl From<serde_json::Error> for Error {

tauri/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub type SyncTask = Box<dyn FnOnce() + Send>;
3131

3232
pub use app::*;
3333
pub use tauri_api as api;
34-
pub use tauri_macros::FromTauriContext;
34+
pub use tauri_macros::{command, generate_handler, FromTauriContext};
3535

3636
/// The Tauri webview implementations.
3737
pub mod flavors {

0 commit comments

Comments
 (0)