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}