Skip to content

Commit

Permalink
add combine handler functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
photovoltex committed Mar 6, 2024
1 parent d77382c commit 95bbf5a
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 43 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion tauri-interop-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ description = "Macros for the tauri-interop crate."
proc-macro = true

[dependencies]
syn = { version = "^2.0", features = [ "full" ]}
syn = { version = "^2.0", features = [ "full", "extra-traits" ]}
quote = "^1.0"
convert_case = "^0.6"
lazy_static = "^1.4"
serde = "^1.0"
proc-macro2 = "^1.0"
proc-macro-error = "1.0.4"

[dev-dependencies]
tauri = { version = "^1.5", default-features = false, features = ["wry"] }
log = "^0.4"

[features]
# todo: remove default feature before publish
Expand Down
1 change: 1 addition & 0 deletions tauri-interop-macro/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use syn::{parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, ItemFn
use crate::command::wrapper::{InvokeArgument, InvokeCommand};

mod wrapper;
pub mod collect;

pub fn convert_to_binding(stream: TokenStream) -> TokenStream {
let item_fn = parse_macro_input!(stream as ItemFn);
Expand Down
58 changes: 58 additions & 0 deletions tauri-interop-macro/src/command/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::collections::HashSet;

use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_quote, ExprPath};

pub fn commands_with_mod_name(mod_name: &str, commands: &HashSet<String>) -> HashSet<String> {
commands
.iter()
.map(|cmd| format!("{mod_name}::{cmd}"))
.collect()
}

pub fn commands_to_punctuated(commands: &HashSet<String>) -> Punctuated<ExprPath, Comma> {
commands.iter().map(command_to_expr_path).collect()
}

pub fn command_to_expr_path(command: &String) -> ExprPath {
match get_separated_command(command) {
None => {
let ident = format_ident!("{command}");
parse_quote!(#ident)
}
Some((mod_name, cmd_name)) => parse_quote!(#mod_name::#cmd_name),
}
}

pub fn get_separated_command(input: &str) -> Option<(Ident, Ident)> {
let mut split_cmd = input.split("::");
let mod_name = format_ident!("{}", split_cmd.next()?);
// order matters
let cmd_name = format_ident!("{}", split_cmd.next()?);

Some((mod_name, cmd_name))
}

pub fn get_handler_function(
fn_name: Ident,
commands: &HashSet<String>,
handlers: Punctuated<ExprPath, Comma>,
include_mods: Vec<ExprPath>,
) -> TokenStream {
let commands = commands.iter().collect::<Vec<_>>();
quote! {
#[cfg(not(target_family = "wasm"))]
#[doc = "auto generated function to register all configured commands"]
pub fn #fn_name() -> impl Fn(tauri::Invoke) {
#( use #include_mods; )*

let handlers = vec! [ #( #commands ),* ];
log::debug!("Registering following commands to tauri: {handlers:#?}");

::tauri::generate_handler![ #handlers ]
}
}
}
175 changes: 151 additions & 24 deletions tauri-interop-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
#![feature(iter_intersperse)]
#![warn(missing_docs)]
//! The macros use by `tauri_interop` to generate dynamic code depending on the target
use proc_macro::TokenStream;
use std::{collections::BTreeSet, sync::Mutex};
use std::collections::HashSet;
use std::sync::Mutex;

use proc_macro_error::{emit_call_site_error, emit_call_site_warning, proc_macro_error};
use quote::{format_ident, quote, ToTokens};
use syn::{parse::Parser, parse_macro_input, punctuated::Punctuated, token::Comma, Ident, ItemFn, ItemUse, Token};
use syn::{
parse::Parser, parse_macro_input, punctuated::Punctuated, ExprPath, ItemFn, ItemMod, ItemUse,
Token,
};

use crate::command::collect::commands_to_punctuated;

mod command;
mod event;
Expand Down Expand Up @@ -67,15 +75,21 @@ pub fn binding(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
}

lazy_static::lazy_static! {
static ref HANDLER_LIST: Mutex<BTreeSet<String>> = Mutex::new(Default::default());
static ref COMMAND_LIST_ALL: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
}

lazy_static::lazy_static! {
static ref COMMAND_LIST: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
}

/// Conditionally adds `tauri_interop::binding` or `tauri::command` to a struct
static COMMAND_MOD_NAME: Mutex<Option<String>> = Mutex::new(None);

/// Conditionally adds the `tauri_interop::binding` or `tauri::command` macro to a struct
#[proc_macro_attribute]
pub fn command(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
let fn_item = parse_macro_input!(stream as ItemFn);

HANDLER_LIST
COMMAND_LIST
.lock()
.unwrap()
.insert(fn_item.sig.ident.to_string());
Expand All @@ -89,35 +103,148 @@ pub fn command(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
TokenStream::from(command_macro.to_token_stream())
}

/// Marks a mod that contains commands
///
/// A mod needs to be marked when multiple command mods should be combined.
/// See [combine_handlers] for a detailed explanation/example.
///
/// Requires usage of unstable feature: `#![feature(proc_macro_hygiene)]`
#[proc_macro_attribute]
pub fn commands(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
let item_mod = parse_macro_input!(stream as ItemMod);
let _ = COMMAND_MOD_NAME
.lock()
.unwrap()
.insert(item_mod.ident.to_string());

TokenStream::from(item_mod.to_token_stream())
}

/// Collects all commands annotated with `tauri_interop::command` and
/// provides these with a `get_handlers()` in the current namespace
/// provides these with a `get_handlers()` in the current mod
///
/// The provided function isn't available for wasm
/// The provided function isn't available in wasm
#[proc_macro]
pub fn collect_commands(_: TokenStream) -> TokenStream {
let mut handler = HANDLER_LIST.lock().unwrap();
let to_generated_handler = handler
.iter()
.map(|s| format_ident!("{s}"))
.collect::<Punctuated<Ident, Comma>>();

let stream = quote! {
#[cfg(not(target_family = "wasm"))]
/// the all mighty handler collector
pub fn get_handlers() -> impl Fn(tauri::Invoke) {
let handlers = vec! [ #( #handler ),* ];
log::debug!("Registering following commands to tauri: {handlers:#?}");

::tauri::generate_handler![ #to_generated_handler ]
}
};
let mut commands = COMMAND_LIST.lock().unwrap();
let stream = command::collect::get_handler_function(
format_ident!("get_handlers"),
&commands,
commands_to_punctuated(&commands),
Vec::new(),
);

// logic for renaming the commands, so that combine methode can just use the provided commands
if let Some(mod_name) = COMMAND_MOD_NAME.lock().unwrap().as_ref() {
COMMAND_LIST_ALL
.lock()
.unwrap()
.extend(command::collect::commands_with_mod_name(
mod_name, &commands,
));
} else {
// if there is no mod provided we can just move/clear the commands
COMMAND_LIST_ALL.lock().unwrap().extend(commands.iter().cloned());
}

// clearing the already used handlers
handler.clear();
commands.clear();
// set mod name to none
let _ = COMMAND_MOD_NAME.lock().unwrap().take();

TokenStream::from(stream.to_token_stream())
}

/// Combines multiple modules containing commands
///
/// Takes multiple module paths as input and provides a `get_all_handlers()` function in
/// the current mod that registers all commands from the provided mods. This macro does
/// still require the invocation of [collect_commands] at the end of a command mod. In
/// addition, a mod has to be marked with [commands].
///
/// ### Example
///
/// ```
/// #[tauri_interop_macro::commands]
/// mod cmd1 {
/// #[tauri_interop_macro::command]
/// pub fn cmd1() {}
///
/// tauri_interop_macro::collect_commands!();
/// }
///
/// mod whatever {
/// #[tauri_interop_macro::commands]
/// pub mod cmd2 {
/// #[tauri_interop_macro::command]
/// pub fn cmd2() {}
///
/// tauri_interop_macro::collect_commands!();
/// }
/// }
///
/// tauri_interop_macro::combine_handlers!( cmd1, whatever::cmd2 );
///
/// ```
#[proc_macro_error]
#[proc_macro]
pub fn combine_handlers(stream: TokenStream) -> TokenStream {
let command_mods = Punctuated::<ExprPath, Token![,]>::parse_terminated
.parse2(stream.into())
.unwrap()
.into_iter()
.collect::<Vec<_>>();

let org_commands = COMMAND_LIST_ALL.lock().unwrap();
let commands = org_commands
.iter()
.flat_map(|command| {
let (mod_name, _) = command::collect::get_separated_command(command)?;
command_mods
.iter()
.any(|r#mod| {
r#mod
.path
.segments
.iter()
.any(|seg| mod_name.eq(&seg.ident))
})
.then_some(command.clone())
})
.collect::<HashSet<_>>();

if commands.is_empty() {
emit_call_site_error!("No commands will be registered")
}

let remaining_commands = COMMAND_LIST.lock().unwrap();
if !remaining_commands.is_empty() {
emit_call_site_error!(
"Their are dangling commands that won't be registered. See {:?}",
remaining_commands
)
}

if org_commands.len() > commands.len() {
let diff = org_commands
.difference(&commands)
.cloned()
.intersperse(String::from(","))
.collect::<String>();
emit_call_site_warning!(
"Not all commands will be registered. Missing commands: {:?}",
diff
);
}

TokenStream::from(command::collect::get_handler_function(
format_ident!("get_all_handlers"),
&commands,
commands_to_punctuated(&commands),
command_mods,
))
}

fn collect_uses(stream: TokenStream) -> Vec<ItemUse> {
Punctuated::<ItemUse, Token![|]>::parse_terminated
.parse2(stream.into())
Expand Down
1 change: 1 addition & 0 deletions test-project/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,5 @@ pub mod broken {
Ok(())
}
}

tauri_interop::collect_commands!();
7 changes: 0 additions & 7 deletions test-project/api/src/command.rs

This file was deleted.

2 changes: 0 additions & 2 deletions test-project/api/src/command/other_cmd.rs

This file was deleted.

7 changes: 6 additions & 1 deletion test-project/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
#![allow(clippy::disallowed_names)]
#![feature(iter_intersperse)]
#![feature(proc_macro_hygiene)]

#[tauri_interop::commands]
pub mod cmd;

pub mod command;
pub mod model;

#[cfg(target_family = "wasm")]
pub use tauri_interop::*;

tauri_interop::combine_handlers!( cmd, model::other_cmd );
5 changes: 5 additions & 0 deletions test-project/api/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#![allow(dead_code)]
#![allow(path_statements)]

// this mod at this position doesn't make much sense logic vise
// for testing the combine feature tho its a quite convienend spot :D
#[tauri_interop::commands]
pub mod other_cmd;

use tauri_interop::Event;

#[derive(Default, Event)]
Expand Down
10 changes: 10 additions & 0 deletions test-project/api/src/model/other_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tauri_interop::host_usage! {
use tauri_interop::command::TauriAppHandle;
}

#[tauri_interop::command]
pub fn stop_application(handle: TauriAppHandle) {
handle.exit(0)
}

tauri_interop::collect_commands!();
2 changes: 1 addition & 1 deletion test-project/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fn main() {
.unwrap();

tauri::Builder::default()
.invoke_handler(api::command::get_handlers())
.invoke_handler(api::get_all_handlers())
.setup(move |app| {
let main_window = app.handle().get_window("main").unwrap();

Expand Down
Loading

0 comments on commit 95bbf5a

Please sign in to comment.