diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5bdbc4e..5131343 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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) diff --git a/Cargo.lock b/Cargo.lock index ace424e..a5631e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3327,7 +3327,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "tauri", - "tauri-interop-macro 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-interop-macro", "thiserror", "wasm-bindgen", "wasm-bindgen-futures", @@ -3349,20 +3349,6 @@ dependencies = [ "tauri-interop", ] -[[package]] -name = "tauri-interop-macro" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa63b70e8ab92c2a60b8bc6fbb112c3036ec0fab2ac598f5e5bee58c6533788" -dependencies = [ - "convert_case 0.6.0", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.52", -] - [[package]] name = "tauri-macros" version = "1.4.3" diff --git a/Cargo.toml b/Cargo.toml index 71f0171..53d3e74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } @@ -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" diff --git a/src/command/bindings.rs b/src/command/bindings.rs index 4579a0c..9b43850 100644 --- a/src/command/bindings.rs +++ b/src/command/bindings.rs @@ -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; + /// - [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; /// The binding for the frontend that listens to events /// @@ -33,25 +22,66 @@ extern "C" { ) -> Result; } -/// Wrapper for [async_invoke], to return an -/// expected [DeserializeOwned] object -pub async fn wrapped_async_invoke(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::() { + let regex = RegExp::new("command (\\w+) not found", "g"); + if string.match_(®ex).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(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] where both generics are [DeserializeOwned] -pub async fn wrapped_invoke_catch(command: &str, args: JsValue) -> Result +/// Wrapper for [invoke], to return an expected [Result] +pub async fn catch_invoke(command: &str, args: JsValue) -> Result 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()), + } } diff --git a/src/event.rs b/src/event.rs index b2e8607..4a0465d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -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}; @@ -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 @@ -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; @@ -81,6 +84,8 @@ where fn update(s: &mut P, handle: &AppHandle, 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 { diff --git a/src/event/emit.rs b/src/event/emit.rs index 25c735c..5e8fb58 100644 --- a/src/event/emit.rs +++ b/src/event/emit.rs @@ -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] @@ -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] @@ -103,7 +105,8 @@ 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 { @@ -111,7 +114,7 @@ pub trait Emit: Sized { /// pub bar: bool, /// } /// - /// // require because we compile + /// #[cfg(feature = "initial_value")] /// impl tauri_interop::event::ManagedEmit for Test {} /// /// #[tauri_interop::command] diff --git a/src/event/listen.rs b/src/event/listen.rs index 459bbd3..f0bcfc9 100644 --- a/src/event/listen.rs +++ b/src/event/listen.rs @@ -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()); diff --git a/tauri-interop-macro/src/command/wrapper.rs b/tauri-interop-macro/src/command/wrapper.rs index 04a363c..58acc54 100644 --- a/tauri-interop-macro/src/command/wrapper.rs +++ b/tauri-interop-macro/src/command/wrapper.rs @@ -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) ); diff --git a/test-project/Cargo.lock b/test-project/Cargo.lock index 418fec6..d08814e 100644 --- a/test-project/Cargo.lock +++ b/test-project/Cargo.lock @@ -3483,7 +3483,7 @@ dependencies = [ [[package]] name = "tauri-interop" -version = "2.1.1" +version = "2.1.4" dependencies = [ "js-sys", "leptos", @@ -3499,7 +3499,7 @@ dependencies = [ [[package]] name = "tauri-interop-macro" -version = "2.1.1" +version = "2.1.3" dependencies = [ "convert_case 0.6.0", "lazy_static", diff --git a/test-project/api/src/cmd.rs b/test-project/api/src/cmd.rs index 0c5e517..9494cb4 100644 --- a/test-project/api/src/cmd.rs +++ b/test-project/api/src/cmd.rs @@ -40,8 +40,8 @@ pub fn invoke_with_return_vec() -> Vec { } #[tauri_interop::command] -pub fn result_test() -> Result { - Ok(69) +pub fn result_test(switch_on: bool) -> Result { + switch_on.then_some(69).ok_or(String::from("oh nyo")) } #[tauri_interop::command] diff --git a/test-project/api/src/lib.rs b/test-project/api/src/lib.rs index 5a74af0..8f3da04 100644 --- a/test-project/api/src/lib.rs +++ b/test-project/api/src/lib.rs @@ -14,6 +14,6 @@ tauri_interop::combine_handlers!( cmd, model::other_cmd, model::test_mod, - model::NamingTestEnumField, + // model::NamingTestEnumField, model::naming_test_default ); diff --git a/test-project/src/main.rs b/test-project/src/main.rs index 35d010b..f8a8353 100644 --- a/test-project/src/main.rs +++ b/test-project/src/main.rs @@ -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"); @@ -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") }); @@ -45,6 +51,7 @@ fn main() { fn App() -> impl IntoView { use leptos::SignalGet; + let _bar = NamingTestEnum::use_field::(None); let bar = TestState::use_field::(Some(true)); let exit = move |_| api::model::other_cmd::stop_application();