Skip to main content

bitwarden_ffi_macro/
wasm_record.rs

1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{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.
9pub(crate) fn wasm_record(attr: TokenStream, item: TokenStream) -> TokenStream {
10    if !attr.is_empty() {
11        return syn::Error::new_spanned(attr, "#[wasm_record] takes no arguments")
12            .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    quote! {
28        #[cfg_attr(
29            feature = "wasm",
30            derive(::bitwarden_ffi::_macro::Tsify),
31            tsify(into_wasm_abi, from_wasm_abi)
32        )]
33        #item
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    fn expand(item: TokenStream) -> String {
42        wasm_record(TokenStream::new(), item)
43            .to_string()
44            .replace(' ', "")
45    }
46
47    #[test]
48    fn derives_tsify_for_both_abi_directions() {
49        let out = expand(quote! {
50            #[derive(Serialize, Deserialize)]
51            pub struct Point { pub x: i32 }
52        });
53
54        assert!(!out.contains("compile_error!"), "{out}");
55        assert!(
56            out.contains("derive(::bitwarden_ffi::_macro::Tsify)"),
57            "{out}"
58        );
59        assert!(out.contains("tsify(into_wasm_abi,from_wasm_abi)"), "{out}");
60        assert!(out.contains("feature=\"wasm\""), "{out}");
61    }
62
63    #[test]
64    fn leaves_tsify_and_serde_attributes_in_place() {
65        // `Tsify`'s derive reads both, so neither may be consumed here.
66        let out = expand(quote! {
67            #[derive(Serialize, Deserialize)]
68            #[serde(rename_all = "camelCase")]
69            pub struct Cipher {
70                #[cfg_attr(feature = "wasm", tsify(optional))]
71                pub name: Option<String>,
72            }
73        });
74
75        assert!(out.contains("#[serde(rename_all=\"camelCase\")]"), "{out}");
76        assert!(
77            out.contains("#[cfg_attr(feature=\"wasm\",tsify(optional))]"),
78            "{out}"
79        );
80    }
81
82    #[test]
83    fn rejects_arguments() {
84        let out = wasm_record(quote!(into_wasm_abi), quote! { pub struct Point; })
85            .to_string()
86            .replace(' ', "");
87
88        assert!(out.contains("compile_error!"), "{out}");
89        assert!(out.contains("takesnoarguments"), "{out}");
90    }
91
92    #[test]
93    fn rejects_generics() {
94        let out = expand(quote! {
95            pub struct Wrapper<T> { pub inner: T }
96        });
97
98        assert!(out.contains("compile_error!"), "{out}");
99        assert!(out.contains("doesnotsupportgenerics"), "{out}");
100    }
101
102    #[test]
103    fn rejects_an_item_that_is_not_a_struct_or_an_enum() {
104        let out = expand(quote! {
105            pub fn point() {}
106        });
107
108        assert!(out.contains("compile_error!"), "{out}");
109        assert!(out.contains("appliestoastructoranenum"), "{out}");
110    }
111}