tauri_typed_ipc/lib.rs
1//! Trait-based, type-safe Tauri IPC, built on [specta](https://specta.dev)
2//! v2. Sync by default.
3//!
4//! Define your IPC surface once as a Rust trait. [`macro@procedures`]
5//! flattens it into a dispatch core and a descriptor the TypeScript client
6//! is rendered from; [`handler`] mounts that on tauri's invoke pipeline.
7//! The wire is identical to a raw `#[tauri::command]` -- same command name,
8//! same named-argument JSON, no envelope -- so generated calls,
9//! hand-written `invoke`s, and raw commands are interchangeable, and a
10//! trait can be adopted one command at a time through
11//! [`handler_with_fallback`].
12//!
13//! A procedure is a plain `fn` by default, dispatched inline on the main
14//! thread; mark one `async fn` and only that one is spawned on tauri's
15//! runtime. Sync-first is the founding choice: a sync handler carries no
16//! `Send` bound on its own logic and skips the executor hop, where TauRPC
17//! makes every procedure `async`. (A sync handler still runs on the UI
18//! thread, so long-running work opts into `async`.)
19//!
20//! # Quickstart
21//!
22//! Define the surface and implement it:
23//!
24//! ```
25//! use tauri_typed_ipc::procedures;
26//!
27//! #[procedures]
28//! trait Greeter {
29//! fn greet(&self, name: String) -> String;
30//! }
31//!
32//! struct Backend;
33//!
34//! impl Greeter for Backend {
35//! fn greet(&self, name: String) -> String {
36//! format!("Hello, {name}!")
37//! }
38//! }
39//!
40//! // `#[procedures]` adds `into_procedures`, which type-erases the trait
41//! // for the handler:
42//! let procedures = Backend.into_procedures();
43//! assert_eq!(procedures.names(), &["greet"]);
44//! ```
45//!
46//! Mount every procedure on one handler. `handler` adapts the type-erased
47//! procedures into a tauri invoke handler; drop it into your `Builder`,
48//! then run as usual with `.run(tauri::generate_context!())`:
49//!
50//! ```no_run
51//! # use tauri_typed_ipc::procedures;
52//! # #[procedures]
53//! # trait Greeter {
54//! # fn greet(&self, name: String) -> String;
55//! # }
56//! # struct Backend;
57//! # impl Greeter for Backend {
58//! # fn greet(&self, name: String) -> String {
59//! # format!("Hello, {name}!")
60//! # }
61//! # }
62//! fn mount<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
63//! builder.invoke_handler(tauri_typed_ipc::handler(Backend.into_procedures()))
64//! }
65//! ```
66//!
67//! With the `export` feature, render the matching client and guard it
68//! against drift:
69//!
70//! ```no_run
71//! # use tauri_typed_ipc::procedures;
72//! # #[procedures]
73//! # trait Greeter {
74//! # fn greet(&self, name: String) -> String;
75//! # }
76//! tauri_typed_ipc::Bindings::new()
77//! .register::<GreeterProcedures>()
78//! .export_to("../src/bindings.ts")?;
79//! # Ok::<(), Box<dyn std::error::Error>>(())
80//! ```
81//!
82//! ```typescript
83//! import { greeter } from "./bindings";
84//! const hello = await greeter.greet("world"); // Promise<string>
85//! ```
86//!
87//! # What's here
88//!
89//! - **Procedures** -- [`macro@procedures`] on a trait: a sync `fn`
90//! dispatches inline, an `async fn` is spawned on tauri's runtime.
91//! - **Typed client** -- with the `export` feature, `Bindings` renders a
92//! TypeScript client from the generated [`ProcedureSet`] descriptor, and
93//! its `check` fails CI when the committed client drifts.
94//! - **Events**, both directions -- [`macro@Event`] on an enum emits typed
95//! payloads and generates the matching listeners.
96//! - **Typed errors** -- [`macro@Error`]: a `Result<_, E>` procedure
97//! rejects `E` on the wire, and the client types its `catch` against it.
98//! - **Injection by type** -- an `AppHandle` or [`tauri::State`] parameter
99//! is resolved from the [`Context`] by its type, never by its name.
100//! - **Streaming** -- a [`Channel<T>`](Channel) parameter streams values
101//! back to the caller, on top of the value the procedure returns.
102//! - **Composition** -- [`Procedures::merge`] mounts several traits on one
103//! handler.
104
105mod bindings;
106mod channel;
107mod context;
108mod handler;
109#[cfg(feature = "validate")]
110mod validate;
111
112#[cfg(feature = "export")]
113pub use bindings::{Bindings, BindingsError, Layout, MethodCase};
114pub use bindings::{ErrorSet, EventSet, ProcedureSet};
115#[cfg(feature = "validate")]
116pub use handler::{handler_validated, handler_validated_with_fallback};
117#[cfg(feature = "validate")]
118pub use validate::{ValidationError, Validator, ValidatorError};
119// Descriptor payloads carried from the derives to the bindings generator. Named
120// in generated code (`ProcedureSet::procedures` etc.), never built by hand.
121#[doc(hidden)]
122pub use bindings::{ErrorType, EventType, ProcedureType};
123pub use channel::Channel;
124pub use context::Context;
125pub use handler::{Procedures, handler, handler_with_fallback};
126// The dispatch result types: returned by generated `dispatch` and consumed by
127// `handler`. Plumbing, not part of the hand-written surface.
128#[doc(hidden)]
129pub use handler::{Dispatch, Outcome};
130pub use tauri_typed_ipc_macros::{Error, Event, procedures};
131
132/// Error returned by a generated `dispatch` when a call cannot reach
133/// or leave the procedure. Wire-level failures only -- a procedure's
134/// own failures belong in its return type.
135#[derive(Debug, thiserror::Error)]
136pub enum DispatchError {
137 /// No procedure with the requested name exists in this set.
138 #[error("unknown procedure: {0}")]
139 UnknownProcedure(String),
140 /// The arguments failed to deserialize into the procedure's
141 /// parameter types.
142 #[error("invalid arguments: {0}")]
143 Deserialize(#[source] serde_json::Error),
144 /// The procedure's return value failed to serialize.
145 #[error("response serialization failed: {0}")]
146 Serialize(#[source] serde_json::Error),
147 /// The invoke arrived with a raw byte body; procedures take JSON
148 /// named arguments.
149 #[error("procedures take JSON arguments, not a raw body")]
150 RawBody,
151 /// A parameter is injected by type, but the call's [`Context`]
152 /// offered no value of that type.
153 #[error("missing injectable value: {0}")]
154 MissingInjection(&'static str),
155 /// A `State<T>` parameter was requested, but no value of that type
156 /// is managed -- the app never called `.manage()` for it.
157 #[error("state not managed: {0} -- did the app call `.manage()` for it?")]
158 MissingState(&'static str),
159 /// A `Channel<T>` parameter was requested, but the dispatch
160 /// [`Context`] carried no channel factory. The handler always
161 /// installs one from the webview, so this arises only when
162 /// dispatching against a bare [`Context`] (no [`with_channels`]).
163 ///
164 /// [`with_channels`]: Context::with_channels
165 #[error("no channel factory: {0}")]
166 MissingChannel(&'static str),
167 /// The payload failed runtime validation against the command's schema.
168 /// Only produced by [`handler_validated`], under the `validate` feature;
169 /// rejected at the boundary, before the procedure runs.
170 #[cfg(feature = "validate")]
171 #[error(transparent)]
172 Invalid(#[from] crate::validate::ValidationError),
173}
174
175// Referenced by macro-generated code. Not public API.
176#[doc(hidden)]
177pub mod __private {
178 pub use serde;
179 pub use serde_json;
180 pub use specta;
181 pub use tauri;
182}