tauri_specta/
lib.rs

1//! Typesafe Tauri commands
2//!
3//! ## Install
4//!
5//! ```bash
6//! cargo add specta
7//! cargo add tauri-specta --features javascript,typescript
8//! ```
9//!
10//! ## Adding Specta to custom types
11//!
12//! ```rust
13//! use specta::Type;
14//! use serde::{Deserialize, Serialize};
15//!
16//! // The `specta::Type` macro allows us to understand your types
17//! // We implement `specta::Type` on primitive types for you.
18//! // If you want to use a type from an external crate you may need to enable the feature on Specta.
19//! #[derive(Serialize, Type)]
20//! pub struct MyCustomReturnType {
21//!     pub some_field: String,
22//! }
23//!
24//! #[derive(Deserialize, Type)]
25//! pub struct MyCustomArgumentType {
26//!     pub foo: String,
27//!     pub bar: i32,
28//! }
29//! ```
30//!
31//! ## Annotate your Tauri commands with Specta
32//!
33//! ```rust
34//! # //! #[derive(Serialize, Type)]
35//! # pub struct MyCustomReturnType {
36//! #    pub some_field: String,
37//! # }
38//! #[tauri::command]
39//! #[specta::specta] // <-- This bit here
40//! fn greet3() -> MyCustomReturnType {
41//!     MyCustomReturnType {
42//!         some_field: "Hello World".into(),
43//!     }
44//! }
45//!
46//! #[tauri::command]
47//! #[specta::specta] // <-- This bit here
48//! fn greet(name: String) -> String {
49//!   format!("Hello {name}!")
50//! }
51//! ```
52//!
53//! ## Export your bindings
54//!
55//! ```rust
56//! # #[specta::specta]
57//! # fn greet() {}
58//! # #[specta::specta]
59//! # fn greet2() {}
60//! # #[specta::specta]
61//! # fn greet3() {}
62//! use specta::collect_types;
63//! use tauri_specta::{ts, js};
64//!
65//! // this example exports your types on startup when in debug mode or in a unit test. You can do whatever.
66//! fn main() {
67//!     #[cfg(debug_assertions)]
68//!     ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();
69//!
70//!     // or export to JS with JSDoc
71//!     #[cfg(debug_assertions)]
72//!     js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
73//! }
74//!
75//! #[test]
76//! fn export_bindings() {
77//!     ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();
78//!     js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
79//! }
80//! ```
81//!
82//! ## Usage on frontend
83//!
84//! ```ts
85//! import * as commands from "./bindings"; // This should point to the file we export from Rust
86//!
87//! await commands.greet("Brendan");
88//! ```
89//!
90#![forbid(unsafe_code)]
91#![warn(clippy::all, clippy::unwrap_used, clippy::panic, missing_docs)]
92#![cfg_attr(docsrs, feature(doc_cfg))]
93
94use std::{
95    borrow::Cow,
96    fs::{self, File},
97    io::Write,
98    marker::PhantomData,
99    path::{Path, PathBuf},
100};
101
102use specta::{
103    functions::FunctionDataType,
104    ts::{ExportConfiguration, TsExportError},
105    ExportError, TypeDefs,
106};
107
108/// The exporter for [Javascript](https://www.javascript.com).
109#[cfg(feature = "javascript")]
110#[cfg_attr(docsrs, doc(cfg(feature = "javascript")))]
111pub mod js;
112
113/// The exporter for [TypeScript](https://www.typescriptlang.org).
114#[cfg(feature = "typescript")]
115#[cfg_attr(docsrs, doc(cfg(feature = "typescript")))]
116pub mod ts;
117
118/// This remains for backwards compatibility. Please use [`specta::collect_types`] instead!
119#[macro_export]
120#[deprecated(
121    note = "Please use `specta::collect_types` instead! This alias will be removed in a future release."
122)]
123macro_rules! collate_types {
124    (type_map: $type_map:ident, $($command:path),*) => {{
125        specta::functions::collect_types!(type_map: $type_map, $($command),*)
126    }};
127    ($($command:path),*) => {{
128        let mut type_map = specta::TypeDefs::default();
129        specta::functions::collect_types!(type_map: type_map, $($command),*)
130    }};
131}
132
133pub(crate) const DO_NOT_EDIT: &str = "// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.";
134
135pub(crate) const CRINGE_ESLINT_DISABLE: &str = "/* eslint-disable */
136";
137
138// TODO
139// #[cfg(doctest)]
140// doc_comment::doctest!("../README.md");
141
142/// A set of functions that produce language-specific code
143pub trait ExportLanguage {
144    /// Type definitions and constants that the generated functions rely on
145    fn globals() -> String;
146
147    /// Renders a collection of [`FunctionDataType`] into a string.
148    fn render_functions(
149        function_types: Vec<FunctionDataType>,
150        cfg: &specta::ts::ExportConfiguration,
151    ) -> Result<String, TsExportError>;
152
153    /// Renders the output of [`globals`], [`render_functions`] and all dependant types into a TypeScript string.
154    fn render(
155        function_types: Vec<FunctionDataType>,
156        type_map: TypeDefs,
157        cfg: &specta::ts::ExportConfiguration,
158    ) -> Result<String, TsExportError>;
159}
160
161/// General exporter, takes a generic for the specific language that is being exported to.
162pub struct Exporter<TLang: ExportLanguage> {
163    macro_data: Result<(Vec<FunctionDataType>, TypeDefs), ExportError>,
164    export_path: PathBuf,
165    cfg: Option<ExportConfiguration>,
166    header: Cow<'static, str>,
167    lang: PhantomData<TLang>,
168}
169
170impl<TLang: ExportLanguage> Exporter<TLang> {
171    /// Creates a new TypeScript exporter
172    pub fn new(
173        macro_data: Result<(Vec<FunctionDataType>, TypeDefs), ExportError>,
174        export_path: impl AsRef<Path>,
175    ) -> Self {
176        Self {
177            macro_data,
178            export_path: export_path.as_ref().into(),
179            cfg: None,
180            header: CRINGE_ESLINT_DISABLE.into(), // TODO: Remove this as a default. SemVer moment.
181            lang: Default::default(),
182        }
183    }
184
185    /// Allows for specifying a custom [`ExportConfiguration`](specta::ts::ExportConfiguration).
186    pub fn with_cfg(mut self, cfg: ExportConfiguration) -> Self {
187        self.cfg = Some(cfg);
188        self
189    }
190
191    /// Allows for specifying a custom header to
192    pub fn with_header(mut self, header: &'static str) -> Self {
193        self.header = header.into();
194        self
195    }
196
197    /// Exports the output of [`internal::render`] for a collection of [`FunctionDataType`] into a TypeScript file.
198    pub fn export(self) -> Result<(), TsExportError> {
199        let Self {
200            macro_data,
201            export_path,
202            cfg,
203            header,
204            ..
205        } = self;
206
207        let (function_types, type_map) = macro_data?;
208
209        if let Some(export_dir) = export_path.parent() {
210            fs::create_dir_all(export_dir)?;
211        }
212
213        let mut file = File::create(export_path)?;
214
215        write!(
216            file,
217            "{}{}",
218            header,
219            TLang::render(function_types, type_map, &cfg.unwrap_or_default())?
220        )?;
221
222        Ok(())
223    }
224}