diff --git a/README.md b/README.md index 13f4db2..e220fa2 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Tsify container attributes - `into_wasm_abi` implements `IntoWasmAbi` and `OptionIntoWasmAbi`. This can be converted directly from Rust to JS via `serde_json` or `serde-wasm-bindgen`. - `from_wasm_abi` implements `FromWasmAbi` and `OptionFromWasmAbi`. This is the opposite operation of the above. - `namespace` generates a namespace for the enum variants. +- `vector_into_wasm_abi` implements `VectorIntoWasmAbi`. This is the vector version of `into_wasm_abi` and is needed when working with vectors. +- `vector_from_wasm_abi` implements `VectorFromWasmAbi`. This is the opposite operation of the above. Tsify field attributes diff --git a/tsify-next-macros/src/attrs.rs b/tsify-next-macros/src/attrs.rs index 4ef01c3..2cf8570 100644 --- a/tsify-next-macros/src/attrs.rs +++ b/tsify-next-macros/src/attrs.rs @@ -8,8 +8,12 @@ use crate::comments::extract_doc_comments; pub struct TsifyContainerAttrs { /// Implement `IntoWasmAbi` for the type. pub into_wasm_abi: bool, + /// Implement `VectorIntoWasmAbi` for the type. + pub vector_into_wasm_abi: bool, /// Implement `FromWasmAbi` for the type. pub from_wasm_abi: bool, + /// Implement `VectorFromWasmAbi` for the type. + pub vector_from_wasm_abi: bool, /// Whether the type should be wrapped in a Typescript namespace. pub namespace: bool, /// Information about how the type should be serialized. @@ -46,7 +50,9 @@ impl TsifyContainerAttrs { pub fn from_derive_input(input: &syn::DeriveInput) -> syn::Result { let mut attrs = Self { into_wasm_abi: false, + vector_into_wasm_abi: false, from_wasm_abi: false, + vector_from_wasm_abi: false, namespace: false, ty_config: TypeGenerationConfig::default(), comments: extract_doc_comments(&input.attrs), @@ -66,6 +72,14 @@ impl TsifyContainerAttrs { return Ok(()); } + if meta.path.is_ident("vector_into_wasm_abi") { + if attrs.vector_into_wasm_abi { + return Err(meta.error("duplicate attribute")); + } + attrs.vector_into_wasm_abi = true; + return Ok(()); + } + if meta.path.is_ident("from_wasm_abi") { if attrs.from_wasm_abi { return Err(meta.error("duplicate attribute")); @@ -73,6 +87,14 @@ impl TsifyContainerAttrs { attrs.from_wasm_abi = true; return Ok(()); } + + if meta.path.is_ident("vector_from_wasm_abi") { + if attrs.vector_from_wasm_abi { + return Err(meta.error("duplicate attribute")); + } + attrs.vector_from_wasm_abi = true; + return Ok(()); + } if meta.path.is_ident("namespace") { if !matches!(input.data, syn::Data::Enum(_)) { diff --git a/tsify-next-macros/src/wasm_bindgen.rs b/tsify-next-macros/src/wasm_bindgen.rs index e0dc094..8806172 100644 --- a/tsify-next-macros/src/wasm_bindgen.rs +++ b/tsify-next-macros/src/wasm_bindgen.rs @@ -29,6 +29,18 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { } }); + let wasm_vector_abi = attrs.vector_into_wasm_abi || attrs.vector_from_wasm_abi; + let wasm_describe_vector = wasm_vector_abi.then(|| { + quote! { + impl #impl_generics WasmDescribeVector for #ident #ty_generics #where_clause { + #[inline] + fn describe_vector() { + ::JsType::describe_vector() + } + } + } + }); + let use_serde = wasm_abi.then(|| match cont.serde_container.attrs.custom_serde_path() { Some(path) => quote! { use #path as _serde; @@ -37,9 +49,19 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { extern crate serde as _serde; }, }); + let into_wasm_abi = attrs.into_wasm_abi.then(|| expand_into_wasm_abi(cont)); + + let vector_into_wasm_abi = attrs + .vector_into_wasm_abi + .then(|| expand_vector_into_wasm_abi(cont)); + let from_wasm_abi = attrs.from_wasm_abi.then(|| expand_from_wasm_abi(cont)); + let vector_from_wasm_abi = attrs + .vector_from_wasm_abi + .then(|| expand_vector_from_wasm_abi(cont)); + let typescript_type = decl.id(); let missing_as_null = attrs.ty_config.missing_as_null; @@ -52,8 +74,8 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { #use_serde use tsify_next::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, RefFromWasmAbi}, - describe::WasmDescribe, + convert::{FromWasmAbi, VectorFromWasmAbi, IntoWasmAbi, VectorIntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, RefFromWasmAbi}, + describe::WasmDescribe, describe::WasmDescribeVector, prelude::*, }; @@ -76,8 +98,11 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { #typescript_custom_section #wasm_describe + #wasm_describe_vector #into_wasm_abi + #vector_into_wasm_abi #from_wasm_abi + #vector_from_wasm_abi }; } } @@ -147,6 +172,50 @@ fn expand_into_wasm_abi(cont: &Container) -> TokenStream { } } +fn expand_vector_into_wasm_abi(cont: &Container) -> TokenStream { + let ident = cont.ident(); + let serde_path = cont.serde_container.attrs.serde_path(); + + let borrowed_generics = cont.generics(); + let mut generics = cont.generics().clone(); + generics + .make_where_clause() + .predicates + .push(parse_quote!(#ident #borrowed_generics: #serde_path::Serialize)); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + impl #impl_generics VectorIntoWasmAbi for #ident #ty_generics #where_clause { + type Abi = ::Abi; + + #[inline] + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi { + let values = vector + .iter() + .map(|value| + // wasm_bindgen doesn't forward the error message from the `into_js` result. + // https://github.com/rustwasm/wasm-bindgen/issues/2732 + // Until that issue is fixed, we don't directly use `unwrap_throw()` and instead build our + // own error message. + // Convert to `value.into_js().unwrap_throw().into()` when fixed. + match value.into_js() { + Ok(js) => js.into(), + Err(err) => { + let loc = core::panic::Location::caller(); + let msg = format!("(Converting type failed) {} ({}:{}:{})", err, loc.file(), loc.line(), loc.column()); + // In theory, `wasm_bindgen::throw_str(&msg)` should work, but the error emitted by `wasm_bindgen::throw_str` cannot be picked up by `#[should_panic(expect = ...)]` in tests, so we use a regular panic. + panic!("{}", msg); + } + }) + .collect(); + + JsValue::vector_into_abi(values) + } + } + } +} + fn expand_from_wasm_abi(cont: &Container) -> TokenStream { let ident = cont.ident(); let serde_path = cont.serde_container.attrs.serde_path(); @@ -206,3 +275,37 @@ fn expand_from_wasm_abi(cont: &Container) -> TokenStream { } } } + +fn expand_vector_from_wasm_abi(cont: &Container) -> TokenStream { + let ident = cont.ident(); + let serde_path = cont.serde_container.attrs.serde_path(); + + let mut generics = cont.generics().clone(); + + generics + .make_where_clause() + .predicates + .push(parse_quote!(Self: #serde_path::de::DeserializeOwned)); + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + impl #impl_generics VectorFromWasmAbi for #ident #ty_generics #where_clause { + type Abi = ::Abi; + + #[inline] + unsafe fn vector_from_abi(js: Self::Abi) -> Box<[Self]> { + JsValue::vector_from_abi(js) + .into_iter() + .map(|value| { + let result = Self::from_js(value); + if let Err(err) = result { + wasm_bindgen::throw_str(err.to_string().as_ref()); + } + result.unwrap_throw() + }) + .collect() + } + } + } +}