From 71de564f29263d73b09240ba9fb8dd432cfcd1bf Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Thu, 25 Jan 2024 19:03:04 -0500 Subject: [PATCH] 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