bitwarden_ffi_macro/
wasm_record.rs1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::{Ident, Item, parse2};
4
5use crate::attrs;
6
7pub(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 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 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}