Skip to content

Commit

Permalink
Merge pull request #32 from photovoltex/command-overhaul
Browse files Browse the repository at this point in the history
Handle unregistered commands
  • Loading branch information
photovoltex authored Mar 20, 2024
2 parents 4b160bf + 1dc2d11 commit 86fff6f
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 71 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ jobs:
cd tauri-interop-macro
cargo test --features event,leptos,initial_value
- name: Run tests for crate
- name: Run tests for crate (no features)
run: cargo test --features=event

- name: Run tests for crate (all-features)
run: cargo test --all-features

- name: Build test-project (wasm)
Expand Down
16 changes: 1 addition & 15 deletions Cargo.lock

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

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ description = "Easily connect your rust frontend and backend without writing dup
readme = "README.md"

[dependencies]
#tauri-interop-macro = { path = "./tauri-interop-macro" }
tauri-interop-macro = "2.1.3"
tauri-interop-macro = { path = "./tauri-interop-macro" }
#tauri-interop-macro = "2.1.3"

js-sys = "0.3"
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -42,8 +42,8 @@ leptos = { version = "0.6", optional = true }
tauri = { version = "1.6", default-features = false, features = ["wry"] }

[target.'cfg(target_family = "wasm")'.dependencies]
#tauri-interop-macro = { path = "./tauri-interop-macro", features = ["_wasm"] }
tauri-interop-macro = { version = "2.1.3", features = [ "_wasm" ] }
tauri-interop-macro = { path = "./tauri-interop-macro", features = ["_wasm"] }
#tauri-interop-macro = { version = "2.1.3", features = [ "_wasm" ] }

