Skip to main content

bitwarden_ffi_macro/
wasm_record.rs

1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{Ident, Item, parse2};
4
5use crate::attrs;
6
7/// Declares a serde DTO: derives `Tsify` under the `wasm` feature and opts the type into both ABI
8/// directions, or only JavaScript-to-Rust with `input`.
9pub(crate) fn wasm_record(attr: TokenStream, item: TokenStream) -> TokenStream {
10    let input_only = match parse_input_only(attr) {
11        Ok(input_only) => input_only,
12        Err(err) => return err.to_compile_error(),
13    };
14
15    let item = match parse2::<Item>(item) {
16        Ok(item) => item,
17        Err(err) => return err.to_compile_error(),
18    };
19
20    if let Err(err) = attrs::check_exportable_type(&item, "wasm_record") {
21        return err.to_compile_error();
22    }
23
24    // `#[serde(..)]` and `#[tsify(..)]` stay on the item and its fields for `Tsify`'s derive to
25    // read, so the item is emitted as written.
26    let item = item.to_token_stream();
27    let directions = if input_only {
28        quote!(from_wasm_abi)
29    } else {
30        quote!(into_wasm_abi, from_wasm_abi)
31    };
32    quote! {
33        #[cfg_attr(
34            feature = "wasm",
35            derive(::bitwarden_ffi::_macro::Tsify),
36            tsify(#directions)
37        )]
38        #item
39    }
40}
41
42fn parse_input_only(attr: TokenStream) -> syn::Result<bool> {
43    if attr.is_empty() {
44        return Ok(false);
45    }
46    match parse2::<Ident>(attr.clone()) {
47        Ok(ident) if ident == "input" => Ok(true),
48        _ => Err(syn::Error::new_spanned(
49            attr,
50            "#[wasm_record] takes no arguments other than `input`",
51        )),
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn expand(item: TokenStream) -> String {
60        wasm_record(TokenStream::new(), item)
61            .to_string()
62            .replace(' ', "")
63    }
64
65    #[test]
66    fn derives_tsify_for_both_abi_directions() {
67        let out = expand(quote! {
68            #[derive(Serialize, Deserialize)]
69            pub struct Point { pub x: i32 }
70        });
71
72        assert!(!out.contains("compile_error!"), "{out}");
73        assert!(
74            out.contains("derive(::bitwarden_ffi::_macro::Tsify)"),
75            "{out}"
76        );
77        assert!(out.contains("tsify(into_wasm_abi,from_wasm_abi)"), "{out}");
78        assert!(out.contains("feature=\"wasm\""), "{out}");
79    }
80
81    #[test]
82    fn leaves_tsify_and_serde_attributes_in_place() {
83        // `Tsify`'s derive reads both, so neither may be consumed here.
84        let out = expand(quote! {
85            #[derive(Serialize, Deserialize)]
86            #[serde(rename_all = "camelCase")]
87            pub struct Cipher {
88                #[cfg_attr(feature = "wasm", tsify(optional))]
89                pub name: Option<String>,
90            }
91        });
92
93        assert!(out.contains("#[serde(rename_all=\"camelCase\")]"), "{out}");
94        assert!(
95            out.contains("#[cfg_attr(feature=\"wasm\",tsify(optional))]"),
96            "{out}"
97        );
98    }
99
100    #[test]
101    fn input_declares_only_the_js_to_rust_direction() {
102        let out = wasm_record(quote!(input), quote! { pub struct Credentials; })
103            .to_string()
104            .replace(' ', "");
105
106        assert!(!out.contains("compile_error!"), "{out}");
107        assert!(out.contains("tsify(from_wasm_abi)"), "{out}");
108        assert!(!out.contains("into_wasm_abi"), "{out}");
109    }
110
111    #[test]
112    fn rejects_other_arguments() {
113        let out = wasm_record(quote!(into_wasm_abi), quote! { pub struct Point; })
114            .to_string()
115            .replace(' ', "");
116
117        assert!(out.contains("compile_error!"), "{out}");
118        assert!(out.contains("takesnoargumentsotherthan`input`"), "{out}");
119    }
120
121    #[test]
122    fn rejects_generics() {
123        let out = expand(quote! {
124            pub struct Wrapper<T> { pub inner: T }
125        });
126
127        assert!(out.contains("compile_error!"), "{out}");
128        assert!(out.contains("doesnotsupportgenerics"), "{out}");
129    }
130
131    #[test]
132    fn rejects_an_item_that_is_not_a_struct_or_an_enum() {
133        let out = expand(quote! {
134            pub fn point() {}
135        });
136
137        assert!(out.contains("compile_error!"), "{out}");
138        assert!(out.contains("appliestoastructoranenum"), "{out}");
139    }
140}