wasm_bindgen_struct/
lib.rs

1//! A crate to make working with [`wasm-bindgen`](https://crates.io/crates/wasm-bindgen)
2//! easier.
3//!
4//! This crate contains the `wasm_bindgen_struct` macro which
5//! can be used to declare `wasm_bindgen` types, and implement
6//! getters/setter as if they were normal rust structs. It can
7//! also be used to implement methods for the type, including inline
8//! mapping to overcome `wasm-bindgen-futures` limitations.
9//!
10//! # Example
11//!
12//! ```rust
13//! // Or you can use this instead to avoid needing to import
14//! // the macro throughout your crate
15//! // #[macro_use]
16//! // extern crate wasm_bindgen_struct;
17//! use wasm_bindgen_struct::wasm_bindgen_struct;
18//!
19//! # use wasm_bindgen::prelude::*;
20//!
21//! #[wasm_bindgen_struct]
22//! // `module` here means this type comes from the JS "my-module" module
23//! // with the class name `aAnotherType`
24//! #[opts(module = "my-module", js_name = "AnotherType")]
25//! struct MyJsType {
26//!   // field names are automatically converted to `camelCase`
27//!   field_a: String,
28//!   // ...but can be manually specified if needed
29//!   #[opts(js_name = "field_b")]
30//!   field_b: String,
31//! }
32//!
33//! #[wasm_bindgen_struct]
34//! #[opts(module = "my-module")]
35//! impl MyJsType {
36//!   // Automatically gets the `static_method_of = MyJsType` applied,
37//!   // as well as `catch` due to the `Result`
38//!   fn apple() -> Result<String, JsValue>;
39//!
40//!   // `async` functions in `wasm-bindgen` can only currently return
41//!   // `()`, `JsValue`, or Result<_, JsValue>`, where `_` is one of
42//!   // the two previously mentioned types. We use `MapValue<T, U>`
43//!   // to tell the macro that the binding method should return `T`,
44//!   // but we'll map the value to `U`
45//!   async fn oranges(&self) -> MapValue<JsValue, String> {
46//!     self
47//!       .oranges_js()
48//!       .await
49//!       .unchecked_into::<js_sys::JsString>()
50//!       .into()
51//!   }
52//! }
53//! ```
54//!
55//! The above expands to:
56//!
57//! ```rust
58//! # use wasm_bindgen::prelude::*;
59//!
60//! // For getters/setters
61//! #[wasm_bindgen(module = "my-module")]
62//! extern "C" {
63//!   #[wasm_bindgen(js_name = "AnotherType")]
64//!   type MyJsType;
65//!
66//!   #[wasm_bindgen(
67//!     method,
68//!     getter,
69//!     js_class = "AnotherType",
70//!     js_name = "fieldA"
71//!   )]
72//!   fn field_a(this: &MyJsType) -> String;
73//!
74//!   #[wasm_bindgen(
75//!     method,
76//!     setter,
77//!     js_class = "AnotherType",
78//!     js_name = "fieldA"
79//!   )]
80//!   fn set_field_a(this: &MyJsType, value: String);
81//!
82//!   #[wasm_bindgen(
83//!     method,
84//!     getter,
85//!     js_class = "AnotherType",
86//!     js_name = "field_b"
87//!   )]
88//!   fn field_b(this: &MyJsType) -> String;
89//!   #[wasm_bindgen(
90//!     method,
91//!     setter,
92//!     js_class = "AnotherType",
93//!     js_name = "field_b"
94//!   )]
95//!   fn set_field_b(this: &MyJsType, value: String);
96//! }
97//!
98//! // And for methods
99//! impl MyJsType {
100//!   fn apple() -> Result<String, JsValue> {
101//!     #[wasm_bindgen(module = "my-module")]
102//!     extern "C" {
103//!       #[wasm_bindgen(static_method_of = MyJsType, js_name = "apple")]
104//!       #[wasm_bindgen(catch)]
105//!       fn apple_js() -> Result<String, JsValue>;
106//!     }
107//!     Self::apple_js()
108//!   }
109//!   async fn oranges(&self) -> String {
110//!     #[wasm_bindgen(module = "my-module")]
111//!     extern "C" {
112//!       #[wasm_bindgen(method, js_name = "oranges")]
113//!       async fn oranges_js(this: &MyJsType) -> JsValue;
114//!     }
115//!     self
116//!       .oranges_js()
117//!       .await
118//!       .unchecked_into::<js_sys::JsString>()
119//!       .into()
120//!   }
121//! }
122//! ```
123//!
124//! # `#[opts(...)]` on structs
125//! - dbg: `bool`
126//!   
127//!   Show the formatted output of the macro as a warning
128//! - on: `Type`
129//!
130//!   Allows using an existing type with the struct syntax.
131//!
132//!   ## Example
133//!   ```rust
134//!   # use wasm_bindgen_struct::wasm_bindgen_struct;
135//!   # use wasm_bindgen::prelude::*;
136//!   
137//!   #[wasm_bindgen]
138//!   extern "C" {
139//!     type JsType;
140//!   }
141//!
142//!   #[wasm_bindgen_struct]
143//!   #[opts(on = JsType)]
144//!   // Struct can be named anything, it doesn't matter
145//!   struct JsType {
146//!     field_1: bool,
147//!   }
148//!   ```
149//! - [extends: `Type`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/extends.html)
150//! - [getter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
151//! - [setter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
152//! - [final_: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/final.html)
153//! - [js_name: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_name.html)
154//! - [js_namespacejsnamespace: Vec: `[Lit1, Lit2, Lit3, ...]`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_namespace.html)
155//! - [module: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/module.html)
156//! - [raw_module: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/raw_module.html)
157//! - _`<unknown>`_
158//!
159//!   Unknown attributes, such as doc comments, are forwarded to
160//!   the `type MyType` declaration, or ignored in the case of using
161//!   `on`.
162//!
163//! # `#[opts(...)]` on struct fields
164//!
165//! - [getter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
166//! - [setter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
167//! - [structural: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/structural.html)
168//! - [final_: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/final.html)
169//! - [js_name: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_name.html)
170//!
171//! # `#[opts(...)]` on `impl`
172//!
173//! - dbg: bool
174//!
175//!   Show the formatted output of the macro as a warning
176//! - [final_: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/final.html)
177//! - [js_name: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_name.html)
178//! - [js_namespace: `[Lit1, Lit2, Lit3, ...]`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_namespace.html)
179//! - [module: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/module.html)
180//! - [raw_module: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/raw_module.html)
181//!
182//! # `#[opts(...)]` on `impl` methods
183//!
184//! - [constructor: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/constructor.html)
185//! - [final_: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/final.html)
186//! - [structural: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/structural.html)
187//! - [getter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
188//! - [setter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/getter-and-setter.html)
189//! - [indexing_getter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/indexing-getter-setter-deleter.html)
190//! - [indexing_setter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/indexing-getter-setter-deleter.html)
191//! - [indexing_deleter: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/indexing-getter-setter-deleter.html)
192//! - [js_name: `Lit`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/js_name.html)
193//! - [variadic: `bool`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/variadic.html)
194//!
195//! # Treatment of Special Types
196//! - `Result<T, E>`
197//!
198//!   Implies [`catch`](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/catch.html).
199//! - `MapValue<T, U>`
200//!
201//!   Allows specifing the types of the generated `wasm-bindgen`
202//!   binding, as well as the actual method type. This allows
203//!   arbitrary transformations between the bound method and the
204//!   output method.
205//!
206//!   ## Example
207//!
208//!   ```rust
209//!   # use wasm_bindgen::prelude::*;
210//!   # use wasm_bindgen_struct::wasm_bindgen_struct;
211//!   
212//!   #[wasm_bindgen_struct]
213//!   struct JsType;
214//!
215//!   #[wasm_bindgen_struct]
216//!   impl JsType {
217//!     async fn get_async_string(&self) -> MapValue<JsValue, String> {
218//!       self
219//!         .get_async_string_js()
220//!         .await
221//!         .unchecked_into::<js_sys::JsString>()
222//!         .into()
223//!     }
224//!   }
225//!   ```
226//!
227//!   ### Important note
228//!   `MapValue` is a pseudo-type which only exists within the macro's
229//!   scope. i.e., this type does not exist outside of
230//!   `#[wasm_bindgen_struct] impl { /* ... */ }` and therefore does not
231//!   need to be imported.
232
233#[macro_use]
234extern crate proc_macro_error;
235
236#[macro_use]
237mod utils;
238mod exts;
239mod model;
240
241use crate::model::Model;
242use proc_macro_error::proc_macro_error;
243use quote::ToTokens;
244
245#[proc_macro_attribute]
246#[proc_macro_error]
247pub fn wasm_bindgen_struct(
248  _: proc_macro::TokenStream,
249  input: proc_macro::TokenStream,
250) -> proc_macro::TokenStream {
251  let model = syn::parse_macro_input!(input as Model);
252
253  model.to_token_stream().into()
254}
255
256#[cfg(test)]
257mod macro_tests {
258  use super::*;
259  use crate::exts::TokenStreamExt;
260
261  #[track_caller]
262  fn parse_model(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
263    let model = syn::parse2::<Model>(input).unwrap();
264
265    model.to_token_stream()
266  }
267
268  #[track_caller]
269  fn assert_eq_token_stream(
270    output: proc_macro2::TokenStream,
271    expected_output: proc_macro2::TokenStream,
272  ) {
273    let left = output.to_pretty();
274    let right = expected_output.to_pretty();
275
276    let theme = termdiff::SignsColorTheme::default();
277
278    let diff = termdiff::DrawDiff::new(&right, &left, &theme);
279
280    assert_eq!(left, right, "\n\n{diff}");
281  }
282
283  fn test_macro(
284    input: proc_macro2::TokenStream,
285    expected_output: proc_macro2::TokenStream,
286  ) {
287    let output = parse_model(input);
288
289    assert_eq_token_stream(output, expected_output);
290  }
291
292  #[test]
293  fn simple_struct() {
294    test_macro(
295      quote! {
296        struct JsType {
297          my_prop_1: String,
298        }
299      },
300      quote! {
301        #[::wasm_bindgen::prelude::wasm_bindgen]
302        extern "C" {
303          type JsType;
304
305          #[wasm_bindgen(method, getter)]
306          #[wasm_bindgen(js_name = "myProp1")]
307          fn my_prop_1(this: &JsType) -> String;
308
309          #[wasm_bindgen(method, setter)]
310          #[wasm_bindgen(js_name = "myProp1")]
311          fn set_my_prop_1(this: &JsType, value: String);
312        }
313      },
314    );
315  }
316
317  #[test]
318  fn struct_with_field_rename() {
319    test_macro(
320      quote! {
321        struct JsType {
322          #[opts(js_name = "prop")]
323          my_prop_1: String,
324        }
325      },
326      quote! {
327        #[::wasm_bindgen::prelude::wasm_bindgen]
328        extern "C" {
329          type JsType;
330
331          #[wasm_bindgen(method, getter)]
332          #[wasm_bindgen(js_name = "prop")]
333          fn my_prop_1(this: &JsType) -> String;
334
335          #[wasm_bindgen(method, setter)]
336          #[wasm_bindgen(js_name = "prop")]
337          fn set_my_prop_1(this: &JsType, value: String);
338        }
339      },
340    );
341  }
342
343  #[test]
344  fn struct_on_other_type() {
345    test_macro(
346      quote! {
347        #[opts(on = SomeType)]
348        struct JsType {
349          my_prop_1: String,
350        }
351      },
352      quote! {
353        #[::wasm_bindgen::prelude::wasm_bindgen]
354        extern "C" {
355          #[wasm_bindgen(method, getter)]
356          #[wasm_bindgen(js_name = "myProp1")]
357          fn my_prop_1(this: &SomeType) -> String;
358
359          #[wasm_bindgen(method, setter)]
360          #[wasm_bindgen(js_name = "myProp1")]
361          fn set_my_prop_1(this: &SomeType, value: String);
362        }
363      },
364    );
365  }
366
367  #[test]
368  fn struct_final_and_structural() {
369    test_macro(
370      quote! {
371        #[opts(final_)]
372        struct JsType {
373          my_prop_1: String,
374          #[opts(structural)]
375          prop: String,
376        }
377      },
378      quote! {
379        #[::wasm_bindgen::prelude::wasm_bindgen]
380        extern "C" {
381          type JsType;
382
383          #[wasm_bindgen(method, getter)]
384          #[wasm_bindgen(js_name = "myProp1")]
385          #[wasm_bindgen(final)]
386          fn my_prop_1(this: &JsType) -> String;
387
388          #[wasm_bindgen(method, setter)]
389          #[wasm_bindgen(js_name = "myProp1")]
390          #[wasm_bindgen(final)]
391          fn set_my_prop_1(this: &JsType, value: String);
392
393          #[wasm_bindgen(method, getter)]
394          #[wasm_bindgen(js_name = "prop")]
395          fn prop(this: &JsType) -> String;
396
397          #[wasm_bindgen(method, setter)]
398          #[wasm_bindgen(js_name = "prop")]
399          fn set_prop(this: &JsType, value: String);
400        }
401      },
402    );
403  }
404
405  #[test]
406  fn struct_only_get_set() {
407    test_macro(
408      quote! {
409        struct JsType {
410          #[opts(getter)]
411          my_prop_1: String,
412          #[opts(setter)]
413          prop: String,
414        }
415      },
416      quote! {
417        #[::wasm_bindgen::prelude::wasm_bindgen]
418        extern "C" {
419          type JsType;
420
421          #[wasm_bindgen(method, getter)]
422          #[wasm_bindgen(js_name = "myProp1")]
423          fn my_prop_1(this: &JsType) -> String;
424
425          #[wasm_bindgen(method, setter)]
426          #[wasm_bindgen(js_name = "prop")]
427          fn set_prop(this: &JsType, value: String);
428        }
429      },
430    );
431  }
432
433  #[test]
434  fn struct_global_setter_with_local_getter() {
435    test_macro(
436      quote! {
437        #[opts(setter)]
438        struct JsType {
439          #[opts(getter)]
440          my_prop_1: String,
441          prop: String,
442        }
443      },
444      quote! {
445        #[::wasm_bindgen::prelude::wasm_bindgen]
446        extern "C" {
447          type JsType;
448
449          #[wasm_bindgen(method, getter)]
450          #[wasm_bindgen(js_name = "myProp1")]
451          fn my_prop_1(this: &JsType) -> String;
452
453          #[wasm_bindgen(method, setter)]
454          #[wasm_bindgen(js_name = "myProp1")]
455          fn set_my_prop_1(this: &JsType, value: String);
456
457          #[wasm_bindgen(method, setter)]
458          #[wasm_bindgen(js_name = "prop")]
459          fn set_prop(this: &JsType, value: String);
460        }
461      },
462    );
463  }
464
465  #[test]
466  fn struct_js_class() {
467    test_macro(
468      quote! {
469        #[opts(js_name = "String")]
470        struct JsString {
471          prop: String,
472        }
473      },
474      quote! {
475        #[::wasm_bindgen::prelude::wasm_bindgen]
476        extern "C" {
477          #[wasm_bindgen(js_name = "String")]
478          type JsString;
479
480          #[wasm_bindgen(method, getter)]
481          #[wasm_bindgen(js_class = "String")]
482          #[wasm_bindgen(js_name = "prop")]
483          fn prop(this: &JsString) -> String;
484
485          #[wasm_bindgen(method, setter)]
486          #[wasm_bindgen(js_class = "String")]
487          #[wasm_bindgen(js_name = "prop")]
488          fn set_prop(this: &JsString, value: String);
489        }
490      },
491    );
492  }
493
494  #[test]
495  fn struct_extends() {
496    test_macro(
497      quote! {
498        #[opts(js_name = "String")]
499        #[opts(extends = Object)]
500        struct JsString {}
501      },
502      quote! {
503        #[::wasm_bindgen::prelude::wasm_bindgen]
504        extern "C" {
505          #[wasm_bindgen(js_name = "String")]
506          #[wasm_bindgen(extends = Object)]
507          type JsString;
508        }
509      },
510    );
511  }
512
513  #[test]
514  fn struct_can_use_self_ty() {
515    test_macro(
516      quote! {
517        struct JsType {
518          a: Self,
519        }
520      },
521      quote! {
522        #[::wasm_bindgen::prelude::wasm_bindgen]
523        extern "C" {
524          type JsType;
525
526          #[wasm_bindgen(method, getter)]
527          #[wasm_bindgen(js_name = "a")]
528          fn a(this: &JsType) -> JsType;
529
530          #[wasm_bindgen(method, setter)]
531          #[wasm_bindgen(js_name = "a")]
532          fn set_a(this: &JsType, value: JsType);
533        }
534      },
535    );
536  }
537
538  #[test]
539  fn simpl_impl() {
540    test_macro(
541      quote! {
542        impl JsType {
543          fn example(&self);
544        }
545      },
546      quote! {
547        impl JsType {
548          fn example(&self) {
549            #[::wasm_bindgen::prelude::wasm_bindgen]
550            extern "C" {
551              #[wasm_bindgen(method)]
552              #[wasm_bindgen(js_name = "example")]
553              fn example_js(this: &JsType);
554            }
555
556            self.example_js()
557          }
558        }
559      },
560    );
561  }
562
563  #[test]
564  fn impl_static() {
565    test_macro(
566      quote! {
567        impl JsType {
568          fn example();
569        }
570      },
571      quote! {
572        impl JsType {
573          fn example() {
574            #[::wasm_bindgen::prelude::wasm_bindgen]
575            extern "C" {
576              #[wasm_bindgen(static_method_of = JsType)]
577              #[wasm_bindgen(js_name = "example")]
578              fn example_js();
579            }
580
581            Self::example_js()
582          }
583        }
584      },
585    );
586  }
587
588  #[test]
589  fn impl_can_map_value() {
590    test_macro(
591      quote! {
592        impl JsType {
593          fn example(&self) -> MapValue<T, U>;
594        }
595      },
596      quote! {
597        impl JsType {
598          fn example(&self) -> U {
599            #[::wasm_bindgen::prelude::wasm_bindgen]
600            extern "C" {
601              #[wasm_bindgen(method)]
602              #[wasm_bindgen(js_name = "example")]
603              fn example_js(this: &JsType) -> T;
604            }
605
606            self.example_js()
607          }
608        }
609      },
610    );
611  }
612
613  #[test]
614  fn impl_can_async_with_args_can_map_value() {
615    test_macro(
616      quote! {
617        impl JsType {
618          async fn example(&self, a: String) -> MapValue<T, U>;
619        }
620      },
621      quote! {
622        impl JsType {
623          async fn example(&self, a: String) -> U {
624            #[::wasm_bindgen::prelude::wasm_bindgen]
625            extern "C" {
626              #[wasm_bindgen(method)]
627              #[wasm_bindgen(js_name = "example")]
628              async fn example_js(this: &JsType, a: String) -> T;
629            }
630
631            self.example_js(a).await
632          }
633        }
634      },
635    );
636  }
637
638  #[test]
639  fn impl_can_async_with_args_can_map_value_with_block() {
640    test_macro(
641      quote! {
642        impl JsType {
643          async fn example(&self, a: String) -> MapValue<T, U> {
644            self.example_js(a).await.into()
645          }
646        }
647      },
648      quote! {
649        impl JsType {
650          async fn example(&self, a: String) -> U {
651            #[::wasm_bindgen::prelude::wasm_bindgen]
652            extern "C" {
653              #[wasm_bindgen(method)]
654              #[wasm_bindgen(js_name = "example")]
655              async fn example_js(this: &JsType, a: String) -> T;
656            }
657
658            self.example_js(a).await.into()
659          }
660        }
661      },
662    );
663  }
664
665  #[test]
666  fn impl_with_result_catches() {
667    test_macro(
668      quote! {
669        impl JsType {
670          fn example(&self) -> Result<String, JsValue>;
671        }
672      },
673      quote! {
674        impl JsType {
675          fn example(&self) -> Result<String, JsValue> {
676            #[::wasm_bindgen::prelude::wasm_bindgen]
677            extern "C" {
678              #[wasm_bindgen(method)]
679              #[wasm_bindgen(js_name = "example")]
680              #[wasm_bindgen(catch)]
681              fn example_js(this: &JsType) -> Result<String, JsValue>;
682            }
683
684            self.example_js()
685          }
686        }
687      },
688    );
689  }
690
691  #[test]
692  fn impl_with_map_value_result_catches() {
693    test_macro(
694      quote! {
695        impl JsType {
696          async fn example(&self) -> MapValue<
697            Result<JsValue, JsValue>,
698            Result<String, JsValue>,
699          >;
700        }
701      },
702      quote! {
703        impl JsType {
704          async fn example(&self) -> Result<String, JsValue> {
705            #[::wasm_bindgen::prelude::wasm_bindgen]
706            extern "C" {
707              #[wasm_bindgen(method)]
708              #[wasm_bindgen(js_name = "example")]
709              #[wasm_bindgen(catch)]
710              async fn example_js(this: &JsType) -> Result<JsValue, JsValue>;
711            }
712
713            self.example_js().await
714          }
715        }
716      },
717    );
718  }
719
720  #[test]
721  fn impl_with_module_gets_applied() {
722    test_macro(
723      quote! {
724        #[opts(module = "my-module")]
725        impl JsType {
726          fn example();
727        }
728      },
729      quote! {
730        impl JsType {
731          fn example() {
732            #[::wasm_bindgen::prelude::wasm_bindgen(module = "my-module")]
733            extern "C" {
734              #[wasm_bindgen(static_method_of = JsType)]
735              #[wasm_bindgen(js_name = "example")]
736              fn example_js();
737            }
738
739            Self::example_js()
740          }
741        }
742      },
743    );
744  }
745}