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