Skip to main content

tauri_bindgen_ts_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, format_ident};
3use syn::{ItemFn, FnArg, Type, Pat, PatType, Path, Ident};
4
5/// Creates a test that generates a corresponding TypeScript interface for this struct. To generate TypeScript bindings, run ```cargo test```
6/// **Important:** In order for this macro to work, both ts_rs and serde need to be in scope. This can be achieved by importing the prelude: ```use tauri_bindgen_ts::prelude::*```
7///
8/// By default, the location is set to "../src-gen" which results in a top-level directory "src-gen in your Tauri app.
9/// A different output directory can be specified by passing a path as string argument, i.e. ```#[entity("./my-custom-dir)"] struct MyStruct { }```
10#[proc_macro_attribute]
11pub fn entity(attr: TokenStream, item: TokenStream) -> TokenStream {
12    let item: proc_macro2::TokenStream = item.into();
13    let dir = format!("{}/", parse_dir_arg(&attr));
14
15    quote! {
16        #[derive(ts_rs::TS, serde::Serialize, serde::Deserialize)]
17        #[ts(export)]
18        #[ts(export_to=#dir)]
19        #item
20    }.into()
21}
22
23/// Turns this function into a Tauri command and creates a test that generates a TypeScript binding to this function. To generate TypeScript bindings, run ```cargo test```
24/// **Important:** In order for this macro to work, both ts_rs and serde need to be in scope. This can be achieved by importing the prelude: ```use tauri_bindgen_ts::prelude::*```
25///
26/// By default, the location is set to "../src-gen" which results in a top-level directory "src-gen in your Tauri app.
27/// A different output directory can be specified by passing a path as string argument, i.e. ```#[entity("./my-custom-dir)"] struct MyStruct { }```
28#[proc_macro_attribute]
29pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
30    let func = syn::parse::<ItemFn>(item.clone()).expect("This attribute should be used on a function!");
31    let item: proc_macro2::TokenStream = item.into();
32
33    let dir = parse_dir_arg(&attr);
34    let func = func_metadata(func);
35    let test = generate_test(func, dir);
36
37    quote! {
38        #[tauri::command]
39        #item
40        #test
41    }.into()
42}
43
44
45/// Parse the specified export dir from attributes. Defaults to "../src-gen"
46fn parse_dir_arg(attr: &TokenStream) -> String {
47    // TODO: Validate path
48    let dir = attr.to_string();
49    let dir = dir.trim_matches(|c| c == '"' || c == '\'' );
50    if dir.is_empty() { "../src-gen".to_owned() } else { dir.to_owned() }
51}
52
53struct Func {
54    name: String,
55    args: Vec<(Ident, Path)>,
56}
57
58fn func_metadata(func: ItemFn) -> Func {
59    let name = func.sig.ident.to_string();
60    // TODO: Implement mechanism to skip args such as tauris app handle (can be done with attrs)
61    let args = func.sig.inputs.into_iter()
62        .filter_map(|arg| if let FnArg::Typed(t) = arg { Some(t) } else { panic!("Only top-level functions are allowed as commands!") })
63        .collect::<Vec<_>>();
64    // TODO: Support more function arg types
65    let args = types(&args);
66
67    Func { name, args }
68}
69
70fn types(args: &[PatType]) -> Vec<(Ident, Path)> {
71    args.iter()
72        .map(|arg| (arg.pat.clone(), arg.ty.clone()))
73        .filter_map(|(pat, ty)| match *pat {
74            Pat::Ident(p) => Some((p, ty)),
75            _ => panic!("Only simple owned types are allowed as arguments at the moment!"),
76        })
77        .filter_map(|(pat, ty)| match *ty {
78            Type::Path(t) => Some((pat, t)),
79            _ => { panic!("Only simple owned types are allowed as arguments at the moment!") }
80        })
81        .map(|(pat, ty)| (pat.ident, ty.path))
82        .collect()
83}
84
85/// * `func`- An object that holds a functions metadata such as name and arguments
86/// * `dir` - Directory to which the resulting file will be exported
87fn generate_test(func: Func, dir: String) -> proc_macro2::TokenStream {
88    let Func { name, args } = func;
89    let arg_names = args.iter().map(|(ident, _)| ident.to_string()).collect::<Vec<_>>();
90    let arg_types = args.iter().map(|(_, path)| path).collect::<Vec<_>>();
91
92    let test_fn = format_ident!("export_function_bindings_{}", name);
93
94    let header = "// This file was generated by [tauri-bindgen-ts](https://github.com/antoniusnaumann/tauri-bindgen-ts). Do not edit this file manually.";
95    // TODO: Also import argument types
96    let import = "import { invoke } from \"@tauri-apps/api/tauri\"";
97    let binding = format!("export async function {name}(%0) {{ return await invoke('{name}', {{ %1 }}) }}");
98
99    let file_name = format!("{dir}/{name}.ts");
100    let content = format!("{header}\n{import}\n\n{binding}");
101
102    quote! {
103        #[cfg(test)]
104        #[test]
105        fn #test_fn() {
106            use std::fs;
107            use tauri_bindgen_ts::ts_rs::TS;
108
109            let types = vec![#(#arg_types::name()),*];
110            let names = vec![#(#arg_names),*];
111            let args = types.iter().enumerate().map(|(index, elem)| [names[index].to_owned(), elem.to_owned()].join(": ")).collect::<Vec<String>>().join(", ");
112
113            fs::create_dir_all(#dir).expect("Could not create directory");
114            fs::write(#file_name, #content.replace("%0", args.as_str()).replace("%1", names.join(", ").as_str())).expect("Could not write generated function binding to file");
115        }
116    }
117}