wasm_bindgen_utils/
macros.rs

1/// A macro that implements main wasm traits for the given type.
2/// These traits are the necessary ones to be able to send/receive
3/// the given type through wasm bindgen boundry.
4/// The type needs to have [serde::Serialize], [serde::Deserialize]
5/// and [tsify::Tsify] traits implemented.
6///
7/// Example:
8/// ```ignore
9/// #[derive(Serialize, Deserialize, Tsify)]
10/// #[serde(rename_all = "camelCase")]
11/// pub struct A {
12///     pub field: String,
13///     pub other_field: u8,
14/// }
15/// impl_main_wasm_traits!(A);
16///
17/// #[wasm_bindgen]
18/// pub fn some_fn(arg: A) -> String {
19///     // body
20/// }
21///
22/// #[wasm_bindgen]
23/// pub fn some_other_fn(arg: String) -> Option<A> {
24///     // body
25/// }
26/// ```
27#[macro_export]
28macro_rules! impl_main_wasm_traits {
29    ($type_name:ident $(< $($generics:ident),+ >)?) => {
30        impl$(<$($generics),+>)? $type_name$(<$($generics),+>)?
31        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
32            const TYPE_NAME: &'static str = stringify!($type_name);
33            /// A simple helpful wrapper for serde_wasm_bindgen::to_value
34            /// as self method for easy accessible conversion
35            pub fn try_into_js_value(&self) -> Result<$crate::prelude::JsValue, $crate::prelude::serde_wasm_bindgen::Error> {
36                $crate::prelude::to_js_value(&self)
37            }
38            /// A simple helpful wrapper for serde_wasm_bindgen::from_value
39            /// as Self method for easy accessible conversion
40            pub fn try_from_js_value(js: $crate::prelude::JsValue) -> Result<Self, $crate::prelude::serde_wasm_bindgen::Error> {
41                $crate::prelude::from_js_value(js)
42            }
43        }
44        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::describe::WasmDescribe for $type_name$(<$($generics),+>)? {
45            #[inline]
46            fn describe() {
47                <Self as $crate::prelude::Tsify>::JsType::describe()
48            }
49        }
50        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::IntoWasmAbi for $type_name$(<$($generics),+>)?
51        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
52            type Abi = <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::IntoWasmAbi>::Abi;
53
54            #[inline]
55            fn into_abi(self) -> Self::Abi {
56                let mut err = String::new();
57                err.push_str(Self::TYPE_NAME);
58                err.push_str(": ");
59                let result = self.try_into_js_value().map(<<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::JsCast>::unchecked_from_js);
60                $crate::prelude::UnwrapThrowExt::expect_throw(result.inspect_err(|e| err.push_str(&e.to_string())), &err).into_abi()
61            }
62        }
63        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::OptionIntoWasmAbi for $type_name$(<$($generics),+>)?
64        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
65            #[inline]
66            fn none() -> Self::Abi {
67                <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::OptionIntoWasmAbi>::none()
68            }
69        }
70        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::FromWasmAbi for $type_name$(<$($generics),+>)?
71        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
72            type Abi = <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::FromWasmAbi>::Abi;
73
74            #[inline]
75            unsafe fn from_abi(js: Self::Abi) -> Self {
76                let mut err = String::new();
77                err.push_str(Self::TYPE_NAME);
78                err.push_str(": ");
79                let result = Self::try_from_js_value(<Self as $crate::prelude::Tsify>::JsType::from_abi(js).into());
80                $crate::prelude::UnwrapThrowExt::expect_throw(result.inspect_err(|e| err.push_str(&e.to_string())), &err)
81            }
82        }
83        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::OptionFromWasmAbi for $type_name$(<$($generics),+>)?
84        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
85            #[inline]
86            fn is_none(js: &Self::Abi) -> bool {
87                <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::OptionFromWasmAbi>::is_none(js)
88            }
89        }
90    };
91}
92
93/// Implements complementary wasm traits for the given type.
94/// Needs [impl_main_wasm_traits] to be implemented first.
95/// It allows a type to be used on async functions normally or
96/// as ref or as Vec<> etc.
97/// The type needs to have [serde::Serialize], [serde::Deserialize]
98/// and [tsify::Tsify] traits implemented.
99///
100/// Example:
101/// ```ignore
102/// #[derive(Serialize, Deserialize, Tsify)]
103/// #[serde(rename_all = "camelCase")]
104/// pub struct A {
105///     pub field: String,
106///     pub other_field: u8,
107/// }
108/// impl_main_wasm_traits!(A);
109/// impl_complementary_wasm_traits!(A);
110///
111/// #[wasm_bindgen]
112/// pub async fn some_fn(arg: &A) -> Result<String, Error> {
113///     // body
114/// }
115/// ```
116#[macro_export]
117macro_rules! impl_complementary_wasm_traits {
118    ($type_name:ident $(< $($generics:ident),+ >)?) => {
119        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::RefFromWasmAbi for $type_name$(<$($generics),+>)?
120        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
121            type Abi = <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::RefFromWasmAbi>::Abi;
122            type Anchor = Box<Self>;
123            unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor {
124                Box::new(<Self as $crate::prelude::wasm_bindgen::convert::FromWasmAbi>::from_abi(js))
125            }
126        }
127        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::LongRefFromWasmAbi for $type_name$(<$($generics),+>)?
128        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
129            type Abi = <<Self as $crate::prelude::Tsify>::JsType as $crate::prelude::wasm_bindgen::convert::LongRefFromWasmAbi>::Abi;
130            type Anchor = Box<Self>;
131            unsafe fn long_ref_from_abi(js: Self::Abi) -> Self::Anchor {
132                Box::new(<Self as $crate::prelude::wasm_bindgen::convert::FromWasmAbi>::from_abi(js))
133            }
134        }
135        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::VectorIntoWasmAbi for $type_name$(<$($generics),+>)?
136        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
137            type Abi = <Box<[<Self as $crate::prelude::Tsify>::JsType]> as $crate::prelude::wasm_bindgen::convert::IntoWasmAbi>::Abi;
138            fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi {
139                $crate::prelude::wasm_bindgen::convert::js_value_vector_into_abi(vector)
140            }
141        }
142        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::VectorFromWasmAbi for $type_name$(<$($generics),+>)?
143        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
144            type Abi = <Box<[<Self as $crate::prelude::Tsify>::JsType]> as $crate::prelude::wasm_bindgen::convert::FromWasmAbi>::Abi;
145            unsafe fn vector_from_abi(js: Self::Abi) -> Box<[Self]> {
146                $crate::prelude::wasm_bindgen::convert::js_value_vector_from_abi(js)
147            }
148        }
149        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::describe::WasmDescribeVector for $type_name$(<$($generics),+>)? {
150            fn describe_vector() {
151                $crate::prelude::wasm_bindgen::describe::inform($crate::prelude::wasm_bindgen::describe::VECTOR);
152                <Self as $crate::prelude::wasm_bindgen::describe::WasmDescribe>::describe();
153            }
154        }
155        impl$(<$($generics),+>)? From<$type_name$(<$($generics),+>)?> for $crate::prelude::JsValue
156        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
157            fn from(value: $type_name$(<$($generics),+>)?) -> Self {
158                let mut err = String::new();
159                err.push_str(<$type_name$(<$($generics),+>)?>::TYPE_NAME);
160                err.push_str(": ");
161                let result = value.try_into_js_value();
162                $crate::prelude::UnwrapThrowExt::expect_throw(
163                    result.inspect_err(|e| err.push_str(&e.to_string())),
164                    &err,
165                )
166            }
167        }
168        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::convert::TryFromJsValue for $type_name$(<$($generics),+>)?
169        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
170            type Error = $crate::prelude::serde_wasm_bindgen::Error;
171            fn try_from_js_value(value: $crate::prelude::JsValue) -> Result<Self, Self::Error> {
172                Self::try_from_js_value(value)
173            }
174        }
175        impl$(<$($generics),+>)? $crate::prelude::wasm_bindgen::__rt::VectorIntoJsValue for $type_name$(<$($generics),+>)?
176        $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? {
177            fn vector_into_jsvalue(vector: Box<[Self]>) -> $crate::prelude::JsValue {
178                $crate::prelude::wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector)
179            }
180        }
181    };
182}
183
184/// Implement all wasm traits for the given type.
185/// that is [impl_main_wasm_traits] and [impl_complementary_wasm_traits].
186/// The type needs to have [serde::Serialize], [serde::Deserialize]
187/// and [tsify::Tsify] traits implemented.
188///
189/// Example:
190/// ```ignore
191/// #[derive(Serialize, Deserialize, Tsify)]
192/// #[serde(rename_all = "camelCase")]
193/// pub struct A {
194///     pub field: String,
195///     pub other_field: u8,
196/// }
197/// impl_wasm_traits!(A);
198///
199/// #[wasm_bindgen]
200/// pub fn some_fn(arg: Vec<A>) -> String {
201///     // body
202/// }
203/// ```
204#[macro_export]
205macro_rules! impl_wasm_traits {
206    ($type_name:ident $(< $($generics:ident),+ >)?) => {
207        $crate::impl_main_wasm_traits!($type_name$(<$($generics),+>)?);
208        $crate::impl_complementary_wasm_traits!($type_name$(<$($generics),+>)?);
209    };
210}
211
212/// Implements [tsify::Tsify] with the given type declaration for the given rust
213/// type (structs and enums) identifier.
214///
215/// This is the same as what [tsify::Tsify] derive macro does internally for a
216/// given type but with full customization capability, as both are a sugar
217/// for [wasm_bindgen] `typescript_custom_section` attr plus `extern C` block
218/// defining a wrapped [wasm_bindgen::JsValue] for the given type.
219/// Therefore, this macro (unlike tsify derive macro) puts representative
220/// [wasm_bindgen::JsValue] of the given type on the current scope identified
221/// by prepending "Js" to the orginial type identifier, meaning it would be
222/// accessible by for example:
223/// `JsSomeType` when original type is `SomeType`.
224///
225/// This is very usefull for cases where a rust type is not defined in current
226/// module (like autogen types) and [tsify::Tsify] trait cannot be implemented
227/// for as a result, so this will implement `Tsify` trait for the given type and
228/// also allows to manually serialize/deserialize the [wasm_bindgen::JsValue]
229/// to/from js side from/to the rust type, for example with custom serializers
230/// and deserializers.
231///
232/// Example:
233/// ```ignore
234/// #[derive(Serialize, Deserialize)]
235/// #[serde(rename_all = "camelCase")]
236/// pub struct SomeType {
237///     pub field: String,
238///     pub other_field: u8,
239/// }
240/// impl_custom_tsify!(
241///     SomeType,
242///     // this will become the typescript
243///     // interface bindings for SomeType
244///     "export interface SomeType {
245///         field: string;
246///         otherField: number;
247///     };"
248/// );
249///
250/// #[wasm_bindgen]
251/// pub fn some_fn(arg: JsSomeType) -> JsSomeType {
252///     // deserialize the arg which is a wrapped `JsValue`
253///     // into rust `SomeType` using serde_wasm_bindgen
254///     let val = serde_wasm_bindgen::from_value::<SomeType>(arg.obj).unwrap_throw();
255///
256///     // body
257///
258///     // serialize to JsValue optionally with serializer available
259///     // options and wrap it in JsSomeType for return
260///     let ser = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
261///     JsSomeType { obj: val.serialize(ser).unwrap_throw() }
262/// }
263/// ```
264#[macro_export]
265macro_rules! impl_custom_tsify {
266    ($type_name:ident $(< $($generics:ident),+ >)?, $decl:literal) => {
267        $crate::prelude::paste::paste! {
268            #[$crate::prelude::wasm_bindgen]
269            extern "C" {
270                #[wasm_bindgen(typescript_type = [<$type_name>])]
271                pub type [<Js $type_name>];
272            }
273
274            #[$crate::prelude::wasm_bindgen(typescript_custom_section)]
275            const TYPESCRIPT_CONTENT: &'static str = $decl;
276
277            impl$(<$($generics),+>)? $crate::prelude::Tsify for $type_name$(<$($generics),+>)? {
278                type JsType = [<Js $type_name>];
279                const DECL: &'static str = $decl;
280            }
281        }
282    };
283}
284
285/// Adds/appends the given string literal to wasm bindgen typescript bindings.
286/// This is just a sugar for [wasm_bindgen] `typescript_custom_section`, so
287/// the given text can be anything, from typescript comment to type declarations
288/// or any other valid .d.ts content.
289///
290/// Example:
291/// ```ignore
292/// // add some custom type to .d.ts bindings output
293/// add_ts_content!("export type SomeType = { field: string; otherField: number };");
294///
295/// // add some comment to .d.ts bindings output
296/// add_ts_content!("// this is some comment");
297/// ```
298#[macro_export]
299macro_rules! add_ts_content {
300    ($decl:literal) => {
301        $crate::prelude::paste::paste! {
302            #[$crate::prelude::wasm_bindgen(typescript_custom_section)]
303            const TYPESCRIPT_CONTENT: &'static str = $decl;
304        }
305    };
306}
307
308#[cfg(target_family = "wasm")]
309#[cfg(test)]
310mod tests {
311    use crate::*;
312    use wasm_bindgen::JsCast;
313    use js_sys::{JsString, Reflect};
314    use wasm_bindgen_test::wasm_bindgen_test;
315    use std::{collections::HashMap, str::FromStr};
316
317    #[derive(serde::Deserialize, serde::Serialize, Default)]
318    pub struct A {
319        pub field1: String,
320        #[serde(serialize_with = "serialize_as_bytes")]
321        pub field2: Vec<u8>,
322        #[serde(serialize_with = "serialize_hashmap_as_object")]
323        pub field3: HashMap<String, u64>,
324    }
325
326    // ensures macros validity at compile time
327    // impl tsify manualy for "A" that needs it
328    // before being able to impl all wasm traits
329    impl_custom_tsify!(
330        A,
331        "export interface A {
332            field1: String;
333            field2: Uint8Array;
334            field3: Record<string, bigint>;
335        };"
336    );
337    impl_wasm_traits!(A);
338    add_ts_content!("export type SomeType = string;");
339
340    #[wasm_bindgen_test]
341    fn test_macros() {
342        let res = A::default().try_into_js_value().unwrap();
343        let field1_key = JsString::from_str("field1").unwrap();
344        let field2_key = JsString::from_str("field2").unwrap();
345        let field3_key = JsString::from_str("field3").unwrap();
346
347        // should exist
348        assert!(field1_key.js_in(&res));
349        assert_eq!(
350            Reflect::get(&res, &field1_key)
351                .unwrap()
352                .as_string()
353                .unwrap(),
354            ""
355        );
356        assert!(field2_key.js_in(&res));
357        assert!(Reflect::get(&res, &field2_key)
358            .unwrap()
359            .is_instance_of::<js_sys::Uint8Array>());
360        assert!(field3_key.js_in(&res));
361        assert!(Reflect::get(&res, &field3_key).unwrap().is_object());
362
363        // should not exist
364        assert!(!JsString::from_str("field4").unwrap().js_in(&res));
365    }
366
367    #[derive(serde::Deserialize, serde::Serialize, Default)]
368    pub struct B<T, E> {
369        pub field1: T,
370        pub field2: E,
371    }
372    impl_wasm_traits!(B<T, E>);
373    impl_custom_tsify!(
374        B<T, E>,
375        "export interface B<T, E> {
376            field1: T;
377            field2: E;
378        };"
379    );
380
381    #[wasm_bindgen_test]
382    fn test_macros_generic() {
383        let res = B::<String, u8>::default().try_into_js_value().unwrap();
384        let field1_key = JsString::from_str("field1").unwrap();
385        let field2_key = JsString::from_str("field2").unwrap();
386
387        // should exist
388        assert!(field1_key.js_in(&res));
389        assert_eq!(
390            Reflect::get(&res, &field1_key)
391                .unwrap()
392                .as_string()
393                .unwrap(),
394            ""
395        );
396        assert!(field2_key.js_in(&res));
397        assert_eq!(
398            Reflect::get(&res, &field2_key).unwrap().as_f64().unwrap(),
399            0.0
400        );
401
402        // should not exist
403        assert!(!JsString::from_str("field3").unwrap().js_in(&res));
404    }
405}