Skip to content

Commit b8cfdff

Browse files
authored
Merge pull request #2 from photovoltex/cleanup-macro
Split macros for readability
2 parents 9abdd47 + 1d95553 commit b8cfdff

File tree

11 files changed

+557
-380
lines changed

11 files changed

+557
-380
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ tauri = "1.5.2"
5050
default = [ "event" ]
5151
event = [ "tauri-interop-macro/event" ]
5252
leptos = [ "dep:leptos", "tauri-interop-macro/leptos" ]
53-
# used to generated the missing documentation only generated for "target_family = wasm"
53+
# used to generate the missing documentation, otherwise it's only generated for "target_family = wasm"
5454
wasm = []

README.md

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
![License](https://img.shields.io/crates/l/tauri-interop.svg)
66

77
What this crate tries to achieve:
8-
- generate a equal wasm-function from your defined `tauri::command`
9-
- collect all defined `tauri::command`s without adding them manually
10-
- a simplified way to sending events from tauri and receiving them in the frontend
8+
- generate a equal wasm-function for your defined `tauri::command`
9+
- collecting all defined `tauri::command`s without adding them manually
10+
- a way to sending events from tauri and receiving them in the frontend
1111

1212

1313
## Basic usage:
@@ -41,8 +41,8 @@ Using `tauri_interop::command` does two things:
4141
- it provides the command with two macros which are used depending on the `target_family`
4242
- `tauri_interop::binding` is used when compiling to `wasm`
4343
- `tauri::command` is used otherwise
44-
- it adds an entry to `tauri_interop::collect_commands!()` so that the generated
45-
`get_commands()` function includes/registers the given commands for the tauri context
44+
- it adds an entry to `tauri_interop::collect_commands!()` so that the generated `get_handlers()` function includes the given commands for the tauri context
45+
- the function is not generated when targeting `wasm`
4646

4747
The defined command above can then be used in wasm as below. Due to receiving data from
4848
tauri via a promise, the command response has to be awaited.
@@ -64,13 +64,13 @@ fn main() {
6464

6565
#### Command representation Host/Wasm
6666

67-
- technically all commands need to be of type `Result<T, E>`
68-
- where E is a error that the command isn't defined, if not explicitly redefined via the return type
69-
- this error will show up in the web console, so there shouldn't be a problem ignoring it
67+
- the returned type of the wasm binding should be 1:1 the same type as send from the "backend"
68+
- technically all commands need to be of type `Result<T, E>` because there is always the possibility of a command getting called, that isn't registered in the context of tauri
69+
- when using `tauri_interop::collect_commands!()` this possibility is near fully removed
70+
- for convenience, we ignore that possibility, and even if the error occurs it will be logged into the console
7071
- all arguments with type "State", "AppHandle" and "Window" are removed automatically
7172
> the current implementation relies on the name of the type and can not separate between a
7273
> tauri::State and a self defined "State" struct
73-
- asynchronous commands are values as is seen [async-commands](https://tauri.app/v1/guides/features/command#async-commands) for a detail explanation
7474
7575
```rust , ignore-wasm32-unknown-unknown
7676
// let _: () = trigger_something();
@@ -95,7 +95,7 @@ async fn asynchronous_execution(change: bool) -> Result<String, String> {
9595
}
9696
}
9797

98-
// let _wait_for_completion: () = asynchronous_execution(true).await;
98+
// let _wait_for_completion: () = heavy_computation().await;
9999
#[tauri_interop::command]
100100
async fn heavy_computation() {
101101
std::thread::sleep(std::time::Duration::from_millis(5000))
@@ -117,10 +117,9 @@ pub struct Test {
117117
fn main() {}
118118
```
119119

120-
Using `tauri_interop::emit_or_listen` does provides the command with two macros,
121-
which are used depending on the `target_family`
122-
- `tauri_interop::listen_to` is used when compiling to `wasm`
123-
- derive trait `tauri_interop::Emit` is used otherwise
120+
When using the derive macro `tauri_interop::Event` it expands depending on the `target_family` to
121+
- derive trait `tauri_interop::Listen` (when compiling to `wasm`)
122+
- derive trait `tauri_interop::Emit` (otherwise)
124123

125124
To emit a variable from the above struct (which is mostly intended to be used as state) in the host triplet
126125
```rust , ignore-wasm32-unknown-unknown
@@ -162,7 +161,9 @@ fn main() {
162161

163162
the above emitted value can then be received in wasm as:
164163
```rust , ignore
165-
#[tauri_interop::emit_or_listen]
164+
use tauri_interop::Event;
165+
166+
#[derive(Default, Event)]
166167
pub struct Test {
167168
foo: String,
168169
pub bar: bool,
@@ -175,17 +176,19 @@ async fn main() {
175176
}
176177
```
177178

178-
The `liste_handle` contains the provided closure and the "unlisten" method. It has to be hold in scope as long
179+
The `ListenHandle` contains the provided closure and the "unlisten" method. It has to be hold in scope as long
179180
as the event should be received. Dropping it will automatically detach the closure from the event. See
180181
[cmd.rs](./test-project/api/src/cmd.rs) for other example how it could be used.
181182

182183
#### Feature: leptos
183184
When the `leptos` feature is enabled it will add additional `use_<field>` methods on the provided struct.
184-
These methods take care of the required initial asynchron call to register the listener and will hold the
185+
These methods take care of the required initial asynchronous call to register the listener and will hold the
185186
handle in scope as long as the component is rendered.
186187

187188
```rust , ignore
188-
#[tauri_interop::emit_or_listen]
189+
use tauri_interop::Event;
190+
191+
#[derive(Default, Event)]
189192
pub struct Test {
190193
foo: String,
191194
pub bar: bool,

src/event.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use serde::{Serialize, Deserialize};
1+
use serde::{Deserialize, Serialize};
22
#[cfg(not(target_family = "wasm"))]
3-
use tauri::{AppHandle, Wry, Error};
3+
use tauri::{AppHandle, Error, Wry};
44

55
/// traits for event emitting in the host code (feat: `tauri`)
66
#[cfg(not(target_family = "wasm"))]
@@ -44,7 +44,7 @@ where
4444

4545
#[cfg(not(target_family = "wasm"))]
4646
/// Updates the related field and emit its event
47-
///
47+
///
4848
/// Only required for "target_family = wasm"
4949
fn update(s: &mut P, handle: &AppHandle<Wry>, v: Self::Type) -> Result<(), Error>;
5050
}

src/event/listen.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ impl<'s> ListenHandle<'s> {
115115
}
116116
}
117117

118-
119118
/// Trait that defines the available listen methods
120119
pub trait Listen {
121120
/// Registers an callback to a [Field]
@@ -134,9 +133,7 @@ pub trait Listen {
134133
///
135134
/// Default Implementation: see [ListenHandle::use_register]
136135
#[cfg(feature = "leptos")]
137-
fn use_field<F: Field<Self>>(
138-
initial: F::Type,
139-
) -> (ReadSignal<F::Type>, WriteSignal<F::Type>)
136+
fn use_field<F: Field<Self>>(initial: F::Type) -> (ReadSignal<F::Type>, WriteSignal<F::Type>)
140137
where
141138
Self: Sized + super::Parent,
142139
{

src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#![warn(missing_docs)]
22
#![doc = include_str!("../README.md")]
3-
43
#![feature(trait_alias)]
54

65
/// wasm bindings for tauri's provided js functions (target: `wasm` or feat: `wasm`)

tauri-interop-macro/src/command.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::command::wrapper::{InvokeArgument, InvokeCommand};
2+
use proc_macro::TokenStream;
3+
use proc_macro2::Ident;
4+
use quote::{format_ident, quote, ToTokens};
5+
use syn::punctuated::Punctuated;
6+
use syn::token::Comma;
7+
use syn::{parse_macro_input, FnArg, ItemFn};
8+
9+
mod wrapper;
10+
11+
pub fn convert_to_binding(stream: TokenStream) -> TokenStream {
12+
let item_fn = parse_macro_input!(stream as ItemFn);
13+
let InvokeCommand {
14+
attributes,
15+
name,
16+
generics,
17+
return_type,
18+
invoke,
19+
invoke_argument,
20+
} = wrapper::prepare(item_fn);
21+
22+
let InvokeArgument {
23+
argument_name,
24+
fields,
25+
} = invoke_argument;
26+
27+
let async_ident = invoke.as_async();
28+
let field_usage = fields
29+
.iter()
30+
.map(|field| field.ident.clone())
31+
.collect::<Punctuated<Ident, Comma>>();
32+
let field_definitions = fields
33+
.into_iter()
34+
.map(|field| field.argument)
35+
.collect::<Punctuated<FnArg, Comma>>();
36+
37+
let command_name = name.to_string();
38+
let args_ident = format_ident!("args");
39+
let invoke_binding = invoke.as_expr(command_name, &args_ident);
40+
41+
let stream = quote! {
42+
#[derive(::serde::Serialize, ::serde::Deserialize)]
43+
struct #argument_name #generics {
44+
#field_definitions
45+
}
46+
47+
#( #attributes )*
48+
pub #async_ident fn #name #generics (#field_definitions) #return_type
49+
{
50+
let #args_ident = #argument_name { #field_usage };
51+
let #args_ident = ::serde_wasm_bindgen::to_value(&#args_ident)
52+
.expect("serialized arguments");
53+
54+
#invoke_binding
55+
}
56+
};
57+
58+
TokenStream::from(stream.to_token_stream())
59+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use convert_case::{Case, Casing};
2+
use proc_macro::Span;
3+
use proc_macro2::Ident;
4+
use quote::{format_ident, ToTokens};
5+
use syn::{
6+
parse_quote, Attribute, Expr, ExprPath, FnArg, GenericParam, Generics, ItemFn, Lifetime,
7+
LifetimeParam, Pat, PathSegment, ReturnType, Signature, Type, TypePath,
8+
};
9+
10+
#[derive(PartialEq)]
11+
pub enum Invoke {
12+
Empty,
13+
AsyncEmpty,
14+
Async,
15+
AsyncResult,
16+
}
17+
18+
impl Invoke {
19+
pub fn as_async(&self) -> Option<Ident> {
20+
self.ne(&Invoke::Empty).then_some(format_ident!("async"))
21+
}
22+
23+
pub fn as_expr(&self, cmd_name: String, arg_name: &Ident) -> Expr {
24+
let expr: ExprPath = match self {
25+
Invoke::Empty => parse_quote!(bindings::invoke),
26+
Invoke::Async | Invoke::AsyncEmpty => parse_quote!(command::async_invoke),
27+
Invoke::AsyncResult => parse_quote!(command::invoke_catch),
28+
};
29+
30+
let call = parse_quote!( ::tauri_interop::#expr(#cmd_name, #arg_name) );
31+
32+
if self.as_async().is_some() {
33+
Expr::Await(parse_quote!(#call.await))
34+
} else {
35+
Expr::Call(call)
36+
}
37+
}
38+
}
39+
40+
fn is_result(segment: &PathSegment) -> bool {
41+
segment.ident.to_string().as_str() == "Result"
42+
}
43+
44+
fn determine_invoke(return_type: &ReturnType, is_async: bool) -> Invoke {
45+
match return_type {
46+
ReturnType::Default => {
47+
if is_async {
48+
Invoke::AsyncEmpty
49+
} else {
50+
Invoke::Empty
51+
}
52+
}
53+
ReturnType::Type(_, ty) => match ty.as_ref() {
54+
// fixme: if it's an single ident, catch isn't needed this could probably be a problem later
55+
Type::Path(path) if path.path.segments.iter().any(is_result) => Invoke::AsyncResult,
56+
Type::Path(_) => Invoke::Async,
57+
others => panic!("no support for '{}'", others.to_token_stream()),
58+
},
59+
}
60+
}
61+
62+
const ARGUMENT_LIFETIME: &str = "'arg_lifetime";
63+
64+
fn new_arg_lt() -> Lifetime {
65+
Lifetime::new(ARGUMENT_LIFETIME, Span::call_site().into())
66+
}
67+
68+
const TAURI_TYPES: [&str; 3] = ["State", "AppHandle", "Window"];
69+
70+
fn any_tauri(ty_path: &TypePath) -> bool {
71+
ty_path
72+
.path
73+
.segments
74+
.iter()
75+
.any(|segment| TAURI_TYPES.contains(&segment.ident.to_string().as_str()))
76+
}
77+
78+
pub struct InvokeCommand {
79+
pub attributes: Vec<Attribute>,
80+
pub name: Ident,
81+
pub generics: Generics,
82+
pub return_type: ReturnType,
83+
pub invoke: Invoke,
84+
pub invoke_argument: InvokeArgument,
85+
}
86+
87+
pub struct InvokeArgument {
88+
pub argument_name: Ident,
89+
pub fields: Vec<FieldArg>,
90+
}
91+
92+
pub struct FieldArg {
93+
pub ident: Ident,
94+
pub argument: FnArg,
95+
requires_lifetime: bool,
96+
}
97+
98+
pub fn prepare(function: ItemFn) -> InvokeCommand {
99+
let ItemFn {
100+
attrs: attributes,
101+
sig,
102+
..
103+
} = function;
104+
105+
let Signature {
106+
ident: name,
107+
mut generics,
108+
inputs,
109+
output: return_type,
110+
asyncness,
111+
..
112+
} = sig;
113+
114+
let filtered_fields = inputs
115+
.into_iter()
116+
.filter_map(|mut fn_arg| {
117+
let typed = match fn_arg {
118+
FnArg::Typed(ref mut typed) => typed,
119+
_ => return None,
120+
};
121+
122+
if matches!(typed.ty.as_ref(), Type::Path(ty_path) if any_tauri(ty_path)) {
123+
return None;
124+
}
125+
126+
let req_lf = if let Type::Reference(ty_ref) = typed.ty.as_mut() {
127+
ty_ref.lifetime = Some(new_arg_lt());
128+
true
129+
} else {
130+
false
131+
};
132+
133+
match typed.pat.as_ref() {
134+
Pat::Ident(ident) => Some(FieldArg {
135+
ident: ident.ident.clone(),
136+
argument: fn_arg,
137+
requires_lifetime: req_lf,
138+
}),
139+
_ => None,
140+
}
141+
})
142+
.collect::<Vec<_>>();
143+
144+
if filtered_fields.iter().any(|field| field.requires_lifetime) {
145+
generics
146+
.params
147+
.push(GenericParam::Lifetime(LifetimeParam::new(new_arg_lt())))
148+
}
149+
150+
let invoke = determine_invoke(&return_type, asyncness.is_some());
151+
let argument_name = format_ident!("{}Args", name.to_string().to_case(Case::Pascal));
152+
153+
InvokeCommand {
154+
attributes,
155+
name,
156+
generics,
157+
return_type,
158+
invoke,
159+
invoke_argument: InvokeArgument {
160+
argument_name,
161+
fields: filtered_fields,
162+
},
163+
}
164+
}

0 commit comments

Comments
 (0)