Skip to content

Commit

Permalink
Merge pull request #27 from photovoltex/more-doc-adjustments
Browse files Browse the repository at this point in the history
More doc adjustments
  • Loading branch information
photovoltex authored Mar 10, 2024
2 parents 0f9bb14 + 686a933 commit df1e4af
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 152 deletions.
142 changes: 7 additions & 135 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,143 +8,15 @@ This crate tries to provide a general more enjoyable experience for developing t
> tbf it is a saner approach to write the app in a mix of js + rust, because the frameworks are more mature, there are
> way more devs who have experience with js and their respective frameworks etc...
>
> but tbh... just because something is saner, it doesn't stop us from doing things differently ^ヮ^
> but tbh... just because something is saner, doesn't stop us from doing things differently ^ヮ^
Writing an app in a single language gives us the option of building a common crate/module which connects the backend and
frontend. A common model itself can most of the time be easily compiled to both architectures (arch's) when the types
are compatible with both. The commands on the other hand don't have an option to be compiled to wasm. Which means they
need to be handled manually or be called via a wrapper/helper each time.
need to be handled manually or be called via a wrapper/helper each time.

Repeating the implementation and handling for a function that is already defined properly seems to be a waste of time.
For that reason this crate provides the `tauri_interop::command` macro. This macro is explained in detail in the
[command representation](#command-representation-hostwasm) section. This new macro provides the option to invoke the
command in wasm and by therefore call the defined command in tauri. On the other side, when compiling for tauri in addition
to the tauri logic, the macro provides the option to collect all commands in a single file via the invocation of the
`tauri_interop::collect_commands` macro at the end of the file (see [command](#command-frontend--backend-communication)).

In addition, some quality-of-life macros are provided to ease some inconveniences when compiling to multiple arch's. See
the [QOL](#qol-macros) section.

**Feature `event`**:

Tauri has an [event](https://tauri.app/v1/guides/features/events) mechanic which allows the tauri side to communicate with
the frontend. The usage is not as intuitive and has to some inconveniences that make it quite hard to recommend. To
improve the usage, this crate provides the derive-marcos `Event`, `Emit` and `Listen`. The `Event` macro is just a
conditional wrapper that expands to `Emit` for the tauri compilation and `Listen` for the wasm compilation. It is
the intended way to use this feature. The usage is explained in the documentation of the `Event` macro.
section.

## Basic usage:

> **Disclaimer**:
>
> Some examples in this documentation can't be executed with doctests due to
> the required wasm target and tauri modified environment (see [withGlobalTauri](https://tauri.app/v1/api/config/#buildconfig.withglobaltauri))
### Command (Frontend => Backend Communication)
> For more examples see [cmd.rs](./test-project/api/src/cmd.rs) in test-project
The newly provides macro `tauri_interop::command` does two things:
- it provides the function with two macros which are used depending on the targeted architecture
- `tauri_interop::binding` is used when compiling to `wasm`
- `tauri::command` is used otherwise
- additionally it provides the possibility to collect all defined commands via `tauri_interop::collect_commands!()`
- for more info see [collect commands](#collect-commands))
- the function is not generated when targeting `wasm`

The generated command can then be used in `wasm` like the following:
```rust , ignore
#[tauri_interop::command]
fn greet(name: &str, _handle: tauri::AppHandle) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
console_log::init_with_level(log::Level::Info).unwrap();

wasm_bindgen_futures::spawn_local(async move {
let greetings = greet("frontend").await;
log::info!("{greetings}");
});
}
```

**Command representation Host/Wasm (and a bit background knowledge)**

- the returned type of the wasm binding should be 1:1 the same type as send from the "backend"
- technically all commands need to be of type `Result<T, E>` because there is always the possibility of a command
getting called, that isn't registered in the context of tauri
- when using `tauri_interop::collect_commands!()` this possibility is fully™️ removed
- for convenience, we ignore that possibility, and even if the error occurs it will be logged into the console
- all arguments with `tauri` in their name (case-insensitive) are removed as argument in a defined command
- that includes `tauri::*` usages and `Tauri` named types
- the crate itself provides type aliases for tauri types usable in a command (see [type_aliases](./src/command/type_aliases.rs))
- most return types are automatically determined
- when using a return type with `Result` in the name, the function will also return a `Result`
- that also means, if you create a type alias for `Result<T, E>` and don't include `Result` in the name of the alias,
it will not map the `Result` correctly

#### Collect commands

The `tauri_invoke::collect_commands` macro generates a `get_handlers` function in the current mod, which calls the
`tauri::generate_handler` macro with all function which are annotated with the `tauri_interop::command` macro. The
function is only generated for tauri and not for wasm.

Due to technical limitations we sadly can't combine multiple `get_handlers` functions. This limitation comes to the
underlying mechanic. The `tauri::generate_handler` macro generates a function which consumes `tauri::Invoke` as single
parameter. Because it fully consumes the given parameter we can't call multiple handlers with it. In addition, the
`Builder::invoke_handler` function, which usually consumes the generated `tauri::generate_handler` can't be called
twice without losing the previous registered commands.

Because of this limitation for splitting commands into multiple files it is recommended to create a root mod for the
command which includes other command mod's. The functions in the included mods need to be public and re-imported into
the root mod. With these prerequisites the `tauri_invoke::collect_commands` can be called at the end of the file, which
generates the usual `get_handlers` function, but with all "commands" defined inside the others mods.

For an example see the [test-project/api/src/command.rs](test-project/api/src/command.rs).

### QOL macros

This crate also adds some quality-of-life macros. These are intended to ease the drawbacks of compiling to
multiple architectures.

#### Conditional `use`
Because most crates are not intended to be compiled to wasm and most wasm crates are not intended to be compiled to
the host-triplet they have to be excluded in each others compile process. The usual process to exclude uses for a certain
architecture would look something like this:

```rust
#[cfg(not(target_family = "wasm"))]
use tauri::AppHandle;

#[tauri_interop::command]
pub fn empty_invoke(_handle: AppHandle) {}
```

**General usage:**

With the help of `tauri_interop::host_usage!()` and `tauri_interop::wasm_usage!()` we don't need to remember which
attribute we have to add and can just convert the above to the following:

```rust
tauri_interop::host_usage! {
use tauri::AppHandle;
}

#[tauri_interop::command]
pub fn empty_invoke(_handle: AppHandle) {}
```

**Multiple `use` usage:**

When multiple `use` should be excluded, they need to be separated by a single pipe (`|`). For example:

```rust
tauri_interop::host_usage! {
use tauri::State;
| use std::sync::RwLock;
}

#[tauri_interop::command]
pub fn empty_invoke(_state: State<RwLock<String>>) {}
```
The crates therefore provides the following features:
- generate a wasm function out of the defined tauri-command
- collect and register all defined tauri-commands
- QOL-macros to exclude multiple imports in wasm or the host architecture
- easier usage of [tauri's event feature](https://tauri.app/v1/guides/features/events/)
2 changes: 1 addition & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#[cfg(not(target_family = "wasm"))]
pub use type_aliases::*;

/// wasm bindings for tauri's provided js functions (target: `wasm` or feat: `wasm`)
/// wasm bindings for tauri's provided js functions (target: `wasm`)
#[cfg(any(target_family = "wasm", feature = "_wasm"))]
pub mod bindings;

Expand Down
17 changes: 16 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
//! Tauri-Interop is a library that provides macros to improve developing tauri apps with a rust
//! frontend by generating frontend implementation out of the backend definitions.
//!
//! The main macros intended to be used are:
//! - [macro@command], which is intended to be used as replacement to [macro@tauri::command]
//! - [macro@Event], that provides an easier usage of the [Events feature of tauri](https://tauri.app/v1/guides/features/events/)
//! - derives [event::Listen] when compiling to wasm and [event::Emit] otherwise
//!
//! Additionally, some QOL macros ([host_usage] and [wasm_usage]) are provided that
//! reduce some drawbacks when simultaneously compiling to wasm and the host architecture.
//!
//! ### Explanation and Examples
//!
//! Detail explanations and example can be found on the respected traits or macros. Some
//! examples are ignored because they are only valid when compiling to wasm.
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
#![feature(trait_alias)]

pub use tauri_interop_macro::*;
Expand Down
60 changes: 45 additions & 15 deletions tauri-interop-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![feature(iter_intersperse)]
#![warn(missing_docs)]
//! The macros use by `tauri_interop` to generate dynamic code depending on the target
//! The macros use by `tauri-interop` to generate dynamic code depending on the target
//!
//! Without `tauri-interop` the generated code can't compile.
use proc_macro::TokenStream;
use std::collections::HashSet;
Expand Down Expand Up @@ -63,7 +65,7 @@ pub fn derive_event(stream: TokenStream) -> TokenStream {
/// Generates a default `Emit` implementation for the given struct.
///
/// Used for host code generation. It is not intended to be used directly.
/// See [Event]
/// See [Event] for the usage.
#[cfg(feature = "event")]
#[proc_macro_derive(Emit, attributes(auto_naming, mod_name))]
pub fn derive_emit(stream: TokenStream) -> TokenStream {
Expand All @@ -79,9 +81,10 @@ pub fn derive_emit_field(stream: TokenStream) -> TokenStream {
event::emit::derive_field(stream)
}

/// Generates `listen_to_<field>` functions for the given struct.
/// Generates a default `Listen` implementation for the given struct.
///
/// Used for wasm code generation. It is not intended to be used directly.
/// See [Event] for the usage.
#[cfg(feature = "event")]
#[proc_macro_derive(Listen, attributes(auto_naming, mod_name))]
pub fn derive_listen(stream: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -113,27 +116,41 @@ lazy_static::lazy_static! {

static COMMAND_MOD_NAME: Mutex<Option<String>> = Mutex::new(None);

/// Conditionally adds the [binding] or `tauri::command` macro to a struct
/// Conditionally adds the macro [macro@binding] or `tauri::command` to a struct
///
/// ### Example
/// By using this macro, when compiling to wasm, a version that invokes the
/// current function is generated.
///
/// ### Collecting commands
/// When this macro is compiled to the host target, additionally to adding the
/// `tauri::command` macro, the option to auto collect the command via
/// [macro@collect_commands] and [macro@combine_handlers] is provided.
///
/// ### Binding generation
/// All parameter arguments with `tauri` in their name (case-insensitive) are
/// removed as argument in a defined command. That includes `tauri::*` usages
/// and `Tauri` named types.
///
/// The type returned is evaluated automatically and is most of the time 1:1
/// to the defined type. When using a wrapped `Result<T, E>` type, it should
/// include the phrase "Result" in the type name. Otherwise, the returned type
/// can't be successfully interpreted as a result and by that will result in
/// wrong type/error handling/serialization.
///
/// The commands above the commands is the equivalent usage in wasm
/// ### Example - Definition
///
/// ```rust
/// // let _: () = trigger_something();
/// #[tauri_interop::command]
/// #[tauri_interop_macro::command]
/// fn trigger_something(name: &str) {
/// print!("triggers something, but doesn't need to wait for it")
/// }
///
/// // let value: String = wait_for_sync_execution("value").await;
/// #[tauri_interop::command]
/// #[tauri_interop_macro::command]
/// fn wait_for_sync_execution(value: &str) -> String {
/// format!("Has to wait that the backend completes the computation and returns the {value}")
/// }
///
/// // let result: Result<String, String> = asynchronous_execution(true).await;
/// #[tauri_interop::command]
/// #[tauri_interop_macro::command]
/// async fn asynchronous_execution(change: bool) -> Result<String, String> {
/// if change {
/// Ok("asynchronous execution returning result, need Result in their type name".into())
Expand All @@ -142,12 +159,25 @@ static COMMAND_MOD_NAME: Mutex<Option<String>> = Mutex::new(None);
/// }
/// }
///
/// // let _wait_for_completion: () = heavy_computation().await;
/// #[tauri_interop::command]
/// #[tauri_interop_macro::command]
/// async fn heavy_computation() {
/// std::thread::sleep(std::time::Duration::from_millis(5000))
/// }
/// ```
///
/// ### Example - Usage
///
/// ```rust , ignore
/// fn main() {
/// trigger_something();
///
/// wasm_bindgen_futures::spawn_local(async move {
/// wait_for_sync_execution("value").await;
/// asynchronous_execution(true).await.expect("returns ok");
/// heavy_computation().await;
/// });
/// }
/// ```
#[proc_macro_attribute]
pub fn command(_attributes: TokenStream, stream: TokenStream) -> TokenStream {
let fn_item = parse_macro_input!(stream as ItemFn);
Expand All @@ -159,7 +189,7 @@ pub fn command(_attributes: TokenStream, stream: TokenStream) -> TokenStream {

let command_macro = quote! {
#[cfg_attr(target_family = "wasm", ::tauri_interop::binding)]
#[cfg_attr(not(target_family = "wasm"), tauri::command(rename_all = "snake_case"))]
#[cfg_attr(not(target_family = "wasm"), ::tauri::command(rename_all = "snake_case"))]
#fn_item
};

Expand Down

0 comments on commit df1e4af

Please sign in to comment.