Skip to main content

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            fn try_from_js_value_ref(value: &$crate::prelude::JsValue) -> Option<Self> {
171                Self::try_from_js_value(value.clone()).ok()
172            }
173        }
174    };
175}
176
177/// Implement all wasm traits for the given type.
178/// that is [impl_main_wasm_traits] and [impl_complementary_wasm_traits].
179/// The type needs to have [serde::Serialize], [serde::Deserialize]
180/// and [tsify::Tsify] traits implemented.
181///
182/// Example:
183/// ```ignore
184/// #[derive(Serialize, Deserialize, Tsify)]
185/// #[serde(rename_all = "camelCase")]
186/// pub struct A {
187///     pub field: String,
188///     pub other_field: u8,
189/// }
190/// impl_wasm_traits!(A);
191///
192/// #[wasm_bindgen]
193/// pub fn some_fn(arg: Vec<A>) -> String {
194///     // body
195/// }
196/// ```
197#[macro_export]
198macro_rules! impl_wasm_traits {
199    ($type_name:ident $(< $($generics:ident),+ >)?) => {
200        $crate::impl_main_wasm_traits!($type_name$(<$($generics),+>)?);
201        $crate::impl_complementary_wasm_traits!($type_name$(<$($generics),+>)?);
202    };
203}
204
205/// Implements [tsify::Tsify] with the given type declaration for the given rust
206/// type (structs and enums) identifier.
207///
208/// This is the same as what [tsify::Tsify] derive macro does internally for a
209/// given type but with full customization capability, as both are a sugar
210/// for [wasm_bindgen] `typescript_custom_section` attr plus `extern C` block
211/// defining a wrapped [wasm_bindgen::JsValue] for the given type.
212/// Therefore, this macro (unlike tsify derive macro) puts representative
213/// [wasm_bindgen::JsValue] of the given type on the current scope identified
214/// by prepending "Js" to the orginial type identifier, meaning it would be
215/// accessible by for example:
216/// `JsSomeType` when original type is `SomeType`.
217///
218/// This is very usefull for cases where a rust type is not defined in current
219/// module (like autogen types) and [tsify::Tsify] trait cannot be implemented
220/// for as a result, so this will implement `Tsify` trait for the given type and
221/// also allows to manually serialize/deserialize the [wasm_bindgen::JsValue]
222/// to/from js side from/to the rust type, for example with custom serializers
223/// and deserializers.
224///
225/// Example:
226/// ```ignore
227/// #[derive(Serialize, Deserialize)]
228/// #[serde(rename_all = "camelCase")]
229/// pub struct SomeType {
230///     pub field: String,
231///     pub other_field: u8,
232/// }
233/// impl_custom_tsify!(
234///     SomeType,
235///     // this will become the typescript
236///     // interface bindings for SomeType
237///     "export interface SomeType {
238///         field: string;
239///         otherField: number;
240///     };"
241/// );
242///
243/// #[wasm_bindgen]
244/// pub fn some_fn(arg: JsSomeType) -> JsSomeType {
245///     // deserialize the arg which is a wrapped `JsValue`
246///     // into rust `SomeType` using serde_wasm_bindgen
247///     let val = serde_wasm_bindgen::from_value::<SomeType>(arg.obj).unwrap_throw();
248///
249///     // body
250///
251///     // serialize to JsValue optionally with serializer available
252///     // options and wrap it in JsSomeType for return
253///     let ser = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
254///     JsSomeType { obj: val.serialize(ser).unwrap_throw() }
255/// }
256/// ```
257#[macro_export]
258macro_rules! impl_custom_tsify {
259    ($type_name:ident $(< $($generics:ident),+ >)?, $decl:literal) => {
260        $crate::prelude::paste::paste! {
261            #[$crate::prelude::wasm_bindgen]
262            extern "C" {
263                #[wasm_bindgen(typescript_type = [<$type_name>])]
264                pub type [<Js $type_name>];
265            }
266
267            #[$crate::prelude::wasm_bindgen(typescript_custom_section)]
268            const TYPESCRIPT_CONTENT: &'static str = $decl;
269
270            impl$(<$($generics),+>)? $crate::prelude::Tsify for $type_name$(<$($generics),+>)? {
271                type JsType = [<Js $type_name>];
272                const DECL: &'static str = $decl;
273            }
274        }
275    };
276}
277
278/// Adds/appends the given string literal to wasm bindgen typescript bindings.
279/// This is just a sugar for [wasm_bindgen] `typescript_custom_section`, so
280/// the given text can be anything, from typescript comment to type declarations
281/// or any other valid .d.ts content.
282///
283/// Example:
284/// ```ignore
285/// // add some custom type to .d.ts bindings output
286/// add_ts_content!("export type SomeType = { field: string; otherField: number };");
287///
288/// // add some comment to .d.ts bindings output
289/// add_ts_content!("// this is some comment");
290/// ```
291#[macro_export]
292macro_rules! add_ts_content {
293    ($decl:literal) => {
294        $crate::prelude::paste::paste! {
295            #[$crate::prelude::wasm_bindgen(typescript_custom_section)]
296            const TYPESCRIPT_CONTENT: &'static str = $decl;
297        }
298    };
299}
300
301#[cfg(target_family = "wasm")]
302#[cfg(test)]
303mod tests {
304    use crate::*;
305    use wasm_bindgen::JsCast;
306    use js_sys::{JsString, Reflect};
307    use wasm_bindgen_test::wasm_bindgen_test;
308    use std::{collections::HashMap, str::FromStr};
309
310    #[derive(serde::Deserialize, serde::Serialize, Default)]
311    pub struct A {
312        pub field1: String,
313        #[serde(serialize_with = "serialize_as_bytes")]
314        pub field2: Vec<u8>,
315        #[serde(serialize_with = "serialize_hashmap_as_object")]
316        pub field3: HashMap<String, u64>,
317    }
318
319    // ensures macros validity at compile time
320    // impl tsify manualy for "A" that needs it
321    // before being able to impl all wasm traits
322    impl_custom_tsify!(
323        A,
324        "export interface A {
325            field1: String;
326            field2: Uint8Array;
327            field3: Record<string, bigint>;
328        };"
329    );
330    impl_wasm_traits!(A);
331    add_ts_content!("export type SomeType = string;");
332
333    #[wasm_bindgen_test]
334    fn test_macros() {
335        let res = A::default().try_into_js_value().unwrap();
336        let field1_key = JsString::from_str("field1").unwrap();
337        let field2_key = JsString::from_str("field2").unwrap();
338        let field3_key = JsString::from_str("field3").unwrap();
339
340        // should exist
341        assert!(field1_key.js_in(&res));
342        assert_eq!(
343            Reflect::get(&res, &field1_key)
344                .unwrap()
345                .as_string()
346                .unwrap(),
347            ""
348        );
349        assert!(field2_key.js_in(&res));
350        assert!(Reflect::get(&res, &field2_key)
351            .unwrap()
352            .is_instance_of::<js_sys::Uint8Array>());
353        assert!(field3_key.js_in(&res));
354        assert!(Reflect::get(&res, &field3_key).unwrap().is_object());
355
356        // should not exist
357        assert!(!JsString::from_str("field4").unwrap().js_in(&res));
358    }
359
360    #[derive(serde::Deserialize, serde::Serialize, Default)]
361    pub struct B<T, E> {
362        pub field1: T,
363        pub field2: E,
364    }
365    impl_wasm_traits!(B<T, E>);
366    impl_custom_tsify!(
367        B<T, E>,
368        "export interface B<T, E> {
369            field1: T;
370            field2: E;
371        };"
372    );
373
374    #[wasm_bindgen_test]
375    fn test_macros_generic() {
376        let res = B::<String, u8>::default().try_into_js_value().unwrap();
377        let field1_key = JsString::from_str("field1").unwrap();
378        let field2_key = JsString::from_str("field2").unwrap();
379
380        // should exist
381        assert!(field1_key.js_in(&res));
382        assert_eq!(
383            Reflect::get(&res, &field1_key)
384                .unwrap()
385                .as_string()
386                .unwrap(),
387            ""
388        );
389        assert!(field2_key.js_in(&res));
390        assert_eq!(
391            Reflect::get(&res, &field2_key).unwrap().as_f64().unwrap(),
392            0.0
393        );
394
395        // should not exist
396        assert!(!JsString::from_str("field3").unwrap().js_in(&res));
397    }
398}