[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
tauri = "1.6"
Expand Down
90 changes: 60 additions & 30 deletions src/command/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
use js_sys::{JsString, RegExp};
use serde::de::DeserializeOwned;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
/// Fire and forget invoke/command call
/// Binding for tauri's global invoke function
///
/// [Tauri Commands](https://tauri.app/v1/guides/features/command)
#[wasm_bindgen(js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub fn invoke(cmd: &str, args: JsValue);

/// [invoke] variant that awaits the returned value
///
/// [Async Commands](https://tauri.app/v1/guides/features/command/#async-commands)
#[wasm_bindgen(js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub async fn async_invoke(cmd: &str, args: JsValue) -> JsValue;

/// [async_invoke] variant that additionally returns a possible error
///
/// [Error Handling](https://tauri.app/v1/guides/features/command/#error-handling)
#[wasm_bindgen(catch, js_name = "invoke", js_namespace = ["window", "__TAURI__", "tauri"])]
pub async fn invoke_catch(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
/// - [Tauri Commands](https://tauri.app/v1/guides/features/command)
#[wasm_bindgen(catch, js_namespace = ["window", "__TAURI__", "tauri"])]
pub async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;

/// The binding for the frontend that listens to events
///
Expand All @@ -33,25 +22,66 @@ extern "C" {
) -> Result<JsValue, JsValue>;
}

/// Wrapper for [async_invoke], to return an
/// expected [DeserializeOwned] object
pub async fn wrapped_async_invoke<T>(command: &str, args: JsValue) -> T
enum InvokeResult {
Ok(JsValue),
Err(JsValue),
NotRegistered,
}

/// Wrapper for [invoke], to handle an unregistered function
async fn wrapped_invoke(command: &str, args: JsValue) -> InvokeResult {
match invoke(command, args).await {
Ok(value) => InvokeResult::Ok(value),
Err(value) => {
if let Some(string) = value.dyn_ref::<JsString>() {
let regex = RegExp::new("command (\\w+) not found", "g");
if string.match_(&regex).is_some() {
log::error!("Error: {string}");
return InvokeResult::NotRegistered;
}
}

InvokeResult::Err(value)
},
}
}

/// Wrapper for [wait_invoke], to send a command without waiting for it
pub fn fire_and_forget_invoke(command: &'static str, args: JsValue) {
wasm_bindgen_futures::spawn_local(wait_invoke(command, args))
}

/// Wrapper for [invoke], to await a command execution without handling the returned values
pub async fn wait_invoke(command: &'static str, args: JsValue) {
wrapped_invoke(command, args).await;
}

/// Wrapper for [invoke], to return an expected [DeserializeOwned] item
pub async fn return_invoke<T>(command: &str, args: JsValue) -> T
where
T: DeserializeOwned,
T: Default + DeserializeOwned,
{
let value = async_invoke(command, args).await;
serde_wasm_bindgen::from_value(value).expect("conversion error")
match wrapped_invoke(command, args).await {
InvokeResult::Ok(value) => serde_wasm_bindgen::from_value(value).unwrap_or_else(|why| {
log::error!("Conversion failed: {why}");
Default::default()
}),
_ => Default::default(),
}
}

/// Wrapper for [invoke_catch], to return an
/// expected [Result<T, E>] where both generics are [DeserializeOwned]
pub async fn wrapped_invoke_catch<T, E>(command: &str, args: JsValue) -> Result<T, E>
/// Wrapper for [invoke], to return an expected [Result<T, E>]
pub async fn catch_invoke<T, E>(command: &str, args: JsValue) -> Result<T, E>
where
T: DeserializeOwned,
T: Default + DeserializeOwned,
E: DeserializeOwned,
{
invoke_catch(command, args)
.await
.map(|value| serde_wasm_bindgen::from_value(value).expect("ok: conversion error"))
.map_err(|value| serde_wasm_bindgen::from_value(value).expect("err: conversion error"))
match wrapped_invoke(command, args).await {
InvokeResult::Ok(value) => Ok(serde_wasm_bindgen::from_value(value).unwrap_or_else(|why| {
log::error!("Conversion failed: {why}");
Default::default()
})),
InvokeResult::Err(value) => Err(serde_wasm_bindgen::from_value(value).unwrap()),
InvokeResult::NotRegistered => Ok(Default::default()),
}
}
21 changes: 13 additions & 8 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{de::DeserializeOwned, Serialize};
#[cfg(any(feature = "initial_value", doc))]
use serde::Deserialize;
#[cfg(not(target_family = "wasm"))]
use tauri::{AppHandle, Error, Wry};

Expand All @@ -24,15 +26,15 @@ mod listen;
#[allow(clippy::needless_doctest_main)]
/// Trait defining a [Field] to a related struct implementing [Parent] with the related [Field::Type]
///
/// When using [Event], [Emit] or [Listen], for each field of the struct, a struct named after the
/// When using [Event], [Emit] or [Listen], for each field of the struct, a struct named after the
/// field is generated. The field naming is snake_case to PascalCase, but because of the possibility
/// that the type and the field name are the same, the generated field has a "F" appended at the
/// that the type and the field name are the same, the generated field has a "F" appended at the
/// beginning to separate each other and avoid type collision.
///
///
/// ```
/// use serde::{Deserialize, Serialize};
/// use tauri_interop::{Event, event::ManagedEmit};
///
/// use tauri_interop::Event;
///
/// #[derive(Default, Clone, Serialize, Deserialize)]
/// struct Bar {
/// foo: u16
Expand All @@ -42,8 +44,9 @@ mod listen;
/// struct Test {
/// bar: Bar
/// }
///
/// impl ManagedEmit for Test {}
///
/// #[cfg(feature = "initial_value")]
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// fn main() {
/// let _ = test::FBar;
Expand Down Expand Up @@ -81,6 +84,8 @@ where
fn update(s: &mut P, handle: &AppHandle<Wry>, v: Self::Type) -> Result<(), Error>;
}

