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