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}