tauri_interop_macro/lib.rs
1#![feature(iter_intersperse)]
2#![feature(doc_cfg)]
3#![warn(missing_docs)]
4//! The macros use by `tauri-interop` to generate dynamic code depending on the target
5//!
6//! Without `tauri-interop` the generated code can't compile.
7
8use proc_macro::TokenStream;
9use std::collections::HashSet;
10use std::sync::Mutex;
11
12use proc_macro_error::{emit_call_site_error, emit_call_site_warning, proc_macro_error};
13use quote::{format_ident, quote, ToTokens};
14use syn::{
15 parse::Parser, parse_macro_input, punctuated::Punctuated, ExprPath, ItemFn, ItemMod, Token,
16};
17
18use crate::command::collect::commands_to_punctuated;
19
20mod command;
21#[cfg(feature = "event")]
22mod event;
23
24/// Conditionally adds [Listen] or [Emit] to a struct.
25///
26/// The field values inside the struct require to be self owned.
27/// That means references aren't allowed inside the event struct.
28///
29/// Depending on the targeted architecture the macro generates different results.
30/// When compiling to `wasm` the [Listen] trait is derived. Otherwise, [Emit] is derived.
31///
32/// Both traits generate a new mod in which the related field-structs are generated in.
33/// The mod can be automatically renamed with `#[auto_naming(EnumLike)]` to behave
34/// enum-like (for example a struct `Test`s mod would usually be named `test`, 'EnumLike'
35/// names it `TestField` instead) and `#[mod_name(...)]` is a direct possibility to rename
36/// the mod to any given name.
37///
38/// The generated field-structs represent a field of the struct and are used for the
39/// derived trait functions. The fields are used to `emit`, `update` or `listen_to` a
40/// given field. For detail usages see the individual traits defined in `tauri-interop`.
41///
42/// ### Example
43///
44/// ```
45/// use tauri_interop_macro::Event;
46/// use serde::{Serialize, Deserialize};
47///
48/// #[derive(Default, Clone, Serialize, Deserialize)]
49/// pub struct Bar {
50/// value: bool
51/// }
52///
53/// #[derive(Event)]
54/// struct EventModel {
55/// foo: String,
56/// pub bar: Bar
57/// }
58///
59/// #[cfg(feature = "initial_value")]
60/// impl tauri_interop::event::ManagedEmit for EventModel {}
61///
62/// // has to be defined in this example, otherwise the
63/// // macro expansion panics because of missing super
64/// fn main() {}
65/// ```
66#[cfg(feature = "event")]
67#[doc(cfg(feature = "event"))]
68#[proc_macro_derive(Event, attributes(auto_naming, mod_name))]
69pub fn derive_event(stream: TokenStream) -> TokenStream {
70 if cfg!(feature = "_wasm") {
71 event::listen::derive(stream)
72 } else {
73 event::emit::derive(stream)
74 }
75}
76
77/// Auto implements the `ManagedEmit` trait from `tauri_interop` when compiling to the host
78///
79/// ### Example
80///
81/// ```
82/// use tauri_interop_macro::{Event, ManagedEmit};
83///
84/// #[derive(Event, ManagedEmit)]
85/// struct EventModel {
86/// foo: String,
87/// pub bar: bool
88/// }
89///
90/// // has to be defined in this example, otherwise the
91/// // macro expansion panics because of missing super
92/// fn main() {}
93/// ```
94#[cfg(all(feature = "event", feature = "initial_value"))]
95#[doc(cfg(all(feature = "event", feature = "initial_value")))]
96#[proc_macro_derive(ManagedEmit)]
97pub fn derive_managed_emit(stream: TokenStream) -> TokenStream {
98 (!cfg!(feature = "_wasm"))
99 .then(|| event::emit::derive_managed_emit(stream))
100 .unwrap_or_default()
101}
102
103/// Generates a default `Emit` implementation for the given struct.
104///
105/// Used for host code generation. It is not intended to be used directly.
106/// See [Event] for the usage.
107#[cfg(feature = "event")]
108#[doc(cfg(feature = "event"))]
109#[proc_macro_derive(Emit, attributes(auto_naming, mod_name))]
110pub fn derive_emit(stream: TokenStream) -> TokenStream {
111 event::emit::derive(stream)
112}
113
114/// Generates a default `EmitField` implementation for the given struct.
115///
116/// Used for host code generation. It is not intended to be used directly.
117#[cfg(feature = "event")]
118#[doc(cfg(feature = "event"))]
119#[proc_macro_derive(EmitField, attributes(parent, parent_field_name, parent_field_ty))]
120pub fn derive_emit_field(stream: TokenStream) -> TokenStream {
121 event::emit::derive_field(stream)
122}
123
124/// Generates a default `Listen` implementation for the given struct.
125///
126/// Used for wasm code generation. It is not intended to be used directly.
127/// See [Event] for the usage.
128#[cfg(feature = "event")]
129#[doc(cfg(feature = "event"))]
130#[proc_macro_derive(Listen, attributes(auto_naming, mod_name))]
131pub fn derive_listen(stream: TokenStream) -> TokenStream {
132 event::listen::derive(stream)
133}
134
135/// Generates a default `ListenField` implementation for the given struct.
136///
137/// Used for wasm code generation. It is not intended to be used directly.
138#[cfg(feature = "event")]
139#[doc(cfg(feature = "event"))]
140#[proc_macro_derive(ListenField, attributes(parent, parent_field_ty))]
141pub fn derive_listen_field(stream: TokenStream) -> TokenStream {
142 event::listen::derive_field(stream)
143}
144
145/// Generates the wasm counterpart to a defined `tauri::command`
146#[proc_macro_attribute]
147pub fn binding(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
148 command::convert_to_binding(stream)
149}
150
151lazy_static::lazy_static! {
152 static ref COMMAND_LIST_ALL: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
153}
154
155lazy_static::lazy_static! {
156 static ref COMMAND_LIST: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
157}
158
159static COMMAND_MOD_NAME: Mutex<Option<String>> = Mutex::new(None);
160
161/// Conditionally adds the macro [macro@binding] or `tauri::command` to a struct
162///
163/// By using this macro, when compiling to wasm, a version that invokes the
164/// current function is generated.
165///
166/// ### Collecting commands
167/// When this macro is compiled to the host target, additionally to adding the
168/// `tauri::command` macro, the option to auto collect the command via
169/// [macro@collect_commands] and [macro@combine_handlers] is provided.
170///
171/// ### Binding generation
172/// All parameter arguments with `tauri` in their name (case-insensitive) are
173/// removed as argument in a defined command. That includes `tauri::*` usages
174/// and `Tauri` named types.
175///
176/// The type returned is evaluated automatically and is most of the time 1:1
177/// to the defined type. When using a wrapped `Result<T, E>` type, it should
178/// include the phrase "Result" in the type name. Otherwise, the returned type
179/// can't be successfully interpreted as a result and by that will result in
180/// wrong type/error handling/serialization.
181///
182/// ### Example - Definition
183///
184/// ```rust
185/// #[tauri_interop_macro::command]
186/// fn trigger_something(name: &str) {
187/// print!("triggers something, but doesn't need to wait for it")
188/// }
189///
190/// #[tauri_interop_macro::command]
191/// fn wait_for_sync_execution(value: &str) -> String {
192/// format!("Has to wait that the backend completes the computation and returns the {value}")
193/// }
194///
195/// #[tauri_interop_macro::command]
196/// async fn asynchronous_execution(change: bool) -> Result<String, String> {
197/// if change {
198/// Ok("asynchronous execution returning result, need Result in their type name".into())
199/// } else {
200/// Err("if they don't it, the error will be not be parsed/handled".into())
201/// }
202/// }
203///
204/// #[tauri_interop_macro::command]
205/// async fn heavy_computation() {
206/// std::thread::sleep(std::time::Duration::from_millis(5000))
207/// }
208/// ```
209///
210/// ### Example - Usage
211///
212/// ```rust , ignore
213/// fn main() {
214/// trigger_something();
215///
216/// wasm_bindgen_futures::spawn_local(async move {
217/// wait_for_sync_execution("value").await;
218/// asynchronous_execution(true).await.expect("returns ok");
219/// heavy_computation().await;
220/// });
221/// }
222/// ```
223#[proc_macro_attribute]
224pub fn command(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
225 let fn_item = parse_macro_input!(stream as ItemFn);
226
227 COMMAND_LIST
228 .lock()
229 .unwrap()
230 .insert(fn_item.sig.ident.to_string());
231
232 // root = "tauri_interop", we can only provide an Ident and no path to the re-exported crate
233 // see https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-macros/src/command/wrapper.rs#L76
234 let command_macro = quote! {
235 #[cfg_attr(target_family = "wasm", ::tauri_interop::binding)]
236 #[cfg_attr(not(target_family = "wasm"), ::tauri_interop::export::tauri::command(root = "tauri_interop", rename_all = "snake_case"))]
237 #fn_item
238 };
239
240 TokenStream::from(command_macro.to_token_stream())
241}
242
243/// Marks a mod that contains commands
244///
245/// A mod needs to be marked when multiple command mods should be combined.
246/// See [combine_handlers!] for a detailed explanation/example.
247///
248/// Requires usage of unstable feature: `#![feature(proc_macro_hygiene)]`
249#[proc_macro_attribute]
250pub fn commands(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
251 let item_mod = parse_macro_input!(stream as ItemMod);
252 let _ = COMMAND_MOD_NAME
253 .lock()
254 .unwrap()
255 .insert(item_mod.ident.to_string());
256
257 TokenStream::from(item_mod.to_token_stream())
258}
259
260/// Collects all commands annotated with `tauri_interop::command` and
261/// provides these with a `get_handlers()` in the current mod
262///
263/// ### Example
264///
265/// ```
266/// #[tauri_interop_macro::command]
267/// fn greet(name: &str) -> String {
268/// format!("Hello, {}! You've been greeted from Rust!", name)
269/// }
270///
271/// tauri_interop_macro::collect_commands!();
272///
273/// fn main() {
274/// let _ = tauri::Builder::default()
275/// // This is where you pass in the generated handler collector
276/// // in this example this would only register cmd1
277/// .invoke_handler(get_handlers());
278/// }
279/// ```
280#[proc_macro]
281pub fn collect_commands(_: TokenStream) -> TokenStream {
282 let mut commands = COMMAND_LIST.lock().unwrap();
283 let stream = command::collect::get_handler_function(
284 format_ident!("get_handlers"),
285 &commands,
286 commands_to_punctuated(&commands),
287 Vec::new(),
288 );
289
290 // logic for renaming the commands, so that combine methode can just use the provided commands
291 if let Some(mod_name) = COMMAND_MOD_NAME.lock().unwrap().as_ref() {
292 COMMAND_LIST_ALL
293 .lock()
294 .unwrap()
295 .extend(command::collect::commands_with_mod_name(
296 mod_name, &commands,
297 ));
298 } else {
299 // if there is no mod provided we can just move/clear the commands
300 COMMAND_LIST_ALL
301 .lock()
302 .unwrap()
303 .extend(commands.iter().cloned());
304 }
305
306 // clearing the already used handlers
307 commands.clear();
308 // set mod name to none
309 let _ = COMMAND_MOD_NAME.lock().unwrap().take();
310
311 TokenStream::from(stream.to_token_stream())
312}
313
314/// Combines multiple modules containing commands
315///
316/// Takes multiple module paths as input and provides a `get_all_handlers()` function in
317/// the current mod that registers all commands from the provided mods. This macro does
318/// still require the invocation of [collect_commands!] at the end of a command mod. In
319/// addition, a mod has to be marked with [macro@commands].
320///
321/// ### Example
322///
323/// ```
324/// #[tauri_interop_macro::commands]
325/// mod cmd1 {
326/// #[tauri_interop_macro::command]
327/// pub fn cmd1() {}
328///
329/// tauri_interop_macro::collect_commands!();
330/// }
331///
332/// mod whatever {
333/// #[tauri_interop_macro::commands]
334/// pub mod cmd2 {
335/// #[tauri_interop_macro::command]
336/// pub fn cmd2() {}
337///
338/// tauri_interop_macro::collect_commands!();
339/// }
340/// }
341///
342/// tauri_interop_macro::combine_handlers!( cmd1, whatever::cmd2 );
343///
344/// fn main() {
345/// let _ = tauri::Builder::default()
346/// // This is where you pass in the combined handler collector
347/// // in this example it will register cmd1::cmd1 and whatever::cmd2::cmd2
348/// .invoke_handler(get_all_handlers());
349/// }
350/// ```
351#[proc_macro_error]
352#[proc_macro]
353pub fn combine_handlers(stream: TokenStream) -> TokenStream {
354 if cfg!(feature = "_wasm") {
355 return Default::default();
356 }
357
358 let command_mods = Punctuated::<ExprPath, Token![,]>::parse_terminated
359 .parse2(stream.into())
360 .unwrap()
361 .into_iter()
362 .collect::<Vec<_>>();
363
364 let org_commands = COMMAND_LIST_ALL.lock().unwrap();
365 let commands = command::collect::get_filtered_commands(&org_commands, &command_mods);
366
367 if commands.is_empty() {
368 emit_call_site_error!("No commands will be registered")
369 }
370
371 let remaining_commands = COMMAND_LIST.lock().unwrap();
372 if !remaining_commands.is_empty() {
373 emit_call_site_error!(
374 "Their are dangling commands that won't be registered. See {:?}",
375 remaining_commands
376 )
377 }
378
379 if org_commands.len() > commands.len() {
380 let diff = org_commands
381 .difference(&commands)
382 .cloned()
383 .intersperse(String::from(","))
384 .collect::<String>();
385 emit_call_site_warning!(
386 "Not all commands will be registered. Missing commands: {:?}",
387 diff
388 );
389 }
390
391 TokenStream::from(command::collect::get_handler_function(
392 format_ident!("get_all_handlers"),
393 &commands,
394 commands_to_punctuated(&commands),
395 command_mods,
396 ))
397}
398
399/// Simple macro to include multiple imports (seperated by `|`) not in wasm
400///
401/// ### Example
402///
403/// ```rust
404/// tauri_interop_macro::host_usage! {
405/// use tauri::State;
406/// | use std::sync::RwLock;
407/// }
408///
409/// #[tauri_interop_macro::command]
410/// pub fn empty_invoke(_state: State<RwLock<String>>) {}
411/// ```
412#[proc_macro]
413pub fn host_usage(stream: TokenStream) -> TokenStream {
414 let uses = command::collect::uses(stream);
415 TokenStream::from(quote! {
416 #(
417 #[cfg(not(target_family = "wasm"))]
418 #uses
419 )*
420 })
421}
422
423/// Simple macro to include multiple imports (seperated by `|`) only in wasm
424///
425/// Equivalent to [host_usage!] for wasm imports only required in wasm.
426/// For an example see [host_usage!].
427#[proc_macro]
428pub fn wasm_usage(stream: TokenStream) -> TokenStream {
429 let uses = command::collect::uses(stream);
430 TokenStream::from(quote! {
431 #(
432 #[cfg(target_family = "wasm")]
433 #uses
434 )*
435 })
436}