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