1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
use proc_macro::TokenStream;
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
use quote::quote;
use syn::{
    parse::{Parse, ParseStream},
    parse_macro_input,
    token::{Pub, Semi},
    FnArg, Pat, PatIdent, PatType, Signature, Visibility,
};

mod kw {
    use syn::custom_keyword;

    custom_keyword!(fallible);
    custom_keyword!(infallible);
}

#[proc_macro_attribute]
#[proc_macro_error]
pub fn client_command(attr: TokenStream, item: TokenStream) -> TokenStream {
    let Fallible(fallible) = parse_macro_input!(attr);
    let ClientCommand {
        visibility,
        signature,
    } = parse_macro_input!(item);

    if let Some(constness) = signature.constness {
        abort!(constness, "Function can't be const");
    }

    if signature.asyncness.is_none() {
        abort_call_site!("Function must be async");
    }

    if let Some(variadic) = signature.variadic {
        abort!(variadic, "Function can't be variadic");
    }

    let fn_name = signature.ident.to_string();
    let arg_names = signature.inputs.iter().map(|arg| match arg {
        FnArg::Receiver(self_arg) => abort!(self_arg, "self arguments are not allowed"),
        FnArg::Typed(PatType { pat, .. }) => match pat.as_ref() {
            Pat::Ident(PatIdent { ident, .. }) => ident,
            _ => abort!(pat, "Arguments must be named"),
        },
    });

    let result_handler = if fallible {
        quote!(result
            .map(|ok| serde_wasm_bindgen::from_value(ok).unwrap())
            .map_err(|e| serde_wasm_bindgen::from_value(e).unwrap()))
    } else {
        quote!(serde_wasm_bindgen::from_value(result.unwrap()).unwrap())
    };

    quote!(
        #visibility #signature {
            use ::std::stringify;

            use ::silkenweb_tauri::{
                js_sys::{self, Object, Reflect},
                wasm_bindgen::{self, prelude::wasm_bindgen, JsValue},
                wasm_bindgen_futures,
                serde_wasm_bindgen,
            };

            #[wasm_bindgen(inline_js = r#"
                export async function invoke(name, args) {
                    return await window.__TAURI__.invoke(name, args);
                }
            "#)]

            extern "C" {
                #[wasm_bindgen(catch)]
                async fn invoke(name: String, args: JsValue) -> Result<JsValue, JsValue>;
            }

            let args = Object::new();

            #(Reflect::set(&args, &stringify!(#arg_names).into(), &serde_wasm_bindgen::to_value(&#arg_names).unwrap()).unwrap();)*

            let result = invoke(#fn_name.to_string(), args.into())
                .await;

            #result_handler
        }
    )
    .into()
}

struct Fallible(bool);

impl Parse for Fallible {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();

        let fallible = if lookahead.peek(kw::fallible) {
            input.parse::<kw::fallible>()?;
            true
        } else if lookahead.peek(kw::infallible) {
            input.parse::<kw::infallible>()?;
            false
        } else {
            return Err(lookahead.error());
        };

        Ok(Self(fallible))
    }
}

struct ClientCommand {
    visibility: Option<Visibility>,
    signature: Signature,
}

impl Parse for ClientCommand {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let visibility = if input.peek(Pub) {
            Some(input.parse()?)
        } else {
            None
        };

        let signature = input.parse()?;
        input.parse::<Semi>()?;

        Ok(Self {
            visibility,
            signature,
        })
    }
}