tauri-specta 1.0.2

Completely typesafe Tauri commands
Documentation
//! Typesafe Tauri commands
//!
//! ## Install
//!
//! ```bash
//! cargo add specta
//! cargo add tauri-specta --features javascript,typescript
//! ```
//!
//! ## Adding Specta to custom types
//!
//! ```rust
//! use specta::Type;
//! use serde::{Deserialize, Serialize};
//!
//! // The `specta::Type` macro allows us to understand your types
//! // We implement `specta::Type` on primitive types for you.
//! // If you want to use a type from an external crate you may need to enable the feature on Specta.
//! #[derive(Serialize, Type)]
//! pub struct MyCustomReturnType {
//!     pub some_field: String,
//! }
//!
//! #[derive(Deserialize, Type)]
//! pub struct MyCustomArgumentType {
//!     pub foo: String,
//!     pub bar: i32,
//! }
//! ```
//!
//! ## Annotate your Tauri commands with Specta
//!
//! ```rust
//! # //! #[derive(Serialize, Type)]
//! # pub struct MyCustomReturnType {
//! #    pub some_field: String,
//! # }
//! #[tauri::command]
//! #[specta::specta] // <-- This bit here
//! fn greet3() -> MyCustomReturnType {
//!     MyCustomReturnType {
//!         some_field: "Hello World".into(),
//!     }
//! }
//!
//! #[tauri::command]
//! #[specta::specta] // <-- This bit here
//! fn greet(name: String) -> String {
//!   format!("Hello {name}!")
//! }
//! ```
//!
//! ## Export your bindings
//!
//! ```rust
//! # #[specta::specta]
//! # fn greet() {}
//! # #[specta::specta]
//! # fn greet2() {}
//! # #[specta::specta]
//! # fn greet3() {}
//! use specta::collect_types;
//! use tauri_specta::{ts, js};
//!
//! // this example exports your types on startup when in debug mode or in a unit test. You can do whatever.
//! fn main() {
//!     #[cfg(debug_assertions)]
//!     ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();
//!
//!     // or export to JS with JSDoc
//!     #[cfg(debug_assertions)]
//!     js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
//! }
//!
//! #[test]
//! fn export_bindings() {
//!     ts::export(collect_types![greet, greet2, greet3], "../src/bindings.ts").unwrap();
//!     js::export(collect_types![greet, greet2, greet3], "../src/bindings.js").unwrap();
//! }
//! ```
//!
//! ## Usage on frontend
//!
//! ```ts
//! import * as commands from "./bindings"; // This should point to the file we export from Rust
//!
//! await commands.greet("Brendan");
//! ```
//!
#![forbid(unsafe_code)]
#![warn(clippy::all, clippy::unwrap_used, clippy::panic, missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]

use std::{
    borrow::Cow,
    fs::{self, File},
    io::Write,
    marker::PhantomData,
    path::{Path, PathBuf},
};

use specta::{
    functions::FunctionDataType,
    ts::{ExportConfiguration, TsExportError},
    ExportError, TypeDefs,
};

/// The exporter for [Javascript](https://www.javascript.com).
#[cfg(feature = "javascript")]
#[cfg_attr(docsrs, doc(cfg(feature = "javascript")))]
pub mod js;

/// The exporter for [TypeScript](https://www.typescriptlang.org).
#[cfg(feature = "typescript")]
#[cfg_attr(docsrs, doc(cfg(feature = "typescript")))]
pub mod ts;

/// This remains for backwards compatibility. Please use [`specta::collect_types`] instead!
#[macro_export]
#[deprecated(
    note = "Please use `specta::collect_types` instead! This alias will be removed in a future release."
)]
macro_rules! collate_types {
    (type_map: $type_map:ident, $($command:path),*) => {{
        specta::functions::collect_types!(type_map: $type_map, $($command),*)
    }};
    ($($command:path),*) => {{
        let mut type_map = specta::TypeDefs::default();
        specta::functions::collect_types!(type_map: type_map, $($command),*)
    }};
}

pub(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.";

pub(crate) const CRINGE_ESLINT_DISABLE: &str = "/* eslint-disable */
";

// TODO
// #[cfg(doctest)]
// doc_comment::doctest!("../README.md");

/// A set of functions that produce language-specific code
pub trait ExportLanguage {
    /// Type definitions and constants that the generated functions rely on
    fn globals() -> String;

    /// Renders a collection of [`FunctionDataType`] into a string.
    fn render_functions(
        function_types: Vec<FunctionDataType>,
        cfg: &specta::ts::ExportConfiguration,
    ) -> Result<String, TsExportError>;

    /// Renders the output of [`globals`], [`render_functions`] and all dependant types into a TypeScript string.
    fn render(
        function_types: Vec<FunctionDataType>,
        type_map: TypeDefs,
        cfg: &specta::ts::ExportConfiguration,
    ) -> Result<String, TsExportError>;
}

/// General exporter, takes a generic for the specific language that is being exported to.
pub struct Exporter<TLang: ExportLanguage> {
    macro_data: Result<(Vec<FunctionDataType>, TypeDefs), ExportError>,
    export_path: PathBuf,
    cfg: Option<ExportConfiguration>,
    header: Cow<'static, str>,
    lang: PhantomData<TLang>,
}

impl<TLang: ExportLanguage> Exporter<TLang> {
    /// Creates a new TypeScript exporter
    pub fn new(
        macro_data: Result<(Vec<FunctionDataType>, TypeDefs), ExportError>,
        export_path: impl AsRef<Path>,
    ) -> Self {
        Self {
            macro_data,
            export_path: export_path.as_ref().into(),
            cfg: None,
            header: CRINGE_ESLINT_DISABLE.into(), // TODO: Remove this as a default. SemVer moment.
            lang: Default::default(),
        }
    }

    /// Allows for specifying a custom [`ExportConfiguration`](specta::ts::ExportConfiguration).
    pub fn with_cfg(mut self, cfg: ExportConfiguration) -> Self {
        self.cfg = Some(cfg);
        self
    }

    /// Allows for specifying a custom header to
    pub fn with_header(mut self, header: &'static str) -> Self {
        self.header = header.into();
        self
    }

    /// Exports the output of [`internal::render`] for a collection of [`FunctionDataType`] into a TypeScript file.
    pub fn export(self) -> Result<(), TsExportError> {
        let Self {
            macro_data,
            export_path,
            cfg,
            header,
            ..
        } = self;

        let (function_types, type_map) = macro_data?;

        if let Some(export_dir) = export_path.parent() {
            fs::create_dir_all(export_dir)?;
        }

        let mut file = File::create(export_path)?;

        write!(
            file,
            "{}{}",
            header,
            TLang::render(function_types, type_map, &cfg.unwrap_or_default())?
        )?;

        Ok(())
    }
}