From 9bde38007dcc91975abcb6d44643e2af36c0b2dd Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Thu, 20 Jul 2023 18:35:36 -0400 Subject: [PATCH 01/15] Support ByteBuf --- tsify-macros/src/typescript.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index 25fd6ae..11edd5f 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -321,6 +321,17 @@ impl TsType { Self::Array(Box::new(elem)) } + "ByteBuf" => { + if cfg!(feature = "js") { + Self::Ref { + name: String::from("Uint8Array"), + type_params: vec![], + } + } else { + Self::Array(Box::new(Self::NUMBER)) + } + } + "Option" if args.len() == 1 => Self::Option(Box::new(Self::from_syn_type(args[0]))), "Result" if args.len() == 2 => { @@ -735,12 +746,14 @@ mod tests { assert_ts!(HashMap | BTreeMap, "Map"); assert_ts!(Option, "number | undefined"); assert_ts!(Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | undefined)[]"); + assert_ts!(ByteBuf, "Uint8Array"); } else { assert_ts!((), "null"); assert_ts!(u128 | i128, "number"); assert_ts!(HashMap | BTreeMap, "Record"); assert_ts!(Option, "number | null"); assert_ts!(Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | null)[]"); + assert_ts!(ByteBuf, "number[]"); } assert_ts!( From 96c302fbf199e91ae7837b90341cc288628ce296 Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Thu, 20 Jul 2023 18:52:27 -0400 Subject: [PATCH 02/15] Support RefFromWasmAbi --- tests/expand/borrow.expanded.rs | 22 ++++++++++++- tests/expand/generic_enum.expanded.rs | 22 ++++++++++++- tests/expand/generic_struct.expanded.rs | 44 +++++++++++++++++++++++-- tsify-macros/src/wasm_bindgen.rs | 22 ++++++++++++- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/tests/expand/borrow.expanded.rs b/tests/expand/borrow.expanded.rs index e370963..b406e74 100644 --- a/tests/expand/borrow.expanded.rs +++ b/tests/expand/borrow.expanded.rs @@ -10,7 +10,10 @@ const _: () = { extern crate serde as _serde; use tsify::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi}, + convert::{ + FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, + RefFromWasmAbi, + }, describe::WasmDescribe, prelude::*, }; #[wasm_bindgen] @@ -72,4 +75,21 @@ const _: () = { ::is_none(js) } } + pub struct SelfOwner(T); + impl ::core::ops::Deref for SelfOwner { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl<'a> RefFromWasmAbi for Borrow<'a> + where + Self: _serde::de::DeserializeOwned, + { + type Abi = ::Abi; + type Anchor = SelfOwner; + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + SelfOwner(Self::from_abi(js)) + } + } }; diff --git a/tests/expand/generic_enum.expanded.rs b/tests/expand/generic_enum.expanded.rs index 5821d1d..9346411 100644 --- a/tests/expand/generic_enum.expanded.rs +++ b/tests/expand/generic_enum.expanded.rs @@ -11,7 +11,10 @@ const _: () = { extern crate serde as _serde; use tsify::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi}, + convert::{ + FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, + RefFromWasmAbi, + }, describe::WasmDescribe, prelude::*, }; #[wasm_bindgen] @@ -73,4 +76,21 @@ const _: () = { ::is_none(js) } } + pub struct SelfOwner(T); + impl ::core::ops::Deref for SelfOwner { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl RefFromWasmAbi for GenericEnum + where + Self: _serde::de::DeserializeOwned, + { + type Abi = ::Abi; + type Anchor = SelfOwner; + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + SelfOwner(Self::from_abi(js)) + } + } }; diff --git a/tests/expand/generic_struct.expanded.rs b/tests/expand/generic_struct.expanded.rs index 935749f..2287516 100644 --- a/tests/expand/generic_struct.expanded.rs +++ b/tests/expand/generic_struct.expanded.rs @@ -8,7 +8,10 @@ const _: () = { extern crate serde as _serde; use tsify::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi}, + convert::{ + FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, + RefFromWasmAbi, + }, describe::WasmDescribe, prelude::*, }; #[wasm_bindgen] @@ -70,6 +73,23 @@ const _: () = { ::is_none(js) } } + pub struct SelfOwner(T); + impl ::core::ops::Deref for SelfOwner { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl RefFromWasmAbi for GenericStruct + where + Self: _serde::de::DeserializeOwned, + { + type Abi = ::Abi; + type Anchor = SelfOwner; + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + SelfOwner(Self::from_abi(js)) + } + } }; #[tsify(into_wasm_abi, from_wasm_abi)] pub struct GenericNewtype(T); @@ -78,7 +98,10 @@ const _: () = { extern crate serde as _serde; use tsify::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi}, + convert::{ + FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, + RefFromWasmAbi, + }, describe::WasmDescribe, prelude::*, }; #[wasm_bindgen] @@ -140,4 +163,21 @@ const _: () = { ::is_none(js) } } + pub struct SelfOwner(T); + impl ::core::ops::Deref for SelfOwner { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + impl RefFromWasmAbi for GenericNewtype + where + Self: _serde::de::DeserializeOwned, + { + type Abi = ::Abi; + type Anchor = SelfOwner; + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + SelfOwner(Self::from_abi(js)) + } + } }; diff --git a/tsify-macros/src/wasm_bindgen.rs b/tsify-macros/src/wasm_bindgen.rs index 9d796a4..fb4a95b 100644 --- a/tsify-macros/src/wasm_bindgen.rs +++ b/tsify-macros/src/wasm_bindgen.rs @@ -48,7 +48,7 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { #use_serde use tsify::Tsify; use wasm_bindgen::{ - convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi}, + convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi, OptionIntoWasmAbi, RefFromWasmAbi}, describe::WasmDescribe, prelude::*, }; @@ -137,5 +137,25 @@ fn expand_from_wasm_abi(cont: &Container) -> TokenStream { ::is_none(js) } } + + pub struct SelfOwner(T); + + impl ::core::ops::Deref for SelfOwner { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl #impl_generics RefFromWasmAbi for #ident #ty_generics #where_clause { + type Abi = ::Abi; + + type Anchor = SelfOwner; + + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + SelfOwner(Self::from_abi(js)) + } + } } } From 33ce45dcef6493a6af867aaf766b4e1ad8806f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 14 Aug 2023 13:36:23 +0200 Subject: [PATCH 03/15] Extract and copy rustdoc comments into type definitions --- tsify-macros/src/attrs.rs | 6 +++ tsify-macros/src/comments.rs | 69 ++++++++++++++++++++++++++++++++++ tsify-macros/src/decl.rs | 52 ++++++++++++++++++++++--- tsify-macros/src/lib.rs | 1 + tsify-macros/src/parser.rs | 9 +++++ tsify-macros/src/type_alias.rs | 5 ++- tsify-macros/src/typescript.rs | 34 +++++++++++++++++ 7 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 tsify-macros/src/comments.rs diff --git a/tsify-macros/src/attrs.rs b/tsify-macros/src/attrs.rs index 3e64cc3..34e0c1b 100644 --- a/tsify-macros/src/attrs.rs +++ b/tsify-macros/src/attrs.rs @@ -1,10 +1,13 @@ use serde_derive_internals::ast::Field; +use crate::comments::extract_doc_comments; + #[derive(Debug, Default)] pub struct TsifyContainerAttars { pub into_wasm_abi: bool, pub from_wasm_abi: bool, pub namespace: bool, + pub comments: Vec, } impl TsifyContainerAttars { @@ -13,6 +16,7 @@ impl TsifyContainerAttars { into_wasm_abi: false, from_wasm_abi: false, namespace: false, + comments: extract_doc_comments(&input.attrs), }; for attr in &input.attrs { @@ -60,6 +64,7 @@ impl TsifyContainerAttars { pub struct TsifyFieldAttrs { pub type_override: Option, pub optional: bool, + pub comments: Vec, } impl TsifyFieldAttrs { @@ -67,6 +72,7 @@ impl TsifyFieldAttrs { let mut attrs = Self { type_override: None, optional: false, + comments: extract_doc_comments(&field.original.attrs), }; for attr in &field.original.attrs { diff --git a/tsify-macros/src/comments.rs b/tsify-macros/src/comments.rs new file mode 100644 index 0000000..e9aede3 --- /dev/null +++ b/tsify-macros/src/comments.rs @@ -0,0 +1,69 @@ +use proc_macro2::TokenTree; +use quote::ToTokens; + +use crate::typescript::TsType; + +/// Extract the documentation comments from a Vec of attributes +pub fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { + attrs + .iter() + .filter_map(|a| { + // if the path segments include an ident of "doc" we know this + // this is a doc comment + if a.path() + .segments + .iter() + .any(|s| s.ident.to_string() == "doc") + { + Some(a.to_token_stream().into_iter().filter_map(|t| match t { + TokenTree::Group(group) => { + // this will return the inner tokens of the group + // which will be the doc comments + Some( + group + .stream() + .into_iter() + .filter_map(|t| match t { + TokenTree::Literal(lit) => { + // this will always return the quoted string, we deal with + // that in the cli when we read in the comments + Some(lit.to_string()) + } + _ => None, + }) + .collect::>() + .join(""), + ) + } + _ => None, + })) + } else { + None + } + }) + //Fold up the [[String]] iter we created into Vec + .fold(vec![], |mut acc, a| { + acc.extend(a); + acc + }) +} + +pub fn format_doc_comments(comments: &Vec) -> String { + let comment = comments + .iter() + .map(|line| format!(" *{}\n", line.trim_matches('"'))) + .collect::>() + .join(""); + + format!("/**\n{} */\n", comment) +} + +pub fn clean_comments(typ: &mut TsType) -> () { + if let TsType::TypeLit(ref mut lit) = typ { + lit.members.iter_mut().for_each(|elem| { + elem.comments = vec![]; + // Recurse + clean_comments(&mut elem.type_ann); + }); + } +} diff --git a/tsify-macros/src/decl.rs b/tsify-macros/src/decl.rs index 9be3a9e..5984585 100644 --- a/tsify-macros/src/decl.rs +++ b/tsify-macros/src/decl.rs @@ -1,7 +1,11 @@ -use std::fmt::Display; use std::ops::Deref; +use std::{fmt::Display, vec}; -use crate::typescript::{TsType, TsTypeElement, TsTypeLit}; +use crate::comments::clean_comments; +use crate::{ + comments::format_doc_comments, + typescript::{TsType, TsTypeElement, TsTypeLit}, +}; #[derive(Clone)] pub struct TsTypeAliasDecl { @@ -9,6 +13,18 @@ pub struct TsTypeAliasDecl { pub export: bool, pub type_params: Vec, pub type_ann: TsType, + pub comments: Vec, +} + +impl TsTypeAliasDecl { + pub fn to_string_with_indent(&self, indent: usize) -> String { + let out = self.to_string(); + let indent_str = " ".repeat(indent); + out.split("\n") + .map(|line| format!("{}{}", indent_str, line)) + .collect::>() + .join("\n") + } } impl Display for TsTypeAliasDecl { @@ -20,6 +36,10 @@ impl Display for TsTypeAliasDecl { format!("{}<{}>", self.id, type_params) }; + if !self.comments.is_empty() { + write!(f, "{}", format_doc_comments(&self.comments))?; + } + if self.export { write!(f, "export ")?; } @@ -32,10 +52,15 @@ pub struct TsInterfaceDecl { pub type_params: Vec, pub extends: Vec, pub body: Vec, + pub comments: Vec, } impl Display for TsInterfaceDecl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !self.comments.is_empty() { + write!(f, "{}", format_doc_comments(&self.comments))?; + } + write!(f, "export interface {}", self.id)?; if !self.type_params.is_empty() { @@ -60,7 +85,7 @@ impl Display for TsInterfaceDecl { let members = self .body .iter() - .map(|elem| format!("\n {elem};")) + .map(|elem| format!("\n{};", elem.to_string_with_indent(4))) .collect::>() .join(""); @@ -74,6 +99,7 @@ pub struct TsEnumDecl { pub type_params: Vec, pub members: Vec, pub namespace: bool, + pub comments: Vec, } const ALPHABET_UPPER: [char; 26] = [ @@ -141,6 +167,7 @@ impl TsEnumDecl { key: t.key.clone(), optional: t.optional, type_ann: TsEnumDecl::replace_type_params(t.type_ann.clone(), type_args), + comments: vec![], }) .collect(), }), @@ -187,6 +214,7 @@ impl Display for TsEnumDecl { export: false, type_params: type_refs, type_ann: ts_type, + comments: vec![], } }) .collect::>() @@ -197,6 +225,11 @@ impl Display for TsEnumDecl { for type_ref in type_refs { writeln!(f, "{}", type_ref)?; } + + if !self.comments.is_empty() { + write!(f, "{}", format_doc_comments(&self.comments))?; + } + write!(f, "declare namespace {}", self.id)?; if self.members.is_empty() { @@ -214,8 +247,9 @@ impl Display for TsEnumDecl { .type_ann .clone() .prefix_type_refs(&prefix, &self.type_params), + comments: elem.comments.clone(), }) - .map(|elem| format!("\n {elem}")) + .map(|elem| format!("\n{}", elem.to_string_with_indent(4))) .collect::>() .join(""); @@ -232,9 +266,17 @@ impl Display for TsEnumDecl { type_ann: TsType::Union( self.members .iter() - .map(|member| member.type_ann.clone()) + .map(|member| { + // let mut type_refs = Vec::new(); + // TsEnumDecl::replace_type_params(member.type_ann.clone(), &mut type_refs) + + let mut clone = member.type_ann.clone(); + clean_comments(&mut clone); + clone + }) .collect(), ), + comments: self.comments.clone(), } .fmt(f) } diff --git a/tsify-macros/src/lib.rs b/tsify-macros/src/lib.rs index a48e847..0e261e6 100644 --- a/tsify-macros/src/lib.rs +++ b/tsify-macros/src/lib.rs @@ -1,4 +1,5 @@ mod attrs; +mod comments; mod container; mod ctxt; mod decl; diff --git a/tsify-macros/src/parser.rs b/tsify-macros/src/parser.rs index 66c6c2d..9b55e09 100644 --- a/tsify-macros/src/parser.rs +++ b/tsify-macros/src/parser.rs @@ -7,6 +7,7 @@ use serde_derive_internals::{ use crate::{ attrs::TsifyFieldAttrs, + comments::extract_doc_comments, container::Container, decl::{Decl, TsEnumDecl, TsInterfaceDecl, TsTypeAliasDecl}, typescript::{TsType, TsTypeElement, TsTypeLit}, @@ -74,6 +75,7 @@ impl<'a> Parser<'a> { export: true, type_params: self.create_relevant_type_params(type_ann.type_ref_names()), type_ann, + comments: extract_doc_comments(&self.container.serde_container.original.attrs), }) } @@ -95,6 +97,7 @@ impl<'a> Parser<'a> { type_params, extends, body: members, + comments: extract_doc_comments(&self.container.serde_container.original.attrs), }) } else { let extra = TsType::Intersection( @@ -124,6 +127,7 @@ impl<'a> Parser<'a> { key: tag.clone(), type_ann: TsType::Lit(name), optional: false, + comments: vec![], }; let mut vec = Vec::with_capacity(members.len() + 1); @@ -224,10 +228,13 @@ impl<'a> Parser<'a> { type_ann }; + let comments = extract_doc_comments(&field.original.attrs); + TsTypeElement { key, type_ann, optional: optional || !default_is_none, + comments, } }) .collect(); @@ -248,6 +255,7 @@ impl<'a> Parser<'a> { let decl = self.create_type_alias_decl(self.parse_variant(variant)); if let Decl::TsTypeAlias(mut type_alias) = decl { type_alias.id = variant.attrs.name().serialize_name(); + type_alias.comments = extract_doc_comments(&variant.original.attrs); type_alias } else { @@ -268,6 +276,7 @@ impl<'a> Parser<'a> { type_params: relevant_type_params, members, namespace: self.container.attrs.namespace, + comments: extract_doc_comments(&self.container.serde_container.original.attrs), }) } diff --git a/tsify-macros/src/type_alias.rs b/tsify-macros/src/type_alias.rs index 3efdcca..45e85b9 100644 --- a/tsify-macros/src/type_alias.rs +++ b/tsify-macros/src/type_alias.rs @@ -1,7 +1,9 @@ use proc_macro2::TokenStream; use quote::quote; -use crate::{ctxt::Ctxt, decl::TsTypeAliasDecl, typescript::TsType}; +use crate::{ + comments::extract_doc_comments, ctxt::Ctxt, decl::TsTypeAliasDecl, typescript::TsType, +}; pub fn expend(item: syn::ItemType) -> syn::Result { let ctxt = Ctxt::new(); @@ -17,6 +19,7 @@ pub fn expend(item: syn::ItemType) -> syn::Result { .map(|ty| ty.ident.to_string()) .collect(), type_ann, + comments: extract_doc_comments(&item.attrs), }; let decl_str = decl.to_string(); diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index 25fd6ae..5518b0a 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -2,6 +2,8 @@ use std::{collections::HashSet, fmt::Display}; use serde_derive_internals::{ast::Style, attr::TagType}; +use crate::comments::format_doc_comments; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TsKeywordTypeKind { Number, @@ -19,6 +21,7 @@ pub struct TsTypeElement { pub key: String, pub type_ann: TsType, pub optional: bool, + pub comments: Vec, } impl From for TsTypeLit { @@ -101,6 +104,7 @@ macro_rules! type_lit { key: stringify!($k).to_string(), type_ann: $t, optional: false, + comments: vec![], } ),*], }) @@ -383,6 +387,7 @@ impl TsType { key: name, type_ann, optional: false, + comments: vec![], } .into() } @@ -393,6 +398,7 @@ impl TsType { key: tag.clone(), type_ann: TsType::Lit(name), optional: false, + comments: vec![], } .into(); @@ -402,6 +408,7 @@ impl TsType { key: tag.clone(), type_ann: TsType::Lit(name), optional: false, + comments: vec![], } .into(); @@ -413,6 +420,7 @@ impl TsType { key: tag.clone(), type_ann: TsType::Lit(name), optional: false, + comments: vec![], }; if matches!(style, Style::Unit) { @@ -422,6 +430,7 @@ impl TsType { key: content.clone(), type_ann, optional: false, + comments: vec![], }; TsTypeLit { @@ -519,6 +528,7 @@ impl TsType { key: t.key.clone(), optional: t.optional, type_ann: t.type_ann.clone().prefix_type_refs(prefix, exceptions), + comments: t.comments.clone(), }) .collect(), }), @@ -578,6 +588,17 @@ fn is_js_ident(string: &str) -> bool { !string.contains('-') } +impl TsTypeElement { + pub fn to_string_with_indent(&self, indent: usize) -> String { + let out = self.to_string(); + let indent_str = " ".repeat(indent); + out.split("\n") + .map(|line| format!("{}{}", indent_str, line)) + .collect::>() + .join("\n") + } +} + impl Display for TsTypeElement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let key = &self.key; @@ -585,6 +606,10 @@ impl Display for TsTypeElement { let optional_ann = if self.optional { "?" } else { "" }; + if !self.comments.is_empty() { + write!(f, "{}", format_doc_comments(&self.comments))?; + } + if is_js_ident(key) { write!(f, "{key}{optional_ann}: {type_ann}") } else { @@ -682,6 +707,15 @@ impl Display for TsType { .iter() .map(|ty| match ty { TsType::Union(_) => format!("({ty})"), + TsType::TypeLit(tl) => { + // Intersections are formatted as single lines, so we need to remove + // any comments as they are multi-line and will break the formatting. + let mut copy = tl.clone(); + copy.members.iter_mut().for_each(|elem| { + elem.comments = vec![]; + }); + copy.to_string() + } _ => ty.to_string(), }) .collect::>() From 1816f9ebc2ba0090c8dc81efdab5113e0d18513f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 14 Aug 2023 13:35:02 +0200 Subject: [PATCH 04/15] Adapt tests to test comment generation --- tests/enum.rs | 272 +++++++++++++++++++++++++++++++++++++++-- tests/flatten.rs | 21 ++++ tests/generics.rs | 62 +++++++++- tests/optional.rs | 63 ++++++++++ tests/rename.rs | 69 ++++++++++- tests/skip.rs | 38 +++++- tests/struct.rs | 109 ++++++++++++++++- tests/transparent.rs | 25 +++- tests/type_override.rs | 44 ++++++- 9 files changed, 682 insertions(+), 21 deletions(-) diff --git a/tests/enum.rs b/tests/enum.rs index 7d3453f..ef9c529 100644 --- a/tests/enum.rs +++ b/tests/enum.rs @@ -11,17 +11,27 @@ struct Foo { #[test] fn test_externally_tagged_enum() { + /// Comment for External #[derive(Tsify)] enum External { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" + /** + * Comment for External + */ export type External = { Struct: { x: string; y: number } } | { EmptyStruct: {} } | { Tuple: [number, string] } | { EmptyTuple: [] } | { Newtype: Foo } | "Unit";"# }; @@ -30,28 +40,59 @@ fn test_externally_tagged_enum() { #[test] fn test_externally_tagged_enum_with_namespace() { + /// Comment for External #[derive(Tsify)] #[tsify(namespace)] enum External { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __ExternalFoo = Foo; + /** + * Comment for External + */ declare namespace External { + /** + * Comment for Struct + */ export type Struct = { Struct: { x: string; y: number } }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = { EmptyStruct: {} }; + /** + * Comment for Tuple + */ export type Tuple = { Tuple: [number, string] }; + /** + * Comment for EmptyTuple + */ export type EmptyTuple = { EmptyTuple: [] }; + /** + * Comment for Newtype + */ export type Newtype = { Newtype: __ExternalFoo }; + /** + * Comment for Unit + */ export type Unit = "Unit"; } - + + /** + * Comment for External + */ export type External = { Struct: { x: string; y: number } } | { EmptyStruct: {} } | { Tuple: [number, string] } | { EmptyTuple: [] } | { Newtype: Foo } | "Unit";"# }; @@ -60,16 +101,24 @@ fn test_externally_tagged_enum_with_namespace() { #[test] fn test_internally_tagged_enum() { + /// Comment for Internal #[derive(Tsify)] #[serde(tag = "t")] enum Internal { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" + /** + * Comment for Internal + */ export type Internal = { t: "Struct"; x: string; y: number } | { t: "EmptyStruct" } | ({ t: "Newtype" } & Foo) | { t: "Unit" };"# }; @@ -78,25 +127,48 @@ fn test_internally_tagged_enum() { #[test] fn test_internally_tagged_enum_with_namespace() { + /// Comment for Internal #[derive(Tsify)] #[serde(tag = "t")] #[tsify(namespace)] enum Internal { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __InternalFoo = Foo; + /** + * Comment for Internal + */ declare namespace Internal { + /** + * Comment for Struct + */ export type Struct = { t: "Struct"; x: string; y: number }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = { t: "EmptyStruct" }; + /** + * Comment for Newtype + */ export type Newtype = { t: "Newtype" } & __InternalFoo; + /** + * Comment for Unit + */ export type Unit = { t: "Unit" }; } - + + /** + * Comment for Internal + */ export type Internal = { t: "Struct"; x: string; y: number } | { t: "EmptyStruct" } | ({ t: "Newtype" } & Foo) | { t: "Unit" };"# }; @@ -105,19 +177,29 @@ fn test_internally_tagged_enum_with_namespace() { #[test] fn test_adjacently_tagged_enum() { + /// Comment for Adjacent #[derive(Tsify)] #[serde(tag = "t", content = "c")] enum Adjacent { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" - export type Adjacent = { t: "Struct"; c: { x: string; y: number } } | { t: "EmptyStruct"; c: {} } | { t: "Tuple"; c: [number, string] } | { t: "EmptyTuple"; c: [] } | { t: "Newtype"; c: Foo } | { t: "Unit" };"# + /** + * Comment for Adjacent + */ + export type Adjacent = { t: "Struct"; c: { x: string; y: number } } | { t: "EmptyStruct"; c: {} } | { t: "Tuple"; c: [number, string] } | { t: "EmptyTuple"; c: [] } | { t: "Newtype"; c: Foo } | { t: "Unit" };"# }; assert_eq!(Adjacent::DECL, expected); @@ -125,29 +207,60 @@ fn test_adjacently_tagged_enum() { #[test] fn test_adjacently_tagged_enum_with_namespace() { + /// Comment for Adjacent #[derive(Tsify)] #[serde(tag = "t", content = "c")] #[tsify(namespace)] enum Adjacent { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __AdjacentFoo = Foo; + /** + * Comment for Adjacent + */ declare namespace Adjacent { + /** + * Comment for Struct + */ export type Struct = { t: "Struct"; c: { x: string; y: number } }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = { t: "EmptyStruct"; c: {} }; + /** + * Comment for Tuple + */ export type Tuple = { t: "Tuple"; c: [number, string] }; + /** + * Comment for EmptyTuple + */ export type EmptyTuple = { t: "EmptyTuple"; c: [] }; + /** + * Comment for Newtype + */ export type Newtype = { t: "Newtype"; c: __AdjacentFoo }; + /** + * Comment for Unit + */ export type Unit = { t: "Unit" }; } - + + /** + * Comment for Adjacent + */ export type Adjacent = { t: "Struct"; c: { x: string; y: number } } | { t: "EmptyStruct"; c: {} } | { t: "Tuple"; c: [number, string] } | { t: "EmptyTuple"; c: [] } | { t: "Newtype"; c: Foo } | { t: "Unit" };"# }; @@ -156,23 +269,36 @@ fn test_adjacently_tagged_enum_with_namespace() { #[test] fn test_untagged_enum() { + /// Comment for Untagged #[derive(Tsify)] #[serde(untagged)] enum Untagged { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = if cfg!(feature = "js") { indoc! {r#" + /** + * Comment for Untagged + */ export type Untagged = { x: string; y: number } | {} | [number, string] | [] | Foo | undefined;"# } } else { indoc! {r#" + /** + * Comment for Untagged + */ export type Untagged = { x: string; y: number } | {} | [number, string] | [] | Foo | null;"# } }; @@ -182,44 +308,99 @@ fn test_untagged_enum() { #[test] fn test_untagged_enum_with_namespace() { + /// Comment for Untagged #[derive(Tsify)] #[serde(untagged)] #[tsify(namespace)] enum Untagged { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Unit Unit, } let expected = if cfg!(feature = "js") { indoc! {r#" type __UntaggedFoo = Foo; + /** + * Comment for Untagged + */ declare namespace Untagged { + /** + * Comment for Struct + */ export type Struct = { x: string; y: number }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = {}; + /** + * Comment for Tuple + */ export type Tuple = [number, string]; + /** + * Comment for EmptyTuple + */ export type EmptyTuple = []; + /** + * Comment for Newtype + */ export type Newtype = __UntaggedFoo; + /** + * Comment for Unit + */ export type Unit = undefined; } - + + /** + * Comment for Untagged + */ export type Untagged = { x: string; y: number } | {} | [number, string] | [] | Foo | undefined;"# } } else { indoc! {r#" type __UntaggedFoo = Foo; + /** + * Comment for Untagged + */ declare namespace Untagged { + /** + * Comment for Struct + */ export type Struct = { x: string; y: number }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = {}; + /** + * Comment for Tuple + */ export type Tuple = [number, string]; + /** + * Comment for EmptyTuple + */ export type EmptyTuple = []; + /** + * Comment for Newtype + */ export type Newtype = __UntaggedFoo; + /** + * Comment for Unit + */ export type Unit = null; } - + + /** + * Comment for Untagged + */ export type Untagged = { x: string; y: number } | {} | [number, string] | [] | Foo | null;"# } }; @@ -229,30 +410,65 @@ fn test_untagged_enum_with_namespace() { #[test] fn test_module_reimport_enum() { + /// Comment for Internal #[derive(Tsify)] #[tsify(namespace)] enum Internal { + /// Comment for Struct Struct { x: String, y: i32 }, + /// Comment for EmptyStruct EmptyStruct {}, + /// Comment for Tuple Tuple(i32, String), + /// Comment for EmptyTuple EmptyTuple(), + /// Comment for Newtype Newtype(Foo), + /// Comment for Newtype2 Newtype2(Foo), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __InternalFoo = Foo; + /** + * Comment for Internal + */ declare namespace Internal { + /** + * Comment for Struct + */ export type Struct = { Struct: { x: string; y: number } }; + /** + * Comment for EmptyStruct + */ export type EmptyStruct = { EmptyStruct: {} }; + /** + * Comment for Tuple + */ export type Tuple = { Tuple: [number, string] }; + /** + * Comment for EmptyTuple + */ export type EmptyTuple = { EmptyTuple: [] }; + /** + * Comment for Newtype + */ export type Newtype = { Newtype: __InternalFoo }; + /** + * Comment for Newtype2 + */ export type Newtype2 = { Newtype2: __InternalFoo }; + /** + * Comment for Unit + */ export type Unit = "Unit"; } + /** + * Comment for Internal + */ export type Internal = { Struct: { x: string; y: number } } | { EmptyStruct: {} } | { Tuple: [number, string] } | { EmptyTuple: [] } | { Newtype: Foo } | { Newtype2: Foo } | "Unit";"# }; @@ -261,28 +477,53 @@ fn test_module_reimport_enum() { #[test] fn test_module_template_enum() { + /// Comment for Test struct Test { + /// Comment for inner inner: T, } + /// Comment for Internal #[derive(Tsify)] #[tsify(namespace)] enum Internal { + /// Comment for Newtype Newtype(Test), + /// Comment for NewtypeF NewtypeF(Test), + /// Comment for NewtypeL NewtypeL(Test), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __InternalFoo = Foo; type __InternalTest = Test; + /** + * Comment for Internal + */ declare namespace Internal { + /** + * Comment for Newtype + */ export type Newtype = { Newtype: __InternalTest }; + /** + * Comment for NewtypeF + */ export type NewtypeF = { NewtypeF: __InternalTest<__InternalFoo> }; + /** + * Comment for NewtypeL + */ export type NewtypeL = { NewtypeL: __InternalTest<__InternalFoo> }; + /** + * Comment for Unit + */ export type Unit = "Unit"; } + /** + * Comment for Internal + */ export type Internal = { Newtype: Test } | { NewtypeF: Test } | { NewtypeL: Test } | "Unit";"# }; @@ -295,25 +536,42 @@ struct Test { #[test] fn test_module_template_enum_inner() { + /// Comment for Test struct Test { + /// Comment for inner inner: T, } + /// Comment for Internal #[derive(Tsify)] #[tsify(namespace)] enum Internal { + /// Comment for Newtype Newtype(Test), + /// Comment for Unit Unit, } let expected = indoc! {r#" type __InternalFoo = Foo; type __InternalTest = Test; + /** + * Comment for Internal + */ declare namespace Internal { + /** + * Comment for Newtype + */ export type Newtype = { Newtype: __InternalTest<__InternalFoo> }; + /** + * Comment for Unit + */ export type Unit = "Unit"; } - + + /** + * Comment for Internal + */ export type Internal = { Newtype: Test } | "Unit";"# }; diff --git a/tests/flatten.rs b/tests/flatten.rs index 38cede3..86300d3 100644 --- a/tests/flatten.rs +++ b/tests/flatten.rs @@ -6,23 +6,35 @@ use tsify::Tsify; #[test] fn test_flatten() { + /// Comment for A #[derive(Tsify)] struct A { + /// Comment for a a: i32, + /// Comment for b b: String, } + /// Comment for B #[derive(Tsify)] struct B { + /// Comment for extra #[serde(flatten)] extra: A, + /// Comment for c c: i32, } assert_eq!( B::DECL, indoc! {" + /** + * Comment for B + */ export interface B extends A { + /** + * Comment for c + */ c: number; }" } @@ -31,22 +43,31 @@ fn test_flatten() { #[test] fn test_flatten_option() { + /// Comment for A #[derive(Tsify)] struct A { + /// Comment for a a: i32, + /// Comment for b b: String, } + /// Comment for B #[derive(Tsify)] struct B { + /// Comment for extra #[serde(flatten)] extra: Option, + /// Comment for c c: i32, } assert_eq!( B::DECL, indoc! {" + /** + * Comment for B + */ export type B = { c: number } & (A | {});" } ); diff --git a/tests/generics.rs b/tests/generics.rs index 9a0213a..bbb1414 100644 --- a/tests/generics.rs +++ b/tests/generics.rs @@ -6,45 +6,75 @@ use tsify::Tsify; #[test] fn test_generic_struct() { + /// Comment for GenericStruct #[derive(Tsify)] pub struct GenericStruct<'a, A, B, C, D> { + /// Comment for a a: A, + /// Comment for b b: B, + /// Comment for c #[serde(skip)] c: &'a C, + /// Comment for d d: D, } assert_eq!( GenericStruct::<(), (), (), ()>::DECL, indoc! {" + /** + * Comment for GenericStruct + */ export interface GenericStruct { + /** + * Comment for a + */ a: A; + /** + * Comment for b + */ b: B; + /** + * Comment for d + */ d: D; }" } ); + /// Comment for GenericNewtype #[derive(Tsify)] pub struct GenericNewtype(T); assert_eq!( GenericNewtype::<()>::DECL, - "export type GenericNewtype = T;" + indoc! {" + /** + * Comment for GenericNewtype + */ + export type GenericNewtype = T;" + }, ); + /// Comment for GenericTuple #[derive(Tsify)] pub struct GenericTuple<'a, A, B, C, D>(A, #[serde(skip)] &'a B, C, D); assert_eq!( GenericTuple::<(), (), (), ()>::DECL, - "export type GenericTuple = [A, C, D];" + indoc! {" + /** + * Comment for GenericTuple + */ + export type GenericTuple = [A, C, D];" + ,} ); } #[test] fn test_generic_enum() { + /// Comment for GenericEnum #[derive(Tsify)] pub enum GenericEnum { Unit, @@ -54,6 +84,9 @@ fn test_generic_enum() { } let expected = indoc! {r#" + /** + * Comment for GenericEnum + */ export type GenericEnum = "Unit" | { NewType: T } | { Seq: [T, U] } | { Map: { x: T; y: U } };"# }; @@ -62,23 +95,46 @@ fn test_generic_enum() { #[test] fn test_generic_enum_with_namespace() { + /// Comment for GenericEnum #[derive(Tsify)] #[tsify(namespace)] pub enum GenericEnum { + /// Comment for Unit Unit, + /// Comment for NewType NewType(T), + /// Comment for Seq Seq(T, U), + /// Comment for Map Map { x: T, y: U }, } let expected = indoc! {r#" + /** + * Comment for GenericEnum + */ declare namespace GenericEnum { + /** + * Comment for Unit + */ export type Unit = "Unit"; + /** + * Comment for NewType + */ export type NewType = { NewType: T }; + /** + * Comment for Seq + */ export type Seq = { Seq: [T, U] }; + /** + * Comment for Map + */ export type Map = { Map: { x: T; y: U } }; } - + + /** + * Comment for GenericEnum + */ export type GenericEnum = "Unit" | { NewType: T } | { Seq: [T, U] } | { Map: { x: T; y: U } };"# }; diff --git a/tests/optional.rs b/tests/optional.rs index 8ea3182..2d91f69 100644 --- a/tests/optional.rs +++ b/tests/optional.rs @@ -6,23 +6,32 @@ use tsify::Tsify; #[test] fn test_optional() { + /// Comment for Optional #[derive(Tsify)] struct Optional { + /// Comment for a #[tsify(optional)] a: Option, + /// Comment for b #[serde(skip_serializing_if = "Option::is_none")] b: Option, + /// Comment for c #[serde(default)] c: i32, + /// Comment for d #[serde(default)] d: Option, } + /// Comment for OptionalAll #[derive(Tsify)] #[serde(default)] struct OptionalAll { + /// Comment for a a: i32, + /// Comment for b b: i32, + /// Comment for c c: Option, } @@ -30,10 +39,25 @@ fn test_optional() { assert_eq!( Optional::DECL, indoc! {" + /** + * Comment for Optional + */ export interface Optional { + /** + * Comment for a + */ a?: number; + /** + * Comment for b + */ b?: string; + /** + * Comment for c + */ c?: number; + /** + * Comment for d + */ d?: string | undefined; }" } @@ -41,9 +65,21 @@ fn test_optional() { assert_eq!( OptionalAll::DECL, indoc! {" + /** + * Comment for OptionalAll + */ export interface OptionalAll { + /** + * Comment for a + */ a?: number; + /** + * Comment for b + */ b?: number; + /** + * Comment for c + */ c?: number | undefined; }" } @@ -52,10 +88,25 @@ fn test_optional() { assert_eq!( Optional::DECL, indoc! {" + /** + * Comment for Optional + */ export interface Optional { + /** + * Comment for a + */ a?: number; + /** + * Comment for b + */ b?: string; + /** + * Comment for c + */ c?: number; + /** + * Comment for d + */ d?: string | null; }" } @@ -63,9 +114,21 @@ fn test_optional() { assert_eq!( OptionalAll::DECL, indoc! {" + /** + * Comment for OptionalAll + */ export interface OptionalAll { + /** + * Comment for a + */ a?: number; + /** + * Comment for b + */ b?: number; + /** + * Comment for c + */ c?: number | null; }" } diff --git a/tests/rename.rs b/tests/rename.rs index cb55db1..2626963 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -6,10 +6,13 @@ use tsify::Tsify; #[test] fn test_rename() { + /// Comment for RenamedStruct #[derive(Tsify)] struct RenamedStruct { + /// Comment for X #[serde(rename = "X")] x: i32, + /// Comment for Y #[serde(rename = "Y")] y: i32, } @@ -17,26 +20,43 @@ fn test_rename() { assert_eq!( RenamedStruct::DECL, indoc! {" + /** + * Comment for RenamedStruct + */ export interface RenamedStruct { + /** + * Comment for X + */ X: number; + /** + * Comment for Y + */ Y: number; }" } ); + /// Comment for RenamedEnum #[derive(Tsify)] enum RenamedEnum { + /// Comment for X #[serde(rename = "X")] A(bool), + /// Comment for Y #[serde(rename = "Y")] B(i64), + /// Comment for Z #[serde(rename = "Z")] C(String), + /// Comment for D #[serde(skip)] D(i32), } let expected = indoc! {r#" + /** + * Comment for RenamedEnum + */ export type RenamedEnum = { X: boolean } | { Y: number } | { Z: string };"# }; @@ -46,25 +66,30 @@ fn test_rename() { #[test] fn test_rename_all() { + /// Comment for Enum #[allow(clippy::enum_variant_names)] #[derive(Tsify)] #[serde(rename_all = "snake_case")] #[tsify(namespace)] enum Enum { + /// Comment for snake_case SnakeCase { foo: bool, foo_bar: bool, }, + /// Comment for camel_case #[serde(rename_all = "camelCase")] CamelCase { foo: bool, foo_bar: bool, }, + /// Comment for kebab_case #[serde(rename_all = "kebab-case")] KebabCase { foo: bool, foo_bar: bool, }, + /// Comment for screaming_snake_case #[serde(rename_all = "SCREAMING_SNAKE_CASE")] ScreamingSnakeCase { foo: bool, @@ -72,28 +97,52 @@ fn test_rename_all() { }, } + /// Comment for PascalCase #[derive(Tsify)] #[serde(rename_all = "PascalCase")] struct PascalCase { + /// Comment for Foo foo: bool, + /// Comment for FooBar foo_bar: bool, } + /// Comment for ScreamingKebab #[derive(Tsify)] #[serde(rename_all = "SCREAMING-KEBAB-CASE")] struct ScreamingKebab { + /// Comment for FOO foo: bool, + /// Comment for FOO-BAR foo_bar: bool, } let expected = indoc! {r#" + /** + * Comment for Enum + */ declare namespace Enum { + /** + * Comment for snake_case + */ export type snake_case = { snake_case: { foo: boolean; foo_bar: boolean } }; + /** + * Comment for camel_case + */ export type camel_case = { camel_case: { foo: boolean; fooBar: boolean } }; + /** + * Comment for kebab_case + */ export type kebab_case = { kebab_case: { foo: boolean; "foo-bar": boolean } }; + /** + * Comment for screaming_snake_case + */ export type screaming_snake_case = { screaming_snake_case: { FOO: boolean; FOO_BAR: boolean } }; } - + + /** + * Comment for Enum + */ export type Enum = { snake_case: { foo: boolean; foo_bar: boolean } } | { camel_case: { foo: boolean; fooBar: boolean } } | { kebab_case: { foo: boolean; "foo-bar": boolean } } | { screaming_snake_case: { FOO: boolean; FOO_BAR: boolean } };"# }; @@ -102,8 +151,17 @@ fn test_rename_all() { assert_eq!( PascalCase::DECL, indoc! {" + /** + * Comment for PascalCase + */ export interface PascalCase { + /** + * Comment for Foo + */ Foo: boolean; + /** + * Comment for FooBar + */ FooBar: boolean; }" } @@ -112,8 +170,17 @@ fn test_rename_all() { assert_eq!( ScreamingKebab::DECL, indoc! {r#" + /** + * Comment for ScreamingKebab + */ export interface ScreamingKebab { + /** + * Comment for FOO + */ FOO: boolean; + /** + * Comment for FOO-BAR + */ "FOO-BAR": boolean; }"# } diff --git a/tests/skip.rs b/tests/skip.rs index 5f02a74..2d65f50 100644 --- a/tests/skip.rs +++ b/tests/skip.rs @@ -6,13 +6,18 @@ use tsify::Tsify; #[test] fn test_skip() { + /// Comment for Struct #[derive(Tsify)] struct Struct { + /// Comment for a a: i32, + /// Comment for b #[serde(skip)] b: i32, + /// Comment for c #[serde(skip_serializing)] c: i32, + /// Comment for d #[serde(skip_deserializing)] d: i32, } @@ -20,34 +25,63 @@ fn test_skip() { assert_eq!( Struct::DECL, indoc! {" + /** + * Comment for Struct + */ export interface Struct { + /** + * Comment for a + */ a: number; }" } ); + /// Comment for Tuple #[derive(Tsify)] struct Tuple(#[serde(skip)] String, i32); - assert_eq!(Tuple::DECL, "export type Tuple = [number];"); + assert_eq!( + Tuple::DECL, + indoc! {" + /** + * Comment for Tuple + */ + export type Tuple = [number];" + } + ); + /// Comment for Enum #[derive(Tsify)] #[tsify(namespace)] enum Enum { + /// Comment for A #[serde(skip)] A, + /// Comment for B #[serde(skip_serializing)] B, + /// Comment for C #[serde(skip_deserializing)] C, + /// Comment for D D, } let expected = indoc! {r#" + /** + * Comment for Enum + */ declare namespace Enum { + /** + * Comment for D + */ export type D = "D"; } - + + /** + * Comment for Enum + */ export type Enum = "D";"# }; diff --git a/tests/struct.rs b/tests/struct.rs index a4479b4..abf7a49 100644 --- a/tests/struct.rs +++ b/tests/struct.rs @@ -8,35 +8,73 @@ use tsify::Tsify; #[test] fn test_unit() { + /// Comment for Unit #[derive(Tsify)] struct Unit; if cfg!(feature = "js") { - assert_eq!(Unit::DECL, "export type Unit = undefined;"); + assert_eq!( + Unit::DECL, + indoc! {" + /** + * Comment for Unit + */ + export type Unit = undefined;" + } + ); } else { - assert_eq!(Unit::DECL, "export type Unit = null;"); + assert_eq!( + Unit::DECL, + indoc! {" + /** + * Comment for Unit + */ + export type Unit = null;" + } + ); }; } #[test] fn test_named_fields() { + /// Comment for Struct #[derive(Tsify)] struct A { + /// Comment for a a: (usize, u64), + /// Comment for b b: HashMap, } let expected = if cfg!(feature = "js") { indoc! {" + /** + * Comment for Struct + */ export interface A { + /** + * Comment for a + */ a: [number, number]; + /** + * Comment for b + */ b: Map; }" } } else { indoc! {" + /** + * Comment for Struct + */ export interface A { + /** + * Comment for a + */ a: [number, number]; + /** + * Comment for b + */ b: Record; }" } @@ -47,39 +85,76 @@ fn test_named_fields() { #[test] fn test_newtype_struct() { + /// Comment for Newtype #[derive(Tsify)] struct Newtype(i32); - assert_eq!(Newtype::DECL, "export type Newtype = number;"); + assert_eq!( + Newtype::DECL, + indoc! {" + /** + * Comment for Newtype + */ + export type Newtype = number;" + } + ); } #[test] fn test_tuple_struct() { + /// Comment for Tuple #[derive(Tsify)] struct Tuple(i32, String); + /// Comment for EmptyTuple #[derive(Tsify)] struct EmptyTuple(); - assert_eq!(Tuple::DECL, "export type Tuple = [number, string];"); - assert_eq!(EmptyTuple::DECL, "export type EmptyTuple = [];"); + assert_eq!( + Tuple::DECL, + indoc! {" + /** + * Comment for Tuple + */ + export type Tuple = [number, string];" + } + ); + assert_eq!( + EmptyTuple::DECL, + indoc! {" + /** + * Comment for EmptyTuple + */ + export type EmptyTuple = [];" + } + ); } #[test] fn test_nested_struct() { + /// Comment for A #[derive(Tsify)] struct A { + /// Comment for x x: f64, } + /// Comment for B #[derive(Tsify)] struct B { + /// Comment for a a: A, } assert_eq!( B::DECL, indoc! {" + /** + * Comment for B + */ export interface B { + /** + * Comment for a + */ a: A; }" } @@ -90,17 +165,29 @@ fn test_nested_struct() { fn test_struct_with_borrowed_fields() { use std::borrow::Cow; + /// Comment for Borrow #[derive(Tsify)] struct Borrow<'a> { + /// Comment for raw raw: &'a str, + /// Comment for cow cow: Cow<'a, str>, } assert_eq!( Borrow::DECL, indoc! {" + /** + * Comment for Borrow + */ export interface Borrow { + /** + * Comment for raw + */ raw: string; + /** + * Comment for cow + */ cow: string; }" } @@ -109,19 +196,31 @@ fn test_struct_with_borrowed_fields() { #[test] fn test_tagged_struct() { + /// Comment for TaggedStruct #[derive(Tsify)] #[serde(tag = "type")] struct TaggedStruct { + /// Comment for x x: i32, + /// Comment for y y: i32, } assert_eq!( TaggedStruct::DECL, indoc! {r#" + /** + * Comment for TaggedStruct + */ export interface TaggedStruct { type: "TaggedStruct"; + /** + * Comment for x + */ x: number; + /** + * Comment for y + */ y: number; }"# } diff --git a/tests/transparent.rs b/tests/transparent.rs index 01f5915..4667a43 100644 --- a/tests/transparent.rs +++ b/tests/transparent.rs @@ -1,22 +1,43 @@ #![allow(dead_code)] +use indoc::indoc; use pretty_assertions::assert_eq; use tsify::Tsify; #[test] fn test_transparent() { + /// Comment for A #[derive(Tsify)] #[serde(transparent)] struct A(String, #[serde(skip)] f64); + /// Comment for B #[derive(Tsify)] #[serde(transparent)] struct B { + /// Comment for x #[serde(skip)] x: String, + /// Comment for y y: f64, } - assert_eq!("export type A = string;", A::DECL); - assert_eq!("export type B = number;", B::DECL); + assert_eq!( + A::DECL, + indoc! {" + /** + * Comment for A + */ + export type A = string;" + } + ); + assert_eq!( + B::DECL, + indoc! {" + /** + * Comment for B + */ + export type B = number;" + } + ); } diff --git a/tests/type_override.rs b/tests/type_override.rs index 6058899..4cbee82 100644 --- a/tests/type_override.rs +++ b/tests/type_override.rs @@ -8,50 +8,84 @@ struct Unsupported; #[test] fn test_struct_with_type_override() { + /// Comment for Struct #[derive(Tsify)] struct Struct { + /// Comment for a a: i32, + /// Comment for b #[tsify(type = "0 | 1 | 2")] b: i32, + /// Comment for c #[tsify(type = "string | null")] c: Unsupported, } + /// Comment for Newtype #[derive(Tsify)] struct Newtype(#[tsify(type = "string | null")] Unsupported); assert_eq!( Struct::DECL, indoc! {r#" + /** + * Comment for Struct + */ export interface Struct { + /** + * Comment for a + */ a: number; + /** + * Comment for b + */ b: 0 | 1 | 2; + /** + * Comment for c + */ c: string | null; }"# } ); - assert_eq!(Newtype::DECL, "export type Newtype = string | null;"); + assert_eq!( + Newtype::DECL, + indoc! {" + /** + * Comment for Newtype + */ + export type Newtype = string | null;" + } + ); } #[test] fn test_enum_with_type_override() { + /// Comment for Enum #[derive(Tsify)] enum Enum { + /// Comment for Struct Struct { + /// Comment for x #[tsify(type = "`tpl_lit_${string}`")] x: String, + /// Comment for y #[tsify(type = "0 | 1 | 2")] y: i32, }, + /// Comment for Tuple Tuple( #[tsify(type = "`tpl_lit_${string}`")] String, #[tsify(type = "0 | 1 | 2")] i32, ), + /// Comment for Newtype Newtype(#[tsify(type = "number")] Unsupported), } let expected = indoc! {r#" + /** + * Comment for Enum + */ export type Enum = { Struct: { x: `tpl_lit_${string}`; y: 0 | 1 | 2 } } | { Tuple: [`tpl_lit_${string}`, 0 | 1 | 2] } | { Newtype: number };"# }; @@ -60,14 +94,22 @@ fn test_enum_with_type_override() { #[test] fn test_generic_struct_with_type_override() { + /// Comment for Foo #[derive(Tsify)] pub struct Foo { + /// Comment for bar #[tsify(type = "[T, ...T[]]")] bar: Vec, } let expected = indoc! {r#" + /** + * Comment for Foo + */ export interface Foo { + /** + * Comment for bar + */ bar: [T, ...T[]]; }"# }; From e36e55bcf3c9ac7c1d8185e5ad994885f4a2eb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 14 Aug 2023 13:54:17 +0200 Subject: [PATCH 05/15] Clean up, reduce repetition when writing comments --- tsify-macros/src/comments.rs | 11 +++++++++-- tsify-macros/src/decl.rs | 17 ++++------------- tsify-macros/src/typescript.rs | 6 ++---- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/tsify-macros/src/comments.rs b/tsify-macros/src/comments.rs index e9aede3..1364bd6 100644 --- a/tsify-macros/src/comments.rs +++ b/tsify-macros/src/comments.rs @@ -48,14 +48,21 @@ pub fn extract_doc_comments(attrs: &[syn::Attribute]) -> Vec { }) } -pub fn format_doc_comments(comments: &Vec) -> String { +pub fn write_doc_comments( + f: &mut std::fmt::Formatter<'_>, + comments: &Vec, +) -> Result<(), std::fmt::Error> { + if comments.is_empty() { + return Ok(()); + } + let comment = comments .iter() .map(|line| format!(" *{}\n", line.trim_matches('"'))) .collect::>() .join(""); - format!("/**\n{} */\n", comment) + write!(f, "{}", format!("/**\n{} */\n", comment)) } pub fn clean_comments(typ: &mut TsType) -> () { diff --git a/tsify-macros/src/decl.rs b/tsify-macros/src/decl.rs index 5984585..6d1b990 100644 --- a/tsify-macros/src/decl.rs +++ b/tsify-macros/src/decl.rs @@ -3,7 +3,7 @@ use std::{fmt::Display, vec}; use crate::comments::clean_comments; use crate::{ - comments::format_doc_comments, + comments::write_doc_comments, typescript::{TsType, TsTypeElement, TsTypeLit}, }; @@ -36,9 +36,7 @@ impl Display for TsTypeAliasDecl { format!("{}<{}>", self.id, type_params) }; - if !self.comments.is_empty() { - write!(f, "{}", format_doc_comments(&self.comments))?; - } + write_doc_comments(f, &self.comments)?; if self.export { write!(f, "export ")?; @@ -57,9 +55,7 @@ pub struct TsInterfaceDecl { impl Display for TsInterfaceDecl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if !self.comments.is_empty() { - write!(f, "{}", format_doc_comments(&self.comments))?; - } + write_doc_comments(f, &self.comments)?; write!(f, "export interface {}", self.id)?; @@ -226,9 +222,7 @@ impl Display for TsEnumDecl { writeln!(f, "{}", type_ref)?; } - if !self.comments.is_empty() { - write!(f, "{}", format_doc_comments(&self.comments))?; - } + write_doc_comments(f, &self.comments)?; write!(f, "declare namespace {}", self.id)?; @@ -267,9 +261,6 @@ impl Display for TsEnumDecl { self.members .iter() .map(|member| { - // let mut type_refs = Vec::new(); - // TsEnumDecl::replace_type_params(member.type_ann.clone(), &mut type_refs) - let mut clone = member.type_ann.clone(); clean_comments(&mut clone); clone diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index 5518b0a..4b0a938 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, fmt::Display}; use serde_derive_internals::{ast::Style, attr::TagType}; -use crate::comments::format_doc_comments; +use crate::comments::write_doc_comments; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TsKeywordTypeKind { @@ -606,9 +606,7 @@ impl Display for TsTypeElement { let optional_ann = if self.optional { "?" } else { "" }; - if !self.comments.is_empty() { - write!(f, "{}", format_doc_comments(&self.comments))?; - } + write_doc_comments(f, &self.comments)?; if is_js_ident(key) { write!(f, "{key}{optional_ann}: {type_ann}") From 2b694aba774d831a675a941e6567efb967d843bf Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 13 Oct 2023 10:40:53 -0700 Subject: [PATCH 06/15] update is_js_ident to cover most cases --- tests/rename.rs | 57 ++++++++++++++++++++++++++++++++++ tsify-macros/src/typescript.rs | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/rename.rs b/tests/rename.rs index cb55db1..ddd27c8 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -119,3 +119,60 @@ fn test_rename_all() { } ); } + +#[test] +fn test_quote_non_identifiers() { + #[derive(Tsify)] + struct NonIdentifierRenameStruct { + #[serde(rename = "1")] + x: i32, + #[serde(rename = "1x")] + y: i32, + #[serde(rename = "-")] + z: i32, + #[serde(rename = " ")] + w: i32, + #[serde(rename = "#")] + q: i32, + #[serde(rename = "should_not_quote")] + p: i32, + #[serde(rename = "should$not$quote")] + r: i32, + } + + assert_eq!( + NonIdentifierRenameStruct::DECL, + indoc! {" + export interface NonIdentifierRenameStruct { + \"1\": number; + \"1x\": number; + \"-\": number; + \" \": number; + \"#\": number; + should_not_quote: number; + should$not$quote: number; + }" + } + ); + + #[derive(Tsify)] + enum NonIdentifierRenameEnum { + #[serde(rename = "hello-world")] + A(bool), + #[serde(rename = "hel#&*world")] + B(i64), + #[serde(rename = "hello world")] + C(String), + #[serde(rename = "")] + D(i32), + #[serde(rename = "should_not_quote")] + E(String), + } + + let expected = indoc! {r#" + export type NonIdentifierRenameEnum = { "hello-world": boolean } | { "hel#&*world": number } | { "hello world": string } | { "": number } | { should_not_quote: string };"# + + }; + + assert_eq!(NonIdentifierRenameEnum::DECL, expected); +} \ No newline at end of file diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index 25fd6ae..00183ef 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -575,7 +575,7 @@ fn parse_len(expr: &syn::Expr) -> Option { } fn is_js_ident(string: &str) -> bool { - !string.contains('-') + !string.is_empty() && !string.starts_with(|c: char| c.is_ascii_digit()) && !string.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '$') } impl Display for TsTypeElement { From 3e81856908e8149a856082d194a55ecb402ff7d8 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Thu, 11 Jan 2024 17:23:56 -0500 Subject: [PATCH 07/15] Ensure type names ignore serde rename when exporting --- tsify-macros/src/container.rs | 4 ++++ tsify-macros/src/decl.rs | 4 +++- tsify-macros/src/parser.rs | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tsify-macros/src/container.rs b/tsify-macros/src/container.rs index fb44844..b8395f0 100644 --- a/tsify-macros/src/container.rs +++ b/tsify-macros/src/container.rs @@ -47,6 +47,10 @@ impl<'a> Container<'a> { &self.serde_container.ident } + pub fn ident_str(&self) -> String { + self.ident().to_string() + } + #[inline] pub fn serde_attrs(&self) -> &attr::Container { &self.serde_container.attrs diff --git a/tsify-macros/src/decl.rs b/tsify-macros/src/decl.rs index 9be3a9e..4bd5f49 100644 --- a/tsify-macros/src/decl.rs +++ b/tsify-macros/src/decl.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use crate::typescript::{TsType, TsTypeElement, TsTypeLit}; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct TsTypeAliasDecl { pub id: String, pub export: bool, @@ -27,6 +27,7 @@ impl Display for TsTypeAliasDecl { } } +#[derive(Debug)] pub struct TsInterfaceDecl { pub id: String, pub type_params: Vec, @@ -69,6 +70,7 @@ impl Display for TsInterfaceDecl { } } +#[derive(Debug)] pub struct TsEnumDecl { pub id: String, pub type_params: Vec, diff --git a/tsify-macros/src/parser.rs b/tsify-macros/src/parser.rs index 66c6c2d..08567ee 100644 --- a/tsify-macros/src/parser.rs +++ b/tsify-macros/src/parser.rs @@ -70,7 +70,7 @@ impl<'a> Parser<'a> { fn create_type_alias_decl(&self, type_ann: TsType) -> Decl { Decl::TsTypeAlias(TsTypeAliasDecl { - id: self.container.name(), + id: self.container.ident_str(), export: true, type_params: self.create_relevant_type_params(type_ann.type_ref_names()), type_ann, @@ -91,7 +91,7 @@ impl<'a> Parser<'a> { let type_params = self.create_relevant_type_params(type_ref_names); Decl::TsInterface(TsInterfaceDecl { - id: self.container.name(), + id: self.container.ident_str(), type_params, extends, body: members, @@ -264,7 +264,7 @@ impl<'a> Parser<'a> { let relevant_type_params = self.create_relevant_type_params(type_ref_names); Decl::TsEnum(TsEnumDecl { - id: self.container.name(), + id: self.container.ident_str(), type_params: relevant_type_params, members, namespace: self.container.attrs.namespace, From 605aa840e11ec7d1eb16f21d579ce4bdb1dbb008 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Sat, 13 Jan 2024 15:41:22 -0500 Subject: [PATCH 08/15] Add Github Actions CI (#2) Add CI via Github Actions --- .github/workflows/on-pull-request.yml | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/on-pull-request.yml diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml new file mode 100644 index 0000000..43da5a7 --- /dev/null +++ b/.github/workflows/on-pull-request.yml @@ -0,0 +1,77 @@ +name: Build and Test + +on: + push: + branches: [main, next] + pull_request: + branches: ["*"] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + + - name: Add cargo-expand + run: cargo install cargo-expand + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check From 0c703edf67b4a7b5ddbdd01c181d19cefa5ae235 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Sat, 13 Jan 2024 15:55:04 -0500 Subject: [PATCH 09/15] Fix formatting --- tests/rename.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/rename.rs b/tests/rename.rs index 2626963..1a343ee 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -73,28 +73,16 @@ fn test_rename_all() { #[tsify(namespace)] enum Enum { /// Comment for snake_case - SnakeCase { - foo: bool, - foo_bar: bool, - }, + SnakeCase { foo: bool, foo_bar: bool }, /// Comment for camel_case #[serde(rename_all = "camelCase")] - CamelCase { - foo: bool, - foo_bar: bool, - }, + CamelCase { foo: bool, foo_bar: bool }, /// Comment for kebab_case #[serde(rename_all = "kebab-case")] - KebabCase { - foo: bool, - foo_bar: bool, - }, + KebabCase { foo: bool, foo_bar: bool }, /// Comment for screaming_snake_case #[serde(rename_all = "SCREAMING_SNAKE_CASE")] - ScreamingSnakeCase { - foo: bool, - foo_bar: bool, - }, + ScreamingSnakeCase { foo: bool, foo_bar: bool }, } /// Comment for PascalCase From 8cf9ae31ea03db12c105072b822c19a3e54fa92e Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Sat, 13 Jan 2024 15:59:02 -0500 Subject: [PATCH 10/15] Formatting --- tests/rename.rs | 2 +- tsify-macros/src/typescript.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/rename.rs b/tests/rename.rs index b29342c..0027824 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -230,4 +230,4 @@ fn test_quote_non_identifiers() { }; assert_eq!(NonIdentifierRenameEnum::DECL, expected); -} \ No newline at end of file +} diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index ef1bc82..dbfb171 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -585,7 +585,9 @@ fn parse_len(expr: &syn::Expr) -> Option { } fn is_js_ident(string: &str) -> bool { - !string.is_empty() && !string.starts_with(|c: char| c.is_ascii_digit()) && !string.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '$') + !string.is_empty() + && !string.starts_with(|c: char| c.is_ascii_digit()) + && !string.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '$') } impl TsTypeElement { From c645c3a6ba5717929de7ed9158dd2161e73c556b Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Sat, 13 Jan 2024 19:37:16 -0500 Subject: [PATCH 11/15] Added reference test --- tests/reference_rename.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/reference_rename.rs diff --git a/tests/reference_rename.rs b/tests/reference_rename.rs new file mode 100644 index 0000000..0f641f0 --- /dev/null +++ b/tests/reference_rename.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] + +use indoc::indoc; +use pretty_assertions::assert_eq; +use tsify::Tsify; + +#[test] +fn test_reference_rename() { + #[derive(Tsify)] + #[serde(rename = "foo")] + pub struct Foo { + x: i32, + } + + #[derive(Tsify)] + pub struct Bar { + foo: Foo, + } + + assert_eq!( + Bar::DECL, + indoc! {" + export interface Bar { + foo: Foo; + }" + } + ); + assert_eq!( + Foo::DECL, + indoc! {" + export interface Foo { + x: number; + }" + } + ); +} From 74768a6664af392a7525b39db109f2a1b1ff5a24 Mon Sep 17 00:00:00 2001 From: Carl Sverre <82591+carlsverre@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:27:09 -0800 Subject: [PATCH 12/15] update serde_derive_internal and test rename_all_fields --- tests/enum.rs | 16 ++++++++++++++++ tsify-macros/Cargo.toml | 9 +++++++-- tsify-macros/src/container.rs | 2 +- tsify-macros/src/parser.rs | 6 +++--- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/enum.rs b/tests/enum.rs index ef9c529..26653fe 100644 --- a/tests/enum.rs +++ b/tests/enum.rs @@ -408,6 +408,22 @@ fn test_untagged_enum_with_namespace() { assert_eq!(Untagged::DECL, expected); } +#[test] +fn test_renamed_enum() { + #[derive(Tsify)] + #[serde(rename_all_fields = "camelCase")] + enum Renamed { + First { foo_bar: String, baz_quoox: i32 }, + Second { asdf_asdf: String, qwer_qwer: i32 }, + } + + let expected = indoc! {r#" + export type Renamed = { First: { fooBar: string; bazQuoox: number } } | { Second: { asdfAsdf: string; qwerQwer: number } };"# + }; + + assert_eq!(Renamed::DECL, expected); +} + #[test] fn test_module_reimport_enum() { /// Comment for Internal diff --git a/tsify-macros/Cargo.toml b/tsify-macros/Cargo.toml index 321f868..e738085 100644 --- a/tsify-macros/Cargo.toml +++ b/tsify-macros/Cargo.toml @@ -16,8 +16,13 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "2.0", default-features = false, features = ["full", "parsing", "printing", "proc-macro"] } -serde_derive_internals = "0.28" +syn = { version = "2.0", default-features = false, features = [ + "full", + "parsing", + "printing", + "proc-macro", +] } +serde_derive_internals = "0.29" [features] wasm-bindgen = [] diff --git a/tsify-macros/src/container.rs b/tsify-macros/src/container.rs index b8395f0..d694808 100644 --- a/tsify-macros/src/container.rs +++ b/tsify-macros/src/container.rs @@ -61,7 +61,7 @@ impl<'a> Container<'a> { } pub fn name(&self) -> String { - self.serde_attrs().name().serialize_name() + self.serde_attrs().name().serialize_name().to_owned() } pub fn generics(&self) -> &syn::Generics { diff --git a/tsify-macros/src/parser.rs b/tsify-macros/src/parser.rs index e19147e..5cb246d 100644 --- a/tsify-macros/src/parser.rs +++ b/tsify-macros/src/parser.rs @@ -212,7 +212,7 @@ impl<'a> Parser<'a> { let members = members .into_iter() .map(|field| { - let key = field.attrs.name().serialize_name(); + let key = field.attrs.name().serialize_name().to_owned(); let (type_ann, field_attrs) = self.parse_field(field); let optional = field_attrs.map_or(false, |attrs| attrs.optional); @@ -254,7 +254,7 @@ impl<'a> Parser<'a> { .map(|variant| { let decl = self.create_type_alias_decl(self.parse_variant(variant)); if let Decl::TsTypeAlias(mut type_alias) = decl { - type_alias.id = variant.attrs.name().serialize_name(); + type_alias.id = variant.attrs.name().serialize_name().to_owned(); type_alias.comments = extract_doc_comments(&variant.original.attrs); type_alias @@ -282,7 +282,7 @@ impl<'a> Parser<'a> { fn parse_variant(&self, variant: &Variant) -> TsType { let tag_type = self.container.serde_attrs().tag(); - let name = variant.attrs.name().serialize_name(); + let name = variant.attrs.name().serialize_name().to_owned(); let style = variant.style; let type_ann: TsType = self.parse_fields(style, &variant.fields).into(); type_ann.with_tag_type(name, style, tag_type) From 71de564f29263d73b09240ba9fb8dd432cfcd1bf Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Thu, 25 Jan 2024 19:03:04 -0500 Subject: [PATCH 13/15] Support Serialization and Prefixes/Affixes on Types Co-authored-by: Pantamis --- src/lib.rs | 20 +- tests/affixes.rs | 116 ++++++++++++ tests/expand/borrow.expanded.rs | 5 + tests/expand/generic_enum.expanded.rs | 5 + tests/expand/generic_struct.expanded.rs | 10 + tests/options.rs | 56 ++++++ tsify-macros/src/attrs.rs | 85 ++++++++- tsify-macros/src/container.rs | 22 ++- tsify-macros/src/decl.rs | 11 +- tsify-macros/src/derive.rs | 1 + tsify-macros/src/parser.rs | 12 +- tsify-macros/src/type_alias.rs | 5 +- tsify-macros/src/typescript.rs | 232 +++++++++++++++--------- tsify-macros/src/wasm_bindgen.rs | 9 + 14 files changed, 486 insertions(+), 103 deletions(-) create mode 100644 tests/affixes.rs create mode 100644 tests/options.rs diff --git a/src/lib.rs b/src/lib.rs index b865f82..de9b26d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,15 +2,28 @@ #[cfg(all(feature = "json", not(feature = "js")))] pub use gloo_utils::format::JsValueSerdeExt; +#[cfg(feature = "js")] +pub use serde_wasm_bindgen; pub use tsify_macros::*; #[cfg(feature = "wasm-bindgen")] use wasm_bindgen::{JsCast, JsValue}; +pub struct SerializationConfig { + pub missing_as_null: bool, + pub hashmap_as_object: bool, + pub large_number_types_as_bigints: bool, +} + pub trait Tsify { #[cfg(feature = "wasm-bindgen")] type JsType: JsCast; const DECL: &'static str; + const SERIALIZATION_CONFIG: SerializationConfig = SerializationConfig { + missing_as_null: false, + hashmap_as_object: false, + large_number_types_as_bigints: false, + }; #[cfg(all(feature = "json", not(feature = "js")))] #[inline] @@ -36,7 +49,12 @@ pub trait Tsify { where Self: serde::Serialize, { - serde_wasm_bindgen::to_value(self).map(JsCast::unchecked_from_js) + let config = ::SERIALIZATION_CONFIG; + let serializer = serde_wasm_bindgen::Serializer::new() + .serialize_missing_as_null(config.missing_as_null) + .serialize_maps_as_objects(config.hashmap_as_object) + .serialize_large_number_types_as_bigints(config.large_number_types_as_bigints); + self.serialize(&serializer).map(JsCast::unchecked_from_js) } #[cfg(feature = "js")] diff --git a/tests/affixes.rs b/tests/affixes.rs new file mode 100644 index 0000000..2e94182 --- /dev/null +++ b/tests/affixes.rs @@ -0,0 +1,116 @@ +#![allow(dead_code)] + +use indoc::indoc; +use pretty_assertions::assert_eq; +use tsify::Tsify; + +#[test] +fn test_prefix() { + type MyType = u32; + + #[derive(Tsify)] + #[tsify(type_prefix = "Special")] + struct PrefixedStruct { + // Make sure that prefix isn't applied to builtin types + x: u32, + y: MyType, + } + + assert_eq!( + PrefixedStruct::DECL, + indoc! {" + export interface SpecialPrefixedStruct { + x: number; + y: SpecialMyType; + }" + } + ); + + #[derive(Tsify)] + #[tsify(type_prefix = "Special")] + enum PrefixedEnum { + VariantA(MyType), + VariantB(u32), + } + + assert_eq!( + PrefixedEnum::DECL, + indoc! {" + export type SpecialPrefixedEnum = { VariantA: SpecialMyType } | { VariantB: number };" + } + ); +} + +#[test] +fn test_suffix() { + type MyType = u32; + + #[derive(Tsify)] + #[tsify(type_suffix = "Special")] + struct SuffixedStruct { + // Make sure that prefix isn't applied to builtin types + x: u32, + y: MyType, + } + + assert_eq!( + SuffixedStruct::DECL, + indoc! {" + export interface SuffixedStructSpecial { + x: number; + y: MyTypeSpecial; + }" + } + ); + + #[derive(Tsify)] + #[tsify(type_suffix = "Special")] + enum SuffixedEnum { + VariantA(MyType), + VariantB(u32), + } + + assert_eq!( + SuffixedEnum::DECL, + indoc! {" + export type SuffixedEnumSpecial = { VariantA: MyTypeSpecial } | { VariantB: number };" + } + ); +} + +#[test] +fn test_prefix_suffix() { + type MyType = u32; + + #[derive(Tsify)] + #[tsify(type_prefix = "Pre", type_suffix = "Suf")] + struct DoubleAffixedStruct { + // Make sure that prefix isn't applied to builtin types + x: u32, + y: MyType, + } + + assert_eq!( + DoubleAffixedStruct::DECL, + indoc! {" + export interface PreDoubleAffixedStructSuf { + x: number; + y: PreMyTypeSuf; + }" + } + ); + + #[derive(Tsify)] + #[tsify(type_prefix = "Pre", type_suffix = "Suf")] + enum DoubleAffixedEnum { + VariantA(MyType), + VariantB(u32), + } + + assert_eq!( + DoubleAffixedEnum::DECL, + indoc! {" + export type PreDoubleAffixedEnumSuf = { VariantA: PreMyTypeSuf } | { VariantB: number };" + } + ); +} diff --git a/tests/expand/borrow.expanded.rs b/tests/expand/borrow.expanded.rs index b406e74..2d7bfe3 100644 --- a/tests/expand/borrow.expanded.rs +++ b/tests/expand/borrow.expanded.rs @@ -24,6 +24,11 @@ const _: () = { impl<'a> Tsify for Borrow<'a> { type JsType = JsType; const DECL: &'static str = "export interface Borrow {\n raw: string;\n cow: string;\n}"; + const SERIALIZATION_CONFIG: tsify::SerializationConfig = tsify::SerializationConfig { + missing_as_null: false, + hashmap_as_object: false, + large_number_types_as_bigints: false, + }; } #[wasm_bindgen(typescript_custom_section)] const TS_APPEND_CONTENT: &'static str = "export interface Borrow {\n raw: string;\n cow: string;\n}"; diff --git a/tests/expand/generic_enum.expanded.rs b/tests/expand/generic_enum.expanded.rs index 9346411..492deeb 100644 --- a/tests/expand/generic_enum.expanded.rs +++ b/tests/expand/generic_enum.expanded.rs @@ -25,6 +25,11 @@ const _: () = { impl Tsify for GenericEnum { type JsType = JsType; const DECL: &'static str = "export type GenericEnum = \"Unit\" | { NewType: T } | { Seq: [T, U] } | { Map: { x: T; y: U } };"; + const SERIALIZATION_CONFIG: tsify::SerializationConfig = tsify::SerializationConfig { + missing_as_null: false, + hashmap_as_object: false, + large_number_types_as_bigints: false, + }; } #[wasm_bindgen(typescript_custom_section)] const TS_APPEND_CONTENT: &'static str = "export type GenericEnum = \"Unit\" | { NewType: T } | { Seq: [T, U] } | { Map: { x: T; y: U } };"; diff --git a/tests/expand/generic_struct.expanded.rs b/tests/expand/generic_struct.expanded.rs index 2287516..c82624a 100644 --- a/tests/expand/generic_struct.expanded.rs +++ b/tests/expand/generic_struct.expanded.rs @@ -22,6 +22,11 @@ const _: () = { impl Tsify for GenericStruct { type JsType = JsType; const DECL: &'static str = "export interface GenericStruct {\n x: T;\n}"; + const SERIALIZATION_CONFIG: tsify::SerializationConfig = tsify::SerializationConfig { + missing_as_null: false, + hashmap_as_object: false, + large_number_types_as_bigints: false, + }; } #[wasm_bindgen(typescript_custom_section)] const TS_APPEND_CONTENT: &'static str = "export interface GenericStruct {\n x: T;\n}"; @@ -112,6 +117,11 @@ const _: () = { impl Tsify for GenericNewtype { type JsType = JsType; const DECL: &'static str = "export type GenericNewtype = T;"; + const SERIALIZATION_CONFIG: tsify::SerializationConfig = tsify::SerializationConfig { + missing_as_null: false, + hashmap_as_object: false, + large_number_types_as_bigints: false, + }; } #[wasm_bindgen(typescript_custom_section)] const TS_APPEND_CONTENT: &'static str = "export type GenericNewtype = T;"; diff --git a/tests/options.rs b/tests/options.rs new file mode 100644 index 0000000..284ef09 --- /dev/null +++ b/tests/options.rs @@ -0,0 +1,56 @@ +#![cfg(feature = "js")] +#![allow(dead_code)] + +use std::collections::HashMap; + +use indoc::indoc; +use pretty_assertions::assert_eq; +use tsify::Tsify; + +#[test] +fn test_transparent() { + #[derive(Tsify)] + #[tsify(missing_as_null)] + struct Optional { + a: Option, + } + + assert_eq!( + Optional::DECL, + indoc! {" + export interface Optional { + a: number | null; + }" + } + ); + + #[derive(Tsify)] + #[tsify(hashmap_as_object)] + struct MapWrap { + a: HashMap, + } + + assert_eq!( + MapWrap::DECL, + indoc! {" + export interface MapWrap { + a: Record; + }" + } + ); + + #[derive(Tsify)] + #[tsify(large_number_types_as_bigints)] + struct BigNumber { + a: u64, + } + + assert_eq!( + BigNumber::DECL, + indoc! {" + export interface BigNumber { + a: bigint; + }" + } + ) +} diff --git a/tsify-macros/src/attrs.rs b/tsify-macros/src/attrs.rs index 34e0c1b..c67139f 100644 --- a/tsify-macros/src/attrs.rs +++ b/tsify-macros/src/attrs.rs @@ -3,19 +3,41 @@ use serde_derive_internals::ast::Field; use crate::comments::extract_doc_comments; #[derive(Debug, Default)] -pub struct TsifyContainerAttars { +pub struct TsifyContainerAttrs { pub into_wasm_abi: bool, pub from_wasm_abi: bool, pub namespace: bool, + pub ty_config: TypeGenerationConfig, pub comments: Vec, } -impl TsifyContainerAttars { +#[derive(Debug, Default)] +pub struct TypeGenerationConfig { + pub type_prefix: Option, + pub type_suffix: Option, + pub missing_as_null: bool, + pub hashmap_as_object: bool, + pub large_number_types_as_bigints: bool, +} +impl TypeGenerationConfig { + pub fn format_name(&self, mut name: String) -> String { + if let Some(ref prefix) = self.type_prefix { + name.insert_str(0, prefix); + } + if let Some(ref suffix) = self.type_suffix { + name.push_str(suffix); + } + name + } +} + +impl TsifyContainerAttrs { pub fn from_derive_input(input: &syn::DeriveInput) -> syn::Result { let mut attrs = Self { into_wasm_abi: false, from_wasm_abi: false, namespace: false, + ty_config: TypeGenerationConfig::default(), comments: extract_doc_comments(&input.attrs), }; @@ -52,7 +74,64 @@ impl TsifyContainerAttars { return Ok(()); } - Err(meta.error("unsupported tsify attribute, expected one of `into_wasm_abi`, `from_wasm_abi`, `namespace`")) + if meta.path.is_ident("type_prefix") { + if attrs.ty_config.type_prefix.is_some() { + return Err(meta.error("duplicate attribute")); + } + let lit: syn::LitStr = meta.value()?.parse()?; + attrs.ty_config.type_prefix = Some(lit.value()); + return Ok(()); + } + + if meta.path.is_ident("type_suffix") { + if attrs.ty_config.type_suffix.is_some() { + return Err(meta.error("duplicate attribute")); + } + let lit: syn::LitStr = meta.value()?.parse()?; + attrs.ty_config.type_suffix = Some(lit.value()); + return Ok(()); + } + + if meta.path.is_ident("missing_as_null") { + if attrs.ty_config.missing_as_null { + return Err(meta.error("duplicate attribute")); + } + if cfg!(not(feature = "js")) { + return Err(meta.error( + "#[tsify(missing_as_null)] requires the `js` feature", + )); + } + attrs.ty_config.missing_as_null = true; + return Ok(()); + } + + if meta.path.is_ident("hashmap_as_object") { + if attrs.ty_config.hashmap_as_object { + return Err(meta.error("duplicate attribute")); + } + if cfg!(not(feature = "js")) { + return Err(meta.error( + "#[tsify(hashmap_as_object)] requires the `js` feature", + )); + } + attrs.ty_config.hashmap_as_object = true; + return Ok(()); + } + + if meta.path.is_ident("large_number_types_as_bigints") { + if attrs.ty_config.large_number_types_as_bigints { + return Err(meta.error("duplicate attribute")); + } + if cfg!(not(feature = "js")) { + return Err(meta.error( + "#[tsify(large_number_types_as_bigints)] requires the `js` feature", + )); + } + attrs.ty_config.large_number_types_as_bigints = true; + return Ok(()); + } + + Err(meta.error("unsupported tsify attribute, expected one of `into_wasm_abi`, `from_wasm_abi`, `namespace`, 'type_prefix', 'type_suffix', 'missing_as_null', 'hashmap_as_object', 'large_number_types_as_bigints'")) })?; } diff --git a/tsify-macros/src/container.rs b/tsify-macros/src/container.rs index d694808..1058892 100644 --- a/tsify-macros/src/container.rs +++ b/tsify-macros/src/container.rs @@ -1,17 +1,19 @@ use serde_derive_internals::{ast, ast::Container as SerdeContainer, attr}; -use crate::{attrs::TsifyContainerAttars, ctxt::Ctxt}; +use crate::{attrs::TsifyContainerAttrs, ctxt::Ctxt}; pub struct Container<'a> { pub ctxt: Ctxt, - pub attrs: TsifyContainerAttars, + pub attrs: TsifyContainerAttrs, pub serde_container: SerdeContainer<'a>, + pub ident_str: String, + pub name: String, } impl<'a> Container<'a> { pub fn new(serde_container: SerdeContainer<'a>) -> Self { let input = &serde_container.original; - let attrs = TsifyContainerAttars::from_derive_input(input); + let attrs = TsifyContainerAttrs::from_derive_input(input); let ctxt = Ctxt::new(); let attrs = match attrs { @@ -22,10 +24,20 @@ impl<'a> Container<'a> { } }; + let name = attrs + .ty_config + .format_name(serde_container.attrs.name().serialize_name().to_string()); + + let ident_str = attrs + .ty_config + .format_name(serde_container.ident.to_string()); + Self { ctxt, attrs, serde_container, + ident_str, + name, } } @@ -48,7 +60,7 @@ impl<'a> Container<'a> { } pub fn ident_str(&self) -> String { - self.ident().to_string() + self.ident_str.clone() } #[inline] @@ -61,7 +73,7 @@ impl<'a> Container<'a> { } pub fn name(&self) -> String { - self.serde_attrs().name().serialize_name().to_owned() + self.name.clone() } pub fn generics(&self) -> &syn::Generics { diff --git a/tsify-macros/src/decl.rs b/tsify-macros/src/decl.rs index c33e72b..5c36c57 100644 --- a/tsify-macros/src/decl.rs +++ b/tsify-macros/src/decl.rs @@ -143,10 +143,13 @@ impl TsEnumDecl { .map(|t| TsEnumDecl::replace_type_params(t.clone(), type_args)) .collect(), ), - TsType::Option(t) => TsType::Option(Box::new(TsEnumDecl::replace_type_params( - t.deref().clone(), - type_args, - ))), + TsType::Option(t, null) => TsType::Option( + Box::new(TsEnumDecl::replace_type_params( + t.deref().clone(), + type_args, + )), + null, + ), TsType::Fn { params, type_ann } => TsType::Fn { params: params .iter() diff --git a/tsify-macros/src/derive.rs b/tsify-macros/src/derive.rs index 6ff4c06..cced43e 100644 --- a/tsify-macros/src/derive.rs +++ b/tsify-macros/src/derive.rs @@ -24,6 +24,7 @@ pub fn expand(input: DeriveInput) -> syn::Result { use tsify::Tsify; impl #impl_generics Tsify for #ident #ty_generics #where_clause { const DECL: &'static str = #decl_str; + const CONFIG: tsify::SerializationConfig; } }; } diff --git a/tsify-macros/src/parser.rs b/tsify-macros/src/parser.rs index 5cb246d..4bf44db 100644 --- a/tsify-macros/src/parser.rs +++ b/tsify-macros/src/parser.rs @@ -104,7 +104,7 @@ impl<'a> Parser<'a> { extends .into_iter() .map(|ty| match ty { - TsType::Option(ty) => TsType::Union(vec![*ty, TsType::empty_type_lit()]), + TsType::Option(ty, _) => TsType::Union(vec![*ty, TsType::empty_type_lit()]), _ => ty, }) .collect(), @@ -146,7 +146,9 @@ impl<'a> Parser<'a> { Style::Struct => FieldsStyle::Named, Style::Newtype => return ParsedFields::Transparent(self.parse_field(&fields[0]).0), Style::Tuple => FieldsStyle::Unnamed, - Style::Unit => return ParsedFields::Transparent(TsType::nullish()), + Style::Unit => { + return ParsedFields::Transparent(TsType::nullish(&self.container.attrs.ty_config)) + } }; let fields = fields @@ -188,7 +190,7 @@ impl<'a> Parser<'a> { } }; - let type_ann = TsType::from(field.ty); + let type_ann = TsType::from_syn_type(&self.container.attrs.ty_config, field.ty); if let Some(t) = &ts_attrs.type_override { let type_ref_names = type_ann.type_ref_names(); @@ -221,7 +223,7 @@ impl<'a> Parser<'a> { let type_ann = if optional { match type_ann { - TsType::Option(t) => *t, + TsType::Option(t, _) => *t, _ => type_ann, } } else { @@ -285,7 +287,7 @@ impl<'a> Parser<'a> { let name = variant.attrs.name().serialize_name().to_owned(); let style = variant.style; let type_ann: TsType = self.parse_fields(style, &variant.fields).into(); - type_ann.with_tag_type(name, style, tag_type) + type_ann.with_tag_type(&self.container.attrs.ty_config, name, style, tag_type) } } diff --git a/tsify-macros/src/type_alias.rs b/tsify-macros/src/type_alias.rs index 45e85b9..0b0527d 100644 --- a/tsify-macros/src/type_alias.rs +++ b/tsify-macros/src/type_alias.rs @@ -2,13 +2,14 @@ use proc_macro2::TokenStream; use quote::quote; use crate::{ - comments::extract_doc_comments, ctxt::Ctxt, decl::TsTypeAliasDecl, typescript::TsType, + attrs::TypeGenerationConfig, comments::extract_doc_comments, ctxt::Ctxt, decl::TsTypeAliasDecl, + typescript::TsType, }; pub fn expend(item: syn::ItemType) -> syn::Result { let ctxt = Ctxt::new(); - let type_ann = TsType::from(item.ty.as_ref()); + let type_ann = TsType::from_syn_type(&TypeGenerationConfig::default(), item.ty.as_ref()); let decl = TsTypeAliasDecl { id: item.ident.to_string(), diff --git a/tsify-macros/src/typescript.rs b/tsify-macros/src/typescript.rs index f436825..4194ae0 100644 --- a/tsify-macros/src/typescript.rs +++ b/tsify-macros/src/typescript.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, fmt::Display}; use serde_derive_internals::{ast::Style, attr::TagType}; -use crate::comments::write_doc_comments; +use crate::{attrs::TypeGenerationConfig, comments::write_doc_comments}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TsKeywordTypeKind { @@ -72,13 +72,36 @@ impl TsTypeLit { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NullType { + Null, + Undefined, +} + +impl NullType { + pub const fn new(config: &TypeGenerationConfig) -> Self { + if cfg!(feature = "js") && !config.missing_as_null { + Self::Undefined + } else { + Self::Null + } + } + + pub const fn to_type(&self) -> TsType { + match self { + Self::Null => TsType::NULL, + Self::Undefined => TsType::UNDEFINED, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum TsType { Keyword(TsKeywordTypeKind), Lit(String), Array(Box), Tuple(Vec), - Option(Box), + Option(Box, NullType), Ref { name: String, type_params: Vec, @@ -117,12 +140,6 @@ impl From for TsType { } } -impl From<&syn::Type> for TsType { - fn from(ty: &syn::Type) -> Self { - Self::from_syn_type(ty) - } -} - impl TsType { pub const NUMBER: TsType = TsType::Keyword(TsKeywordTypeKind::Number); pub const BIGINT: TsType = TsType::Keyword(TsKeywordTypeKind::Bigint); @@ -133,12 +150,8 @@ impl TsType { pub const NULL: TsType = TsType::Keyword(TsKeywordTypeKind::Null); pub const NEVER: TsType = TsType::Keyword(TsKeywordTypeKind::Never); - pub const fn nullish() -> Self { - if cfg!(feature = "js") { - Self::UNDEFINED - } else { - Self::NULL - } + pub const fn nullish(config: &TypeGenerationConfig) -> Self { + NullType::new(config).to_type() } pub const fn empty_type_lit() -> Self { @@ -174,7 +187,7 @@ impl TsType { } } - fn from_syn_type(ty: &syn::Type) -> Self { + pub fn from_syn_type(config: &TypeGenerationConfig, ty: &syn::Type) -> Self { use syn::Type::*; use syn::{ TypeArray, TypeBareFn, TypeGroup, TypeImplTrait, TypeParamBound, TypeParen, TypePath, @@ -183,7 +196,7 @@ impl TsType { match ty { Array(TypeArray { elem, len, .. }) => { - let elem = Self::from_syn_type(elem); + let elem = Self::from_syn_type(config, elem); let len = parse_len(len); match len { @@ -192,20 +205,22 @@ impl TsType { } } - Slice(TypeSlice { elem, .. }) => Self::Array(Box::new(Self::from_syn_type(elem))), + Slice(TypeSlice { elem, .. }) => { + Self::Array(Box::new(Self::from_syn_type(config, elem))) + } Reference(TypeReference { elem, .. }) | Paren(TypeParen { elem, .. }) - | Group(TypeGroup { elem, .. }) => Self::from_syn_type(elem), + | Group(TypeGroup { elem, .. }) => Self::from_syn_type(config, elem), BareFn(TypeBareFn { inputs, output, .. }) => { let params = inputs .iter() - .map(|arg| Self::from_syn_type(&arg.ty)) + .map(|arg| Self::from_syn_type(config, &arg.ty)) .collect(); let type_ann = if let syn::ReturnType::Type(_, ty) = output { - Self::from_syn_type(ty) + Self::from_syn_type(config, ty) } else { TsType::VOID }; @@ -218,21 +233,24 @@ impl TsType { Tuple(TypeTuple { elems, .. }) => { if elems.is_empty() { - TsType::nullish() + TsType::nullish(config) } else { - let elems = elems.iter().map(Self::from_syn_type).collect(); + let elems = elems + .iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); Self::Tuple(elems) } } - Path(TypePath { path, .. }) => Self::from_path(path).unwrap_or(TsType::NEVER), + Path(TypePath { path, .. }) => Self::from_path(config, path).unwrap_or(TsType::NEVER), TraitObject(TypeTraitObject { bounds, .. }) | ImplTrait(TypeImplTrait { bounds, .. }) => { let elems = bounds .iter() .filter_map(|t| match t { - TypeParamBound::Trait(t) => Self::from_path(&t.path), + TypeParamBound::Trait(t) => Self::from_path(config, &t.path), _ => None, // skip lifetime etc. }) .collect(); @@ -246,11 +264,13 @@ impl TsType { } } - fn from_path(path: &syn::Path) -> Option { - path.segments.last().map(Self::from_path_segment) + fn from_path(config: &TypeGenerationConfig, path: &syn::Path) -> Option { + path.segments + .last() + .map(|segment| Self::from_path_segment(config, segment)) } - fn from_path_segment(segment: &syn::PathSegment) -> Self { + fn from_path_segment(config: &TypeGenerationConfig, segment: &syn::PathSegment) -> Self { let name = segment.ident.to_string(); let (args, output) = match &segment.arguments { @@ -283,8 +303,15 @@ impl TsType { }; match name.as_str() { - "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" - | "f64" | "f32" => Self::NUMBER, + "u8" | "u16" | "u32" | "i8" | "i16" | "i32" | "f64" | "f32" => Self::NUMBER, + + "usize" | "isize" | "u64" | "i64" => { + if cfg!(feature = "js") && config.large_number_types_as_bigints { + Self::BIGINT + } else { + Self::NUMBER + } + } "u128" | "i128" => { if cfg!(feature = "js") { @@ -299,18 +326,21 @@ impl TsType { "bool" => Self::BOOLEAN, "Box" | "Cow" | "Rc" | "Arc" | "Cell" | "RefCell" if args.len() == 1 => { - Self::from_syn_type(args[0]) + Self::from_syn_type(config, args[0]) } "Vec" | "VecDeque" | "LinkedList" if args.len() == 1 => { - let elem = Self::from_syn_type(args[0]); + let elem = Self::from_syn_type(config, args[0]); Self::Array(Box::new(elem)) } "HashMap" | "BTreeMap" if args.len() == 2 => { - let type_params = args.iter().map(|arg| Self::from_syn_type(arg)).collect(); + let type_params = args + .iter() + .map(|arg| Self::from_syn_type(config, arg)) + .collect(); - let name = if cfg!(feature = "js") { + let name = if cfg!(feature = "js") && !config.hashmap_as_object { "Map" } else { "Record" @@ -321,10 +351,15 @@ impl TsType { } "HashSet" | "BTreeSet" if args.len() == 1 => { - let elem = Self::from_syn_type(args[0]); + let elem = Self::from_syn_type(config, args[0]); Self::Array(Box::new(elem)) } + "Option" if args.len() == 1 => Self::Option( + Box::new(Self::from_syn_type(config, args[0])), + NullType::new(config), + ), + "ByteBuf" => { if cfg!(feature = "js") { Self::Ref { @@ -336,11 +371,9 @@ impl TsType { } } - "Option" if args.len() == 1 => Self::Option(Box::new(Self::from_syn_type(args[0]))), - "Result" if args.len() == 2 => { - let arg0 = Self::from_syn_type(args[0]); - let arg1 = Self::from_syn_type(args[1]); + let arg0 = Self::from_syn_type(config, args[0]); + let arg1 = Self::from_syn_type(config, args[1]); let ok = type_lit! { Ok: arg0 }; let err = type_lit! { Err: arg1 }; @@ -359,7 +392,7 @@ impl TsType { }, "Range" | "RangeInclusive" => { - let start = Self::from_syn_type(args[0]); + let start = Self::from_syn_type(config, args[0]); let end = start.clone(); type_lit! { @@ -369,9 +402,12 @@ impl TsType { } "Fn" | "FnOnce" | "FnMut" => { - let params = args.into_iter().map(Self::from_syn_type).collect(); + let params = args + .into_iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); let type_ann = output - .map(Self::from_syn_type) + .map(|ty| Self::from_syn_type(config, ty)) .unwrap_or_else(|| TsType::VOID); Self::Fn { @@ -380,13 +416,25 @@ impl TsType { } } _ => { - let type_params = args.into_iter().map(Self::from_syn_type).collect(); - Self::Ref { name, type_params } + let type_params = args + .into_iter() + .map(|ty| Self::from_syn_type(config, ty)) + .collect(); + Self::Ref { + name: config.format_name(name), + type_params, + } } } } - pub fn with_tag_type(self, name: String, style: Style, tag_type: &TagType) -> Self { + pub fn with_tag_type( + self, + config: &TypeGenerationConfig, + name: String, + style: Style, + tag_type: &TagType, + ) -> Self { let type_ann = self; match tag_type { @@ -404,7 +452,7 @@ impl TsType { } } TagType::Internal { tag } => { - if type_ann == TsType::nullish() { + if type_ann == TsType::nullish(config) { let tag_field: TsType = TsTypeElement { key: tag.clone(), type_ann: TsType::Lit(name), @@ -465,7 +513,7 @@ impl TsType { TsType::Tuple(elems) => { elems.iter().for_each(|t| t.visit(f)); } - TsType::Option(t) => t.visit(f), + TsType::Option(t, _) => t.visit(f), TsType::Fn { params, type_ann } => { params .iter() @@ -504,7 +552,9 @@ impl TsType { .map(|t| t.clone().prefix_type_refs(prefix, exceptions)) .collect(), ), - TsType::Option(t) => TsType::Option(Box::new(t.prefix_type_refs(prefix, exceptions))), + TsType::Option(t, null) => { + TsType::Option(Box::new(t.prefix_type_refs(prefix, exceptions)), null) + } TsType::Ref { name, type_params } => { if exceptions.contains(&name) { TsType::Ref { @@ -559,7 +609,7 @@ impl TsType { pub fn type_refs(&self, type_refs: &mut Vec<(String, Vec)>) { match self { - TsType::Array(t) | TsType::Option(t) => t.type_refs(type_refs), + TsType::Array(t) | TsType::Option(t, _) => t.type_refs(type_refs), TsType::Tuple(tv) | TsType::Union(tv) | TsType::Intersection(tv) => { tv.iter().for_each(|t| t.type_refs(type_refs)) } @@ -659,7 +709,7 @@ impl Display for TsType { } TsType::Array(elem) => match elem.as_ref() { - TsType::Union(_) | TsType::Intersection(_) | &TsType::Option(_) => { + TsType::Union(_) | TsType::Intersection(_) | &TsType::Option(_, _) => { write!(f, "({elem})[]") } _ => write!(f, "{elem}[]"), @@ -700,8 +750,8 @@ impl Display for TsType { write!(f, "({params}) => {type_ann}") } - TsType::Option(elem) => { - write!(f, "{elem} | {}", TsType::nullish()) + TsType::Option(elem, null) => { + write!(f, "{elem} | {}", null.to_type()) } TsType::TypeLit(type_lit) => { @@ -760,13 +810,15 @@ impl Display for TsType { #[cfg(test)] mod tests { + use crate::attrs::TypeGenerationConfig; + use super::TsType; macro_rules! assert_ts { - ( $( $t:ty )|* , $expected:expr) => { + ($config:expr, $( $t:ty )|* , $expected:expr) => { $({ let ty: syn::Type = syn::parse_quote!($t); - let ts_type = TsType::from_syn_type(&ty); + let ts_type = TsType::from_syn_type(&$config, &ty); assert_eq!(ts_type.to_string(), $expected); })* }; @@ -774,52 +826,66 @@ mod tests { #[test] fn test_basic_types() { + let config = TypeGenerationConfig::default(); if cfg!(feature = "js") { - assert_ts!((), "undefined"); - assert_ts!(u128 | i128, "bigint"); - assert_ts!(HashMap | BTreeMap, "Map"); - assert_ts!(Option, "number | undefined"); - assert_ts!(Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | undefined)[]"); - assert_ts!(ByteBuf, "Uint8Array"); + assert_ts!(config, (), "undefined"); + assert_ts!(config, u128 | i128, "bigint"); + assert_ts!(config, HashMap | BTreeMap, "Map"); + assert_ts!(config, Option, "number | undefined"); + assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | undefined)[]"); } else { - assert_ts!((), "null"); - assert_ts!(u128 | i128, "number"); - assert_ts!(HashMap | BTreeMap, "Record"); - assert_ts!(Option, "number | null"); - assert_ts!(Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | null)[]"); - assert_ts!(ByteBuf, "number[]"); + assert_ts!(config, (), "null"); + assert_ts!(config, u128 | i128, "number"); + assert_ts!(config, HashMap | BTreeMap, "Record"); + assert_ts!(config, Option, "number | null"); + assert_ts!(config, Vec> | VecDeque> | LinkedList> | &'a [Option], "(T | null)[]"); + assert_ts!(config, ByteBuf, "number[]"); } assert_ts!( + config, u8 | u16 | u32 | u64 | usize | i8 | i16 | i32 | i64 | isize | f32 | f64, "number" ); - assert_ts!(String | str | char | Path | PathBuf, "string"); - assert_ts!(bool, "boolean"); - assert_ts!(Box | Rc | Arc | Cell | RefCell | Cow<'a, i32>, "number"); - assert_ts!(Vec | VecDeque | LinkedList | &'a [i32], "number[]"); - assert_ts!(HashSet | BTreeSet, "number[]"); + assert_ts!(config, String | str | char | Path | PathBuf, "string"); + assert_ts!(config, bool, "boolean"); + assert_ts!(config, Box | Rc | Arc | Cell | RefCell | Cow<'a, i32>, "number"); + assert_ts!(config, Vec | VecDeque | LinkedList | &'a [i32], "number[]"); + assert_ts!(config, HashSet | BTreeSet, "number[]"); - assert_ts!(Result, "{ Ok: number } | { Err: string }"); - assert_ts!(dyn Fn(String, f64) | dyn FnOnce(String, f64) | dyn FnMut(String, f64), "(arg0: string, arg1: number) => void"); - assert_ts!(dyn Fn(String) -> i32 | dyn FnOnce(String) -> i32 | dyn FnMut(String) -> i32, "(arg0: string) => number"); + assert_ts!(config, Result, "{ Ok: number } | { Err: string }"); + assert_ts!(config, dyn Fn(String, f64) | dyn FnOnce(String, f64) | dyn FnMut(String, f64), "(arg0: string, arg1: number) => void"); + assert_ts!(config, dyn Fn(String) -> i32 | dyn FnOnce(String) -> i32 | dyn FnMut(String) -> i32, "(arg0: string) => number"); - assert_ts!((i32), "number"); - assert_ts!((i32, String, bool), "[number, string, boolean]"); + assert_ts!(config, (i32), "number"); + assert_ts!(config, (i32, String, bool), "[number, string, boolean]"); - assert_ts!([i32; 4], "[number, number, number, number]"); - assert_ts!([i32; 16], format!("[{}]", ["number"; 16].join(", "))); - assert_ts!([i32; 17], "number[]"); - assert_ts!([i32; 1 + 1], "number[]"); + assert_ts!(config, [i32; 4], "[number, number, number, number]"); + assert_ts!( + config, + [i32; 16], + format!("[{}]", ["number"; 16].join(", ")) + ); + assert_ts!(config, [i32; 17], "number[]"); + assert_ts!(config, [i32; 1 + 1], "number[]"); - assert_ts!(Duration, "{ secs: number; nanos: number }"); + assert_ts!(config, Duration, "{ secs: number; nanos: number }"); assert_ts!( + config, SystemTime, "{ secs_since_epoch: number; nanos_since_epoch: number }" ); - assert_ts!(Range, "{ start: number; end: number }"); - assert_ts!(Range<&'static str>, "{ start: string; end: string }"); - assert_ts!(RangeInclusive, "{ start: number; end: number }"); + assert_ts!(config, Range, "{ start: number; end: number }"); + assert_ts!( + config, + Range<&'static str>, + "{ start: string; end: string }" + ); + assert_ts!( + config, + RangeInclusive, + "{ start: number; end: number }" + ); } } diff --git a/tsify-macros/src/wasm_bindgen.rs b/tsify-macros/src/wasm_bindgen.rs index fb4a95b..39946eb 100644 --- a/tsify-macros/src/wasm_bindgen.rs +++ b/tsify-macros/src/wasm_bindgen.rs @@ -42,6 +42,10 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { let typescript_type = decl.id(); + let missing_as_null = attrs.ty_config.missing_as_null; + let hashmap_as_object = attrs.ty_config.hashmap_as_object; + let large_number_types_as_bigints = attrs.ty_config.large_number_types_as_bigints; + quote! { #[automatically_derived] const _: () = { @@ -63,6 +67,11 @@ pub fn expand(cont: &Container, decl: Decl) -> TokenStream { impl #impl_generics Tsify for #ident #ty_generics #where_clause { type JsType = JsType; const DECL: &'static str = #decl_str; + const SERIALIZATION_CONFIG: tsify::SerializationConfig = tsify::SerializationConfig { + missing_as_null: #missing_as_null, + hashmap_as_object: #hashmap_as_object, + large_number_types_as_bigints: #large_number_types_as_bigints, + }; } #typescript_custom_section From 8a5a550d2ab41612cef88a3a3de2a94639b0d3fc Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 7 Feb 2024 17:19:50 -0500 Subject: [PATCH 14/15] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0ad54e..1dc1ba2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Tsify +# Tsify-next -Tsify is a library for generating TypeScript definitions from Rust code. +Tsify-next is a library for generating TypeScript definitions from Rust code. The original [Tsify](https://github.com/madonoharu/tsify) appears to be in hypernation mode, so this for incorporates updates until main Tsify project comes back to life. Using this with [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) will automatically output the types to `.d.ts`. From 0fa27b0798c9c1c2749b6d58c7cc67b785fe851b Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 10 Apr 2024 17:15:43 -0400 Subject: [PATCH 15/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dc1ba2..24144e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Tsify-next -Tsify-next is a library for generating TypeScript definitions from Rust code. The original [Tsify](https://github.com/madonoharu/tsify) appears to be in hypernation mode, so this for incorporates updates until main Tsify project comes back to life. +Tsify-next is a library for generating TypeScript definitions from Rust code. The original [Tsify](https://github.com/madonoharu/tsify) appears to be in hybernation mode, so this for incorporates updates until main Tsify project comes back to life. Using this with [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) will automatically output the types to `.d.ts`.