Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle unregistered commands #32

Merged
merged 5 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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