diff --git a/.gitignore b/.gitignore index 68d2e05..aa55085 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sw? target -dist \ No newline at end of file +dist +/_out \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c060fe5..8df2dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ tauri-specta-macros = { version = "=2.0.0-rc.6", optional = true, path = "./macr serde = "1" serde_json = "1" thiserror = "1" -tauri = { workspace = true, default-features = false, features = ["specta"] } +tauri = { workspace = true, features = ["specta"] } # Private heck = "0.5.0" diff --git a/README.md b/README.md index d1a0bb6..0f5c85b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,13 @@ cd examples/app/ pnpm tauri dev ``` +### Running tests + +```bash +mkdir _out +OUT_DIR="$(pwd)/_out" cargo test --all --all-features +``` + ## Credit Created by [oscartbeaumont](https://github.com/oscartbeaumont) and [Brendonovich](https://github.com/brendonovich). diff --git a/examples/app/src-tauri/src/main.rs b/examples/app/src-tauri/src/main.rs index f50daae..0c62edc 100644 --- a/examples/app/src-tauri/src/main.rs +++ b/examples/app/src-tauri/src/main.rs @@ -116,7 +116,7 @@ pub struct Testing { } fn main() { - let mut builder = Builder::::new() + let builder = Builder::::new() .commands(tauri_specta::collect_commands![ hello_world, goodbye_world, @@ -128,7 +128,7 @@ fn main() { typesafe_errors_using_thiserror_with_value, ]) .events(tauri_specta::collect_events![crate::DemoEvent, EmptyEvent]) - .ty::() + .typ::() .constant("universalConstant", 42); #[cfg(debug_assertions)] diff --git a/examples/custom-plugin/plugin/bindings.ts b/examples/custom-plugin/plugin/bindings.ts index fe4b7ea..70d340c 100644 --- a/examples/custom-plugin/plugin/bindings.ts +++ b/examples/custom-plugin/plugin/bindings.ts @@ -1,18 +1,20 @@ - // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. - /** user-defined commands **/ +/** user-defined commands **/ - export const commands = { + +export const commands = { /** * Adds two numbers, returning the result. */ async addNumbers(a: number, b: number) : Promise { -return await TAURI_INVOKE("plugin:specta-example|add_numbers", { a, b }); + return await TAURI_INVOKE("plugin:specta-example|add_numbers", { a, b }); } } - /** user-defined events **/ +/** user-defined events **/ + export const events = __makeEvents__<{ randomNumber: RandomNumber @@ -20,9 +22,9 @@ randomNumber: RandomNumber randomNumber: "plugin:specta-example:random-number" }) - /** user-defined statics **/ +/** user-defined constants **/ + - /** user-defined types **/ @@ -30,59 +32,60 @@ export type RandomNumber = number /** tauri-specta globals **/ - import { invoke as TAURI_INVOKE } from "@tauri-apps/api/core"; +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { - listen: ( - cb: TAURI_API_EVENT.EventCallback - ) => ReturnType>; - once: ( - cb: TAURI_API_EVENT.EventCallback - ) => ReturnType>; - emit: T extends null - ? (payload?: T) => ReturnType - : (payload: T) => ReturnType; + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: T extends null + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; }; export type Result = - | { status: "ok"; data: T } - | { status: "error"; error: E }; + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( - mappings: Record + mappings: Record, ) { - return new Proxy( - {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; - }, - { - get: (_, event) => { - const name = mappings[event as keyof T]; - - return new Proxy((() => {}) as any, { - apply: (_, __, [window]: [__WebviewWindow__]) => ({ - listen: (arg: any) => window.listen(name, arg), - once: (arg: any) => window.once(name, arg), - emit: (arg: any) => window.emit(name, arg), - }), - get: (_, command: keyof __EventObj__) => { - switch (command) { - case "listen": - return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case "once": - return (arg: any) => TAURI_API_EVENT.once(name, arg); - case "emit": - return (arg: any) => TAURI_API_EVENT.emit(name, arg); - } - }, - }); - }, - } - ); + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); } - - \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 73c1236..f6bbb23 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -5,21 +5,94 @@ )] use heck::ToKebabCase; +use proc_macro2::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::{ + parse_macro_input, parse_quote, ConstParam, DeriveInput, GenericParam, Generics, LifetimeParam, + TypeParam, WhereClause, +}; #[proc_macro_derive(Event, attributes(tauri_specta))] pub fn derive_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let crate_ref = quote!(tauri_specta); - let DeriveInput { ident, .. } = parse_macro_input!(input); + let DeriveInput { + ident, generics, .. + } = parse_macro_input!(input); let name = ident.to_string().to_kebab_case(); + let bounds = generics_with_ident_and_bounds_only(&generics); + let type_args = generics_with_ident_only(&generics); + let where_bound = add_type_to_where_clause(&generics); quote! { - impl #crate_ref::Event for #ident { + #[automatically_derived] + impl #bounds #crate_ref::Event for #ident #type_args #where_bound { const NAME: &'static str = #name; } } .into() } + +fn generics_with_ident_and_bounds_only(generics: &Generics) -> Option { + (!generics.params.is_empty()) + .then(|| { + use GenericParam::*; + generics.params.iter().map(|param| match param { + Type(TypeParam { + ident, + colon_token, + bounds, + .. + }) => quote!(#ident #colon_token #bounds), + Lifetime(LifetimeParam { + lifetime, + colon_token, + bounds, + .. + }) => quote!(#lifetime #colon_token #bounds), + Const(ConstParam { + const_token, + ident, + colon_token, + ty, + .. + }) => quote!(#const_token #ident #colon_token #ty), + }) + }) + .map(|gs| quote!(<#(#gs),*>)) +} + +fn generics_with_ident_only(generics: &Generics) -> Option { + (!generics.params.is_empty()) + .then(|| { + use GenericParam::*; + + generics.params.iter().map(|param| match param { + Type(TypeParam { ident, .. }) | Const(ConstParam { ident, .. }) => quote!(#ident), + Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime), + }) + }) + .map(|gs| quote!(<#(#gs),*>)) +} + +fn add_type_to_where_clause(generics: &Generics) -> Option { + let generic_types = generics + .params + .iter() + .filter_map(|gp| match gp { + GenericParam::Type(ty) => Some(ty.ident.clone()), + _ => None, + }) + .collect::>(); + if generic_types.is_empty() { + return generics.where_clause.clone(); + } + match generics.where_clause { + None => None, + Some(ref w) => { + let bounds = w.predicates.iter(); + Some(parse_quote! { where #(#bounds),* }) + } + } +} diff --git a/src/builder.rs b/src/builder.rs index 03df8e1..32b195e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -26,13 +26,18 @@ use tauri::{ipc::Invoke, Manager, Runtime}; /// /// You can extend this example by calling other methods on the builder to configure your application further. /// -/// ```rust +/// ```rust,no_run +/// use tauri_specta::{collect_commands, collect_events, Builder}; +/// use specta_typescript::Typescript; +/// +/// /// let mut builder = ::new() -/// .commands(tauri_specta::collect_commands![]); +/// .commands(collect_commands![]) +/// .events(collect_events![]); /// /// #[cfg(debug_assertions)] /// builder -/// .export(Typescript::default().path("../src/bindings.ts")) +/// .export(Typescript::default(), "../src/bindings.ts") /// .expect("Failed to export typescript bindings"); /// /// tauri::Builder::default() @@ -42,14 +47,38 @@ use tauri::{ipc::Invoke, Manager, Runtime}; /// /// Ok(()) /// }) -/// .run(tauri::generate_context!()) +/// // on an actual app, remove the string argument +/// .run(tauri::generate_context!("tests/tauri.conf.json")) /// .expect("error while running tauri application"); /// ``` /// /// # Exporting using JSDoc /// -/// ```rs -/// # TODO +/// ```rust,no_run +/// use tauri_specta::{collect_commands,collect_events,Builder}; +/// use specta_jsdoc::JSDoc; +/// +/// +/// let mut builder = ::new() +/// .commands(collect_commands![]) +/// .events(collect_events![]); +/// +/// // exporting to JsDoc +/// #[cfg(debug_assertions)] +/// builder +/// .export(JSDoc::default(), "../src/bindings.js") +/// .expect("Failed to export jsdoc bindings"); +/// +/// tauri::Builder::default() +/// .invoke_handler(builder.invoke_handler()) // < Required for commands to work +/// .setup(move |app| { +/// builder.mount_events(app); // < Required for events to work +/// +/// Ok(()) +/// }) +/// // on an actual app, remove the string argument +/// .run(tauri::generate_context!("tests/tauri.conf.json")) +/// .expect("error while running tauri application"); /// ``` pub struct Builder { // TODO: Can we just hold a `ExportContext` here to make it a bit neater??? @@ -97,6 +126,20 @@ impl Builder { /// Register commands with the builder. /// /// **WARNING:** This method will overwrite any previously registered commands. + /// + /// # Example + /// + /// ``` + /// use tauri_specta::{Builder, collect_commands}; + /// + /// #[tauri::command] + /// #[specta::specta] + /// fn hello_world(my_name: String) -> String { + /// format!("Hello, {my_name}! You've been greeted from Rust!") + /// } + /// + /// let mut builder = Builder::::new().commands(collect_commands![hello_world]); + /// ``` pub fn commands(mut self, commands: Commands) -> Self { Self { command_types: (commands.1)(&mut self.types), @@ -108,6 +151,19 @@ impl Builder { /// Register events with the builder. /// /// **WARNING:** This method will overwrite any previously registered events. + /// + /// # Example + /// + /// ``` + /// use serde::{Serialize, Deserialize}; + /// use specta::Type; + /// use tauri_specta::{Builder, collect_events, Event}; + /// + /// #[derive(Serialize, Deserialize, Debug, Clone, Type, Event)] + /// pub struct DemoEvent(String); + /// + /// let mut builder = Builder::::new().events(collect_events![DemoEvent]); + /// ``` pub fn events(mut self, events: Events) -> Self { let mut event_sids = BTreeSet::new(); let events = events @@ -129,10 +185,34 @@ impl Builder { ..self } } + + /// This method is deprecated. Please use [Self::typ]. + #[deprecated(note = "Use `Self::ty` instead")] + pub fn ty(mut self) -> Self { + let dt = T::definition_named_data_type(&mut self.types); + self.types.insert(T::sid(), dt); + self + } + /// Export a new type with the frontend. /// /// This is useful if you want to export types that do not appear in any events or commands. - pub fn ty(mut self) -> Self { + /// + /// # Example + /// + /// ``` + /// use tauri_specta::Builder; + /// use serde::{Serialize, Deserialize}; + /// use specta::Type; + /// + /// #[derive(Serialize, Deserialize, Type)] + /// pub struct MyStruct { + /// a: String + /// } + /// + /// let mut builder = Builder::::new().typ::(); + /// ``` + pub fn typ(mut self) -> Self { let dt = T::definition_named_data_type(&mut self.types); self.types.insert(T::sid(), dt); self @@ -141,6 +221,14 @@ impl Builder { /// Export a constant value to the frontend. /// /// This is useful to share application-wide constants or expose data which is generated by Rust. + /// + /// # Example + /// + /// ``` + /// use tauri_specta::Builder; + /// + /// let mut builder = Builder::::new().constant("CONSTANT_NAME","ANY_CONSTANT_VALUE"); + /// ``` #[track_caller] pub fn constant(mut self, k: impl Into>, v: T) -> Self { self.constants.insert( @@ -167,6 +255,26 @@ impl Builder { } /// Mount all of the events in the builder onto a Tauri app. + /// + /// This should be called within [`tauri::Builder::setup`](tauri::Builder::setup) like the example below. + /// + /// # Example + /// + /// ```rust,no_run + /// use tauri_specta::{Builder, collect_events}; + /// + /// let mut builder = Builder::::new().events(collect_events![]); + /// + /// tauri::Builder::default() + /// .setup(move |app| { + /// builder.mount_events(app); + /// + /// Ok(()) + /// }) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("tests/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` pub fn mount_events(&self, handle: &impl Manager) { let registry = EventRegistry::get_or_manage(handle); let mut map = registry.0.write().expect("Failed to lock EventRegistry"); @@ -183,9 +291,22 @@ impl Builder { /// Export the bindings to a string. /// + /// You should prefer to use [`Self::export`], unless you need explicit control over saving. + /// /// # Example - /// ```rust - /// # TODO + /// ``` + /// use std::{ + /// fs::File, + /// io::Write + /// }; + /// use specta_typescript::Typescript; + /// + /// println!( + /// "{}", + /// tauri_specta::Builder::::new() + /// .export_str(Typescript::new()) + /// .unwrap() + /// ); /// ``` pub fn export_str(&self, language: L) -> Result { // TODO: Handle duplicate type names @@ -205,8 +326,18 @@ impl Builder { /// Export the bindings to a file. /// /// # Example - /// ```rust - /// # TODO + /// ``` + /// use tauri_specta::{Builder, collect_commands, collect_events}; + /// use specta_typescript::Typescript; + /// + /// let mut builder = Builder::::new() + /// .commands(collect_commands![]) + /// .events(collect_events![]); + /// + /// #[cfg(debug_assertions)] // only export on debug builds. + /// builder + /// .export(Typescript::default(), "../src/bindings.ts") + /// .expect("Failed to export typescript bindings"); /// ``` pub fn export( &self, diff --git a/src/event.rs b/src/event.rs index 904aa4c..0280216 100644 --- a/src/event.rs +++ b/src/event.rs @@ -83,17 +83,17 @@ macro_rules! make_handler { /// use serde::{Serialize, Deserialize}; /// use specta::Type; /// use tauri_specta::Event; -/// use tauri::AppHandle +/// use tauri::AppHandle; /// /// #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] /// pub struct MyEvent(String); /// -/// fn use_event(app: AppHandle) { -/// DemoEvent::listen(handle, |event| { +/// fn use_event(app_handle: AppHandle) { +/// MyEvent::listen(&app_handle, |event| { /// dbg!(event.payload); /// }); /// -/// DemoEvent("Test".to_string()).emit(handle).ok(); +/// MyEvent("Test".to_string()).emit(&app_handle).ok(); /// } /// ``` pub trait Event: NamedType { diff --git a/src/lib.rs b/src/lib.rs index 8647b54..b042b73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ //! //! The follow is a minimal example of how to setup Tauri Specta with Typescript. //! -//! ```rust +//! ```rust,no_run //! #![cfg_attr( //! all(not(debug_assertions), target_os = "windows"), //! windows_subsystem = "windows" @@ -72,16 +72,20 @@ //! //! Ok(()) //! }) -//! .run(tauri::generate_context!()) +//! // on an actual app, remove the string argument +//! .run(tauri::generate_context!("tests/tauri.conf.json")) //! .expect("error while running tauri application"); //! } //! ``` //! //! ## Export to JSDoc //! -//! If your interested in using JSDoc instead of Typescript you can replace the [`Typescript`](specta_typescript::Typescript) struct with [`JSDoc`](specta_jsdoc::JSDoc) like the following: +//! If your interested in using JSDoc instead of Typescript you can replace the [`specta_typescript::Typescript`](https://docs.rs/specta-typescript/latest/specta_typescript/struct.Typescript.html) struct +//! with [`specta_jsdoc::JSDoc`](https://docs.rs/specta-jsdoc/latest/specta_jsdoc/struct.JSDoc.html) like the following: //! //! ```rust +//! let mut builder = tauri_specta::Builder::::new(); +//! //! #[cfg(debug_assertions)] //! builder //! .export(specta_jsdoc::JSDoc::default(), "../src/bindings.js") @@ -107,6 +111,9 @@ //! pub struct MyStruct { //! a: String //! } +//! +//! // Call `typ()` as much as you want. +//! let mut builder = tauri_specta::Builder::::new().typ::(); //! ``` //! //! ## Events @@ -122,13 +129,11 @@ //! #[derive(Serialize, Deserialize, Debug, Clone, Type, Event)] //! pub struct DemoEvent(String); //! -//! fn main() { -//! let mut builder = Builder::::new() -//! .commands(collect_commands![hello_world,]) +//! let mut builder = Builder::::new() //! // and then register it to your builder -//! .events(collect_events![MyEvent,]); +//! .events(collect_events![DemoEvent]); //! -//! tauri::Builder::default() +//! tauri::Builder::default() //! .invoke_handler(builder.invoke_handler()) //! .setup(move |app| { //! // Ensure you mount your events! @@ -143,10 +148,7 @@ //! DemoEvent("Test".into()).emit(app).unwrap(); //! //! Ok(()) -//! }) -//! .run(tauri::generate_context!()) -//! .expect("error while running tauri application"); -//! } +//! }); //! ``` //! //! and it can be used on the frontend like the following: diff --git a/src/macros.rs b/src/macros.rs index 8768f05..fb3f09b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,14 +1,42 @@ /// Collect commands and their types. /// -/// This is a combination of Tauri's [`generate_handler`](tauri::generate_handler) and Specta's [`collect_functions`](specta::function::collect_functions). -/// -/// This returns a [`Commands`](crate::Commands) struct that can be passed to [`Builder::commands`](crate::Builder::commands). +/// This is a combination of Tauri's [`generate_handler`](tauri::generate_handler) and Specta's [`collect_functions`](specta::function), +/// returning a [`Commands`](crate::Commands) struct that can be passed to [`Builder::commands`](crate::Builder::commands). /// /// # Usage -/// ```rust -/// collect_commands![]; -/// collect_commands![hello_world]; -/// collect_commands![hello_world, some::path::function, generic_fn::]; +/// ``` +/// use tauri_specta::{collect_commands,Builder}; +/// +/// #[tauri::command] +/// #[specta::specta] // < You must annotate your commands +/// fn hello_world(my_name: String) -> String { +/// format!("Hello, {my_name}! You've been greeted from Rust!") +/// } +/// +/// #[tauri::command] +/// #[specta::specta] // < You must annotate your commands +/// fn generic_command(my_name: tauri::AppHandle) -> String { +/// format!("You've been greeted from a generic Rust function!") +/// } +/// +/// mod hello { +/// #[tauri::command] +/// #[specta::specta] // < You must annotate your commands +/// pub fn world() -> String { +/// format!("Hello world") +/// } +/// } +/// +/// let mut builder = Builder::::new() +/// .commands(collect_commands![ +/// // You can pass a function name. +/// hello_world, +/// // You can also pass a module. +/// hello::world, +/// // Unlike `tauri::generate_handler` you may need to specify generics. +/// generic_command:: +/// +/// ]); /// ``` /// #[macro_export] @@ -28,9 +56,35 @@ macro_rules! collect_commands { /// /// # Usage /// ```rust -/// collect_events![]; -/// collect_events![MyEvent]; -/// collect_events![MyEvent, module::MyOtherEvent]; +/// use serde::{Serialize, Deserialize}; +/// use specta::Type; +/// use tauri_specta::{Event, Builder, collect_events}; +/// +/// #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +/// pub struct MyEvent(String); +/// +/// #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +/// pub struct MyGenericEvent(T); +/// +/// mod hello { +/// # use serde::{Serialize, Deserialize}; +/// # use specta::Type; +/// # use tauri_specta::{Event, Builder, collect_events}; +/// use super::*; +/// +/// #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] +/// pub struct World(String); +/// } +/// +/// let mut builder = Builder::::new() +/// .events(collect_events![ +/// // You can pass a struct name. +/// MyEvent, +/// // You can also pass a module. +/// hello::World, +/// // or you can specify generics. +/// MyGenericEvent:: +/// ]); /// ``` /// #[macro_export] diff --git a/tests/icons/icon.ico b/tests/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/tests/icons/icon.ico differ diff --git a/tests/icons/icon.ico~dev b/tests/icons/icon.ico~dev new file mode 100644 index 0000000..db7fd98 Binary files /dev/null and b/tests/icons/icon.ico~dev differ diff --git a/tests/icons/icon.png b/tests/icons/icon.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/tests/icons/icon.png differ diff --git a/tests/tauri.conf.json b/tests/tauri.conf.json new file mode 100644 index 0000000..1488123 --- /dev/null +++ b/tests/tauri.conf.json @@ -0,0 +1,20 @@ +{ + "identifier": "studio.tauri.example", + "build": { + "frontendDist": "./icons", + "devUrl": "http://localhost:4000" + }, + "app": { + "windows": [ + { + "title": "Tauri App" + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true + } +}