#[cfg(any(feature = "initial_value", doc))]
#[doc(cfg(feature = "initial_value"))]
/// General errors that can happen during event exchange
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
pub enum EventError {
Expand Down
7 changes: 5 additions & 2 deletions src/event/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub trait Emit: Sized {
/// pub bar: bool,
/// }
///
/// #[cfg(feature = "initial_value")]
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
Expand All @@ -85,6 +86,7 @@ pub trait Emit: Sized {
/// pub bar: bool,
/// }
///
/// #[cfg(feature = "initial_value")]
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
Expand All @@ -103,15 +105,16 @@ pub trait Emit: Sized {
/// ### Example
///
/// ```
/// use tauri_interop::{command::TauriAppHandle, event::Emit, Event};
/// use tauri_interop::{command::TauriAppHandle, Event, event::Emit};
///
///
/// #[derive(Default, Event)]
/// pub struct Test {
/// foo: String,
/// pub bar: bool,
/// }
///
/// // require because we compile
/// #[cfg(feature = "initial_value")]
/// impl tauri_interop::event::ManagedEmit for Test {}
///
/// #[tauri_interop::command]
Expand Down
1 change: 1 addition & 0 deletions src/event/listen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl ListenHandle {
{
use leptos::SignalSet;

#[cfg(any(all(target_family = "wasm", feature = "initial_value")))]
let acquire_initial_value = initial_value.is_none();
let (signal, set_signal) = leptos::create_signal(initial_value.unwrap_or_default());

Expand Down
7 changes: 4 additions & 3 deletions tauri-interop-macro/src/command/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ impl Invoke {

pub fn as_expr(&self, cmd_name: String, arg_name: &Ident) -> Expr {
let expr: Ident = match self {
Invoke::Empty => parse_quote!(invoke),
Invoke::Async | Invoke::AsyncEmpty => parse_quote!(wrapped_async_invoke),
Invoke::AsyncResult => parse_quote!(wrapped_invoke_catch),
Invoke::Empty => parse_quote!(fire_and_forget_invoke),
Invoke::AsyncEmpty => parse_quote!(wait_invoke),
Invoke::Async => parse_quote!(return_invoke),
Invoke::AsyncResult => parse_quote!(catch_invoke),
};

let call = parse_quote!( ::tauri_interop::command::bindings::#expr(#cmd_name, #arg_name) );
Expand Down
4 changes: 2 additions & 2 deletions test-project/Cargo.lock

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

4 changes: 2 additions & 2 deletions test-project/api/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ pub fn invoke_with_return_vec() -> Vec<i32> {
}

#[tauri_interop::command]
pub fn result_test() -> Result<i32, String> {
Ok(69)
pub fn result_test(switch_on: bool) -> Result<i32, String> {
switch_on.then_some(69).ok_or(String::from("oh nyo"))
}

#[tauri_interop::command]
Expand Down
2 changes: 1 addition & 1 deletion test-project/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ tauri_interop::combine_handlers!(
cmd,
model::other_cmd,
model::test_mod,
model::NamingTestEnumField,
// model::NamingTestEnumField,
model::naming_test_default
);
13 changes: 10 additions & 3 deletions test-project/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#![allow(clippy::disallowed_names)]

use api::event::Listen;
use api::model::{test_mod, TestState};
use gloo_timers::callback::Timeout;
#[cfg(feature = "leptos")]
use leptos::{component, view, IntoView};
use leptos::{component, IntoView, view};

use api::event::Listen;
use api::model::{NamingTestEnum, NamingTestEnumField, test_mod, TestState};

fn main() {
console_log::init_with_level(log::Level::Trace).expect("no errors during logger init");
Expand All @@ -16,6 +17,11 @@ fn main() {
wasm_bindgen_futures::spawn_local(async {
log::info!("{}", api::cmd::greet("frontend").await);

let result = api::cmd::result_test(true).await.expect("positiv test successful");
log::info!("positiv test successful with: {result}");
let result = api::cmd::result_test(false).await.expect_err("negativ test successful");
log::info!("negativ test successful with: {result}");

api::cmd::await_heavy_computing().await;
log::info!("heavy computing finished")
});
Expand Down Expand Up @@ -45,6 +51,7 @@ fn main() {
fn App() -> impl IntoView {
use leptos::SignalGet;

let _bar = NamingTestEnum::use_field::<NamingTestEnumField::FBar>(None);
let bar = TestState::use_field::<test_mod::FBar>(Some(true));

let exit = move |_| api::model::other_cmd::stop_application();
Expand Down

0 comments on commit 86fff6f

Please sign in to comment.