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}