From 495a6bfab27eeea07a99da72bc4b124da81b7fb4 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Mon, 12 Feb 2024 18:56:50 +0100 Subject: [PATCH 01/23] add test --- ts-rs/tests/generics.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 048f8473c..7089f951c 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -349,3 +349,33 @@ fn deeply_nested() { }" ); } + +#[test] +fn inline_generic_enum() { + #[derive(TS)] + struct SomeType(String); + + #[derive(TS)] + enum MyEnum { + VariantA(A), + VariantB(B) + } + + #[derive(TS)] + struct Parent { + e: MyEnum, + #[ts(inline)] + e1: MyEnum + } + + // This fails! + // The #[ts(inline)] seems to inline recursively, so not only the definition of `MyEnum`, but + // also the definition of `SomeType`. + assert_eq!( + Parent::decl(), + "type Parent = { \ + e: MyEnum, \ + e1: { \"VariantA\": number } | { \"VariantB\": SomeType }, \ + }" + ); +} \ No newline at end of file From 5daeed248dc33ad3a88f94a4deab107c91ba34db Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Thu, 15 Feb 2024 22:18:34 +0100 Subject: [PATCH 02/23] re-implement handeling of generics --- macros/src/deps.rs | 6 +- macros/src/lib.rs | 223 ++++++++++++++++++------- macros/src/types/enum.rs | 36 ++-- macros/src/types/generics.rs | 172 ------------------- macros/src/types/mod.rs | 7 +- macros/src/types/named.rs | 13 +- macros/src/types/newtype.rs | 12 +- macros/src/types/tuple.rs | 25 +-- macros/src/types/unit.rs | 20 +-- macros/src/utils.rs | 37 +++- ts-rs/src/lib.rs | 118 ++++--------- ts-rs/tests/docs.rs | 9 +- ts-rs/tests/export_manually.rs | 4 +- ts-rs/tests/generics.rs | 38 ++--- ts-rs/tests/hashmap.rs | 4 +- ts-rs/tests/lifetimes.rs | 4 +- ts-rs/tests/list.rs | 2 +- ts-rs/tests/ranges.rs | 11 +- ts-rs/tests/raw_idents.rs | 2 +- ts-rs/tests/self_referential.rs | 2 +- ts-rs/tests/serde-skip-with-default.rs | 2 +- ts-rs/tests/union_with_data.rs | 4 +- ts-rs/tests/unsized.rs | 2 +- 23 files changed, 330 insertions(+), 423 deletions(-) delete mode 100644 macros/src/types/generics.rs diff --git a/macros/src/deps.rs b/macros/src/deps.rs index b99605656..735df3565 100644 --- a/macros/src/deps.rs +++ b/macros/src/deps.rs @@ -12,9 +12,9 @@ impl Dependencies { .push(quote![.extend(<#ty as ts_rs::TS>::dependency_types())]); } - /// Adds the given type if it's *not* transparent. - /// If it is, all it's child dependencies are added instead. - pub fn push_or_append_from(&mut self, ty: &Type) { + /// Adds the given type. + /// If the type is transparent, then we'll get resolve the child dependencies during runtime. + pub fn push(&mut self, ty: &Type) { self.0.push(quote![.push::<#ty>()]); } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 11b87a830..a1991a8b3 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,5 +1,5 @@ #![macro_use] -#![deny(unused)] +//#![deny(unused)] use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; @@ -9,6 +9,7 @@ use syn::{ }; use crate::deps::Dependencies; +use crate::utils::format_generics; #[macro_use] mod utils; @@ -17,10 +18,10 @@ mod deps; mod types; struct DerivedTS { - name: String, + generics: Generics, + ts_name: String, docs: String, inline: TokenStream, - decl: TokenStream, inline_flattened: Option, dependencies: Dependencies, @@ -29,29 +30,11 @@ struct DerivedTS { } impl DerivedTS { - fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option { - let test_fn = format_ident!("export_bindings_{}", &self.name.to_lowercase()); - let generic_params = generics - .params - .iter() - .filter(|param| matches!(param, GenericParam::Type(_))) - .map(|_| quote! { () }); - let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>); - - Some(quote! { - #[cfg(test)] - #[test] - fn #test_fn() { - #ty::export().expect("could not export type"); - } - }) - } - - fn into_impl(self, rust_ty: Ident, generics: Generics) -> TokenStream { + fn into_impl(mut self, rust_ty: Ident, generics: Generics) -> TokenStream { let mut get_export_to = quote! {}; let export_to = match &self.export_to { Some(dirname) if dirname.ends_with('/') => { - format!("{}{}.ts", dirname, self.name) + format!("{}{}.ts", dirname, self.ts_name) } Some(filename) => filename.clone(), None => { @@ -60,7 +43,7 @@ impl DerivedTS { ts_rs::__private::get_export_to_path::() } }; - format!("bindings/{}.ts", self.name) + format!("bindings/{}.ts", self.ts_name) } }; @@ -69,51 +52,26 @@ impl DerivedTS { false => None, }; - let DerivedTS { - name, - docs, - inline, - decl, - inline_flattened, - dependencies, - .. - } = self; - - let docs = match docs.is_empty() { - true => None, - false => { - Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)) - } + let docs = match &*self.docs { + "" => None, + docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)), }; - let inline_flattened = inline_flattened - .map(|t| { - quote! { - fn inline_flattened() -> String { - #t - } - } - }) - .unwrap_or_else(TokenStream::new); + let impl_start = generate_impl_block_header(&rust_ty, &generics); + let name = self.generate_name_fn(); + let inline = self.generate_inline_fn(); + let decl = self.generate_decl_fn(&rust_ty); + let dependencies = &self.dependencies; - let impl_start = generate_impl(&rust_ty, &generics); quote! { #impl_start { const EXPORT_TO: Option<&'static str> = Some(#export_to); - #get_export_to + #get_export_to #docs - - fn decl() -> String { - #decl - } - fn name() -> String { - #name.to_owned() - } - fn inline() -> String { - #inline - } - #inline_flattened + #name + #decl + #inline #[allow(clippy::unused_unit)] fn dependency_types() -> impl ts_rs::typelist::TypeList @@ -131,10 +89,151 @@ impl DerivedTS { #export } } + + /// Returns an expression which evaluates to the TypeScript name of the type, including generic + /// parameters. + fn name_with_generics(&self) -> TokenStream { + let name = &self.ts_name; + let mut generics_ts_names = self + .generics + .params + .iter() + .filter_map(|g| match g { + GenericParam::Lifetime(_) => None, + GenericParam::Type(ty) => Some(&ty.ident), + GenericParam::Const(_) => None, + }) + .map(|generic| quote!(<#generic as ts_rs::TS>::name())) + .peekable(); + + if generics_ts_names.peek().is_some() { + quote! { + format!("{}<{}>", #name, vec![#(#generics_ts_names),*].join(", ")) + } + } else { + quote!(#name.to_owned()) + } + } + + /// Generate a dummy unit struct for every generic type parameter of this type. + /// Example: + /// ```ignore + /// struct Generic { /* ... */ } + /// ``` + /// has two generic type parameters, `A` and `B`. This function will therefor generate + /// ```ignore + /// struct A; + /// impl ts_rs::TS for A { /* .. */ } + /// + /// struct B; + /// impl ts_rs::TS for B { /* .. */ } + /// ``` + fn generate_generic_types(&self) -> TokenStream { + let generics = self.generics.params.iter().filter_map(|g| match g { + GenericParam::Lifetime(_) => None, + GenericParam::Type(t) => Some(t.ident.clone()), + GenericParam::Const(_) => None, + }); + + quote! { + #( + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] + struct #generics; + impl std::fmt::Display for #generics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } + } + impl TS for #generics { + fn name() -> String { stringify!(#generics).to_owned() } + fn transparent() -> bool { false } + } + )* + } + } + + fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option { + let test_fn = format_ident!("export_bindings_{}", rust_ty.to_string().to_lowercase()); + let generic_params = generics + .params + .iter() + .filter(|param| matches!(param, GenericParam::Type(_))) + .map(|_| quote! { () }); + let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>); + + Some(quote! { + #[cfg(test)] + #[test] + fn #test_fn() { + #ty::export().expect("could not export type"); + } + }) + } + + fn generate_name_fn(&self) -> TokenStream { + let name = self.name_with_generics(); + quote! { + fn name() -> String { + #name + } + } + } + + fn generate_inline_fn(&self) -> TokenStream { + let inline = &self.inline; + + let inline_flattened = self.inline_flattened.as_ref().map(|inline_flattened| { + quote! { + fn inline_flattened() -> String { + #inline_flattened + } + } + }); + let inline = quote! { + fn inline() -> String { + #inline + } + }; + quote! { + #inline + #inline_flattened + } + } + + /// Generates the `decl()` and `decl_concrete()` methods. + /// `decl_concrete()` is simple, and simply defers to `inline()`. + /// For `decl()`, however, we need to change out the generic parameters of the type, replacing + /// them with the dummy types generated by `generate_generic_types()`. + fn generate_decl_fn(&mut self, rust_ty: &Ident) -> TokenStream { + let name = &self.ts_name; + let generic_types = self.generate_generic_types(); + let ts_generics = format_generics(&mut self.dependencies, &self.generics); + // These are the generic parameters we'll be using. + let generic_idents = self.generics.params.iter().filter_map(|p| match p { + GenericParam::Lifetime(_) => None, + // Since we named our dummy types the same as the generic parameters, we can just keep + // the identifier of the generic parameter - its name is shadowed by the dummy struct. + GenericParam::Type(TypeParam { ident, .. }) => Some(quote!(#ident)), + // We keep const parameters as they are, since there's no sensible default value we can + // use instead. This might be something to change in the future. + GenericParam::Const(ConstParam { ident, .. }) => Some(quote!(#ident)), + }); + quote! { + fn decl_concrete() -> String { + format!("type {} = {};", #name, Self::inline()) + } + fn decl() -> String { + #generic_types + let inline = <#rust_ty<#(#generic_idents,)*> as ts_rs::TS>::inline(); + let generics = #ts_generics; + format!("type {}{generics} = {inline};", #name) + } + } + } } // generate start of the `impl TS for #ty` block, up to (excluding) the open brace -fn generate_impl(ty: &Ident, generics: &Generics) -> TokenStream { +fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream { use GenericParam::*; let bounds = generics.params.iter().map(|param| match param { diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 4fb01596e..1527df971 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -5,9 +5,8 @@ use syn::{Fields, Generics, ItemEnum, Type, Variant}; use crate::{ attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, deps::Dependencies, - types, - types::generics::{format_generics, format_type}, DerivedTS, + types, }; pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { @@ -19,15 +18,15 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { }; if s.variants.is_empty() { - return Ok(empty_enum(name, enum_attr)); + return Ok(empty_enum(name, enum_attr, s.generics.clone())); } if s.variants.is_empty() { return Ok(DerivedTS { - name, + generics: s.generics.clone(), + ts_name: name, docs: enum_attr.docs, inline: quote!("never".to_owned()), - decl: quote!("type {} = never;"), inline_flattened: None, dependencies: Dependencies::default(), export: enum_attr.export, @@ -47,18 +46,17 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { )?; } - let generic_args = format_generics(&mut dependencies, &s.generics); Ok(DerivedTS { + generics: s.generics.clone(), inline: quote!([#(#formatted_variants),*].join(" | ")), - decl: quote!(format!("type {}{} = {};", #name, #generic_args, Self::inline())), inline_flattened: Some(quote!( format!("({})", [#(#formatted_variants),*].join(" | ")) )), dependencies, - name, docs: enum_attr.docs, export: enum_attr.export, export_to: enum_attr.export_to, + ts_name: name, }) } @@ -124,9 +122,13 @@ fn format_variant( (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), (Some(type_override), None) => quote! { #type_override }, (None, Some(type_as)) => { - format_type(&syn::parse_str::(&type_as)?, dependencies, generics) + let ty = syn::parse_str::(&type_as)?; + quote!(<#ty as ts_rs::TS>::name()) } - (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), + (None, None) => { + let ty = &unnamed.unnamed[0].ty; + quote!(<#ty as ts_rs::TS>::name()) + }, }; quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty)) @@ -170,9 +172,13 @@ fn format_variant( (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), (Some(type_override), None) => quote! { #type_override }, (None, Some(type_as)) => { - format_type(&syn::parse_str::(&type_as)?, dependencies, generics) + let ty = syn::parse_str::(&type_as)?; + quote!(<#ty as ts_rs::TS>::name()) } - (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), + (None, None) => { + let ty = &unnamed.unnamed[0].ty; + quote!(<#ty as ts_rs::TS>::name()) + }, }; quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty)) @@ -192,16 +198,16 @@ fn format_variant( } // bindings for an empty enum (`never` in TS) -fn empty_enum(name: impl Into, enum_attr: EnumAttr) -> DerivedTS { +fn empty_enum(name: impl Into, enum_attr: EnumAttr, generics: Generics) -> DerivedTS { let name = name.into(); DerivedTS { + generics: generics.clone(), inline: quote!("never".to_owned()), - decl: quote!(format!("type {} = never;", #name)), - name, docs: enum_attr.docs, inline_flattened: None, dependencies: Dependencies::default(), export: enum_attr.export, export_to: enum_attr.export_to, + ts_name: name, } } diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs deleted file mode 100644 index 7da2c48ba..000000000 --- a/macros/src/types/generics.rs +++ /dev/null @@ -1,172 +0,0 @@ -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - GenericArgument, GenericParam, Generics, ItemStruct, PathArguments, Type, TypeGroup, - TypeReference, TypeSlice, TypeTuple, -}; - -use crate::{attr::StructAttr, deps::Dependencies}; - -/// formats the generic arguments (like A, B in struct X{..}) as "" where x is a comma -/// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored). -/// this expands to an expression which evaluates to a `String`. -/// -/// If a default type arg is encountered, it will be added to the dependencies. -pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream { - let mut expanded_params = generics - .params - .iter() - .filter_map(|param| match param { - GenericParam::Type(type_param) => Some({ - let ty = type_param.ident.to_string(); - if let Some(default) = &type_param.default { - let default = format_type(default, deps, generics); - quote!(format!("{} = {}", #ty, #default)) - } else { - quote!(#ty.to_owned()) - } - }), - _ => None, - }) - .peekable(); - - if expanded_params.peek().is_none() { - return quote!(""); - } - - let comma_separated = quote!([#(#expanded_params),*].join(", ")); - quote!(format!("<{}>", #comma_separated)) -} - -pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generics) -> TokenStream { - // If the type matches one of the generic parameters, just pass the identifier: - if let Some(generic) = generics - .params - .iter() - .filter_map(|param| match param { - GenericParam::Type(type_param) => Some(type_param), - _ => None, - }) - .find(|type_param| { - matches!( - ty, - Type::Path(type_path) - if type_path.qself.is_none() - && type_path.path.is_ident(&type_param.ident) - ) - }) - { - let generic_ident = generic.ident.clone(); - let generic_ident_str = generic_ident.to_string(); - - if !generic.bounds.is_empty() { - return quote!(#generic_ident_str.to_owned()); - } - - return quote!( - match <#generic_ident>::inline().as_str() { - // When exporting a generic, the default type used is `()`, - // which gives "null" when calling `.name()`. In this case, we - // want to preserve the type param's identifier as the name used - "null" => #generic_ident_str.to_owned(), - - // If name is not "null", a type has been provided, so we use its - // name instead - x => x.to_owned() - } - ); - } - - // special treatment for arrays and tuples - match ty { - // Arrays have their own implementation that needs to be handle separetly - // be cause the T in `[T; N]` is technically not a generic - Type::Array(type_array) => { - let formatted = format_type(&type_array.elem, dependencies, generics); - return quote!(<#type_array>::name_with_type_args(vec![#formatted])); - } - // The field is a slice (`[T]`) so it technically doesn't have a - // generic argument. Therefore, we handle it explicitly here like a `Vec` - Type::Slice(TypeSlice { ref elem, .. }) => { - let inner_ty = elem; - let vec_ty = syn::parse2::(quote!(Vec::<#inner_ty>)).unwrap(); - return format_type(&vec_ty, dependencies, generics); - } - // same goes for a tuple (`(A, B, C)`) - it doesn't have a type arg, so we handle it - // explicitly here. - Type::Tuple(tuple) => { - if tuple.elems.is_empty() { - // empty tuples `()` should be treated as `null` - return super::unit::null(&StructAttr::default(), "") - .unwrap() - .inline; - } - - // we convert the tuple field to a struct: `(A, B, C)` => `struct A(A, B, C)` - let tuple_struct = super::type_def( - &StructAttr::default(), - &format_ident!("_"), - &tuple_type_to_tuple_struct(tuple).fields, - generics, - ) - .unwrap(); - // now, we return the inline definition - dependencies.append(tuple_struct.dependencies); - return tuple_struct.inline; - } - Type::Reference(syn::TypeReference { ref elem, .. }) => { - return format_type(elem, dependencies, generics) - } - _ => (), - }; - - dependencies.push_or_append_from(ty); - match extract_type_args(ty) { - None => quote!(<#ty as ts_rs::TS>::name()), - Some(type_args) => { - let args = type_args - .iter() - .map(|ty| format_type(ty, dependencies, generics)) - .collect::>(); - let args = quote!(vec![#(#args),*]); - quote!(<#ty as ts_rs::TS>::name_with_type_args(#args)) - } - } -} - -fn extract_type_args(ty: &Type) -> Option> { - let last_segment = match ty { - Type::Group(TypeGroup { elem, .. }) | Type::Reference(TypeReference { elem, .. }) => { - return extract_type_args(elem) - } - Type::Path(type_path) => type_path.path.segments.last(), - _ => None, - }?; - - let segment_arguments = match &last_segment.arguments { - PathArguments::AngleBracketed(generic_arguments) => Some(generic_arguments), - _ => None, - }?; - - let type_args: Vec<_> = segment_arguments - .args - .iter() - .filter_map(|arg| match arg { - GenericArgument::Type(ty) => Some(ty), - _ => None, - }) - .collect(); - if type_args.is_empty() { - return None; - } - - Some(type_args) -} - -// convert a [`TypeTuple`], e.g `(A, B, C)` -// to a [`ItemStruct`], e.g `struct A(A, B, C)` -fn tuple_type_to_tuple_struct(tuple: &TypeTuple) -> ItemStruct { - let elements = tuple.elems.iter(); - syn::parse2(quote!(struct A( #(#elements),* );)) - .expect("could not convert tuple to tuple struct") -} diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs index 7a94e21b6..1bdd276d5 100644 --- a/macros/src/types/mod.rs +++ b/macros/src/types/mod.rs @@ -3,7 +3,6 @@ use syn::{Fields, Generics, Ident, ItemStruct, Result}; use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS}; mod r#enum; -mod generics; mod named; mod newtype; mod tuple; @@ -26,14 +25,14 @@ fn type_def( let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident)); match fields { Fields::Named(named) => match named.named.len() { - 0 => unit::empty_object(attr, &name), + 0 => unit::empty_object(attr, &name, generics.clone()), _ => named::named(attr, &name, named, generics), }, Fields::Unnamed(unnamed) => match unnamed.unnamed.len() { - 0 => unit::empty_array(attr, &name), + 0 => unit::empty_array(attr, &name, generics.clone()), 1 => newtype::newtype(attr, &name, unnamed, generics), _ => tuple::tuple(attr, &name, unnamed, generics), }, - Fields::Unit => unit::null(attr, &name), + Fields::Unit => unit::null(attr, &name, generics.clone()), } } diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 10080d37d..f3cc0dccc 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -5,9 +5,8 @@ use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result, use crate::{ attr::{FieldAttr, Inflection, Optional, StructAttr}, deps::Dependencies, - types::generics::{format_generics, format_type}, - utils::{raw_name_to_ts_field, to_ts_ident}, DerivedTS, + utils::{raw_name_to_ts_field, to_ts_ident}, }; pub(crate) fn named( @@ -39,7 +38,6 @@ pub(crate) fn named( let fields = quote!(<[String]>::join(&[#(#formatted_fields),*], " ")); let flattened = quote!(<[String]>::join(&[#(#flattened_fields),*], " & ")); - let generic_args = format_generics(&mut dependencies, generics); let inline = match (formatted_fields.len(), flattened_fields.len()) { (0, 0) => quote!("{ }".to_owned()), @@ -50,14 +48,14 @@ pub(crate) fn named( }; Ok(DerivedTS { + generics: generics.clone(), inline: quote!(#inline.replace(" } & { ", " ")), - decl: quote!(format!("type {}{} = {}", #name, #generic_args, Self::inline())), inline_flattened: Some(quote!(format!("{{ {} }}", #fields))), - name: name.to_owned(), docs: attr.docs.clone(), dependencies, export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } @@ -77,7 +75,7 @@ fn format_field( dependencies: &mut Dependencies, field: &Field, rename_all: &Option, - generics: &Generics, + _generics: &Generics, ) -> Result<()> { let FieldAttr { type_as, @@ -139,7 +137,8 @@ fn format_field( dependencies.append_from(ty); quote!(<#ty as ts_rs::TS>::inline()) } else { - format_type(ty, dependencies, generics) + dependencies.push(ty); + quote!(<#ty as ts_rs::TS>::name()) } }); let field_name = to_ts_ident(field.ident.as_ref().unwrap()); diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index e1d046318..1b53e6c50 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -4,7 +4,6 @@ use syn::{FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, deps::Dependencies, - types::generics::{format_generics, format_type}, DerivedTS, }; @@ -34,7 +33,7 @@ pub(crate) fn newtype( match (&rename_inner, skip, optional.optional, flatten) { (Some(_), ..) => syn_err!("`rename` is not applicable to newtype fields"), - (_, true, ..) => return super::unit::null(attr, name), + (_, true, ..) => return super::unit::null(attr, name, generics.clone()), (_, _, true, ..) => syn_err!("`optional` is not applicable to newtype fields"), (_, _, _, true) => syn_err!("`flatten` is not applicable to newtype fields"), _ => {} @@ -55,24 +54,23 @@ pub(crate) fn newtype( match (type_override.is_none(), inline) { (false, _) => (), (true, true) => dependencies.append_from(&inner_ty), - (true, false) => dependencies.push_or_append_from(&inner_ty), + (true, false) => dependencies.push(&inner_ty), }; let inline_def = match type_override { Some(ref o) => quote!(#o.to_owned()), None if inline => quote!(<#inner_ty as ts_rs::TS>::inline()), - None => format_type(&inner_ty, &mut dependencies, generics), + None => quote!(<#inner_ty as ts_rs::TS>::name()), }; - let generic_args = format_generics(&mut dependencies, generics); Ok(DerivedTS { - decl: quote!(format!("type {}{} = {};", #name, #generic_args, #inline_def)), + generics: generics.clone(), inline: inline_def, inline_flattened: None, - name: name.to_owned(), docs: attr.docs.clone(), dependencies, export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index 0459c4a60..b72caddd5 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -5,7 +5,6 @@ use syn::{Field, FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, deps::Dependencies, - types::generics::{format_generics, format_type}, DerivedTS, }; @@ -28,28 +27,20 @@ pub(crate) fn tuple( format_field(&mut formatted_fields, &mut dependencies, field, generics)?; } - let generic_args = format_generics(&mut dependencies, generics); Ok(DerivedTS { + generics: generics.clone(), inline: quote! { format!( "[{}]", [#(#formatted_fields),*].join(", ") ) }, - decl: quote! { - format!( - "type {}{} = {};", - #name, - #generic_args, - Self::inline() - ) - }, inline_flattened: None, - name: name.to_owned(), docs: attr.docs.clone(), dependencies, export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } @@ -57,7 +48,7 @@ fn format_field( formatted_fields: &mut Vec, dependencies: &mut Dependencies, field: &Field, - generics: &Generics, + _generics: &Generics, ) -> Result<()> { let FieldAttr { type_as, @@ -99,17 +90,13 @@ fn format_field( formatted_fields.push(match type_override { Some(ref o) => quote!(#o.to_owned()), None if inline => quote!(<#ty as ts_rs::TS>::inline()), - None => format_type(&ty, dependencies, generics), + None => quote!(<#ty as ts_rs::TS>::name()), }); match (inline, type_override) { (_, Some(_)) => (), - (false, _) => { - dependencies.push_or_append_from(&ty); - } - (true, _) => { - dependencies.append_from(&ty); - } + (false, _) => dependencies.push(&ty), + (true, _) => dependencies.append_from(&ty), }; Ok(()) diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 64ce68c1c..9982e9fc5 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -1,50 +1,50 @@ use quote::quote; -use syn::Result; +use syn::{Generics, Result}; use crate::{attr::StructAttr, deps::Dependencies, DerivedTS}; -pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result { +pub(crate) fn empty_object(attr: &StructAttr, name: &str, generics: Generics) -> Result { check_attributes(attr)?; Ok(DerivedTS { + generics: generics.clone(), inline: quote!("Record".to_owned()), - decl: quote!(format!("type {} = Record;", #name)), inline_flattened: None, - name: name.to_owned(), docs: attr.docs.clone(), dependencies: Dependencies::default(), export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } -pub(crate) fn empty_array(attr: &StructAttr, name: &str) -> Result { +pub(crate) fn empty_array(attr: &StructAttr, name: &str, generics: Generics) -> Result { check_attributes(attr)?; Ok(DerivedTS { + generics: generics.clone(), inline: quote!("never[]".to_owned()), - decl: quote!(format!("type {} = never[];", #name)), inline_flattened: None, - name: name.to_owned(), docs: attr.docs.clone(), dependencies: Dependencies::default(), export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } -pub(crate) fn null(attr: &StructAttr, name: &str) -> Result { +pub(crate) fn null(attr: &StructAttr, name: &str, generics: Generics) -> Result { check_attributes(attr)?; Ok(DerivedTS { + generics: generics.clone(), inline: quote!("null".to_owned()), - decl: quote!(format!("type {} = null;", #name)), inline_flattened: None, - name: name.to_owned(), docs: attr.docs.clone(), dependencies: Dependencies::default(), export: attr.export, export_to: attr.export_to.clone(), + ts_name: name.to_owned(), }) } diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 93552b716..4bcadf6f1 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -1,7 +1,9 @@ use std::convert::TryFrom; -use proc_macro2::Ident; -use syn::{spanned::Spanned, Attribute, Error, Expr, ExprLit, Lit, Meta, Result}; +use proc_macro2::{Ident, TokenStream}; +use syn::{Attribute, Error, Expr, ExprLit, GenericParam, Generics, Lit, Meta, Result, spanned::Spanned}; +use quote::quote; +use crate::deps::Dependencies; macro_rules! syn_err { ($l:literal $(, $a:expr)*) => { @@ -197,3 +199,34 @@ mod warning { writer.print(&buffer) } } + +/// formats the generic arguments (like A, B in struct X{..}) as "" where x is a comma +/// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored). +/// this expands to an expression which evaluates to a `String`. +/// +/// If a default type arg is encountered, it will be added to the dependencies. +pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream { + let mut expanded_params = generics + .params + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) => Some({ + let ty = type_param.ident.to_string(); + if let Some(default) = &type_param.default { + deps.push(default); + quote!(format!("{} = {}", #ty, <#default as ts_rs::TS>::name())) + } else { + quote!(#ty.to_owned()) + } + }), + _ => None, + }) + .peekable(); + + if expanded_params.peek().is_none() { + return quote!(""); + } + + let comma_separated = quote!([#(#expanded_params),*].join(", ")); + quote!(format!("<{}>", #comma_separated)) +} diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index efd26d569..f37b80a5f 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -168,12 +168,11 @@ use std::{ pub use ts_rs_macros::TS; -pub use crate::export::ExportError; -use crate::typelist::TypeList; - // Used in generated code. Not public API #[doc(hidden)] pub use crate::export::__private; +pub use crate::export::ExportError; +use crate::typelist::TypeList; #[cfg(feature = "chrono-impl")] mod chrono; @@ -289,18 +288,25 @@ pub trait TS { /// Declaration of this type, e.g. `interface User { user_id: number, ... }`. /// This function will panic if the type has no declaration. + /// + /// If this type is generic, then all provided generic parameters will be swapped for + /// placeholders, resulting in a generic typescript definition. + /// Both `SomeType::::decl()` and `SomeType::::decl()` will therefore result in + /// the same TypeScript declaration `type SomeType = ...`. fn decl() -> String { panic!("{} cannot be declared", Self::name()); } + /// Declaration of this type using the supplied generic arguments. + /// The resulting TypeScript definition will not be generic. For that, see `TS::decl()`. + /// If this type is not generic, then this function is equivalent to `TS::decl()`. + fn decl_concrete() -> String { + panic!("{} cannot be declared", Self::name()); + } + /// Name of this type in TypeScript. fn name() -> String; - /// Name of this type in TypeScript, with type arguments. - fn name_with_type_args(args: Vec) -> String { - format!("{}<{}>", Self::name(), args.join(", ")) - } - /// Formats this types definition in TypeScript, e.g `{ user_id: number }`. /// This function will panic if the type cannot be inlined. fn inline() -> String { @@ -313,12 +319,14 @@ pub trait TS { panic!("{} cannot be flattened", Self::name()) } + /// Returns a `TypeList` of all types on which this type depends. fn dependency_types() -> impl TypeList where Self: 'static, { } + /// Resolves all dependencies of this type recursively. fn dependencies() -> Vec where Self: 'static, @@ -329,7 +337,12 @@ pub trait TS { struct Visit<'a>(&'a mut Vec); impl<'a> TypeVisitor for Visit<'a> { fn visit(&mut self) { - if let Some(dep) = Dependency::from_ty::() { + if T::transparent() { + // the dependency `T` is "transparent", meaning that our original type depends + // on the dependencies of `T` as well. + T::dependency_types().for_each(self); + } else if let Some(dep) = Dependency::from_ty::() { + // the dependency `T` is not transparent, so we just add it to the output self.0.push(dep); } } @@ -408,11 +421,7 @@ macro_rules! impl_primitives { ($($($ty:ty),* => $l:literal),*) => { $($( impl TS for $ty { fn name() -> String { $l.to_owned() } - fn name_with_type_args(args: Vec) -> String { - assert!(args.is_empty(), "called name_with_type_args on primitive"); - $l.to_owned() - } - fn inline() -> String { $l.to_owned() } + fn inline() -> String { Self::name() } fn transparent() -> bool { false } } )*)* }; @@ -425,7 +434,7 @@ macro_rules! impl_tuples { format!("[{}]", [$($i::name()),*].join(", ")) } fn inline() -> String { - format!("[{}]", [$($i::inline()),*].join(", ")) + panic!("tuple cannot be inlined!"); } fn dependency_types() -> impl TypeList where @@ -448,10 +457,6 @@ macro_rules! impl_wrapper { ($($t:tt)*) => { $($t)* { fn name() -> String { T::name() } - fn name_with_type_args(mut args: Vec) -> String { - assert_eq!(args.len(), 1); - args.remove(0) - } fn inline() -> String { T::inline() } fn inline_flattened() -> String { T::inline_flattened() } fn dependency_types() -> impl TypeList @@ -470,7 +475,6 @@ macro_rules! impl_shadow { (as $s:ty: $($impl:tt)*) => { $($impl)* { fn name() -> String { <$s>::name() } - fn name_with_type_args(args: Vec) -> String { <$s>::name_with_type_args(args) } fn inline() -> String { <$s>::inline() } fn inline_flattened() -> String { <$s>::inline_flattened() } fn dependency_types() -> impl $crate::typelist::TypeList @@ -486,17 +490,7 @@ macro_rules! impl_shadow { impl TS for Option { fn name() -> String { - unreachable!(); - } - - fn name_with_type_args(args: Vec) -> String { - assert_eq!( - args.len(), - 1, - "called Option::name_with_type_args with {} args", - args.len() - ); - format!("{} | null", args[0]) + format!("{} | null", T::name()) } fn inline() -> String { @@ -517,7 +511,7 @@ impl TS for Option { impl TS for Result { fn name() -> String { - unreachable!(); + format!("{{ Ok : {} }} | {{ Err : {} }}", T::name(), E::name()) } fn inline() -> String { format!("{{ Ok : {} }} | {{ Err : {} }}", T::inline(), E::inline()) @@ -535,7 +529,7 @@ impl TS for Result { impl TS for Vec { fn name() -> String { - "Array".to_owned() + format!("Array<{}>", T::name()) } fn inline() -> String { @@ -561,25 +555,10 @@ impl TS for [T; N] { return Vec::::name(); } - "[]".to_owned() - } - - fn name_with_type_args(args: Vec) -> String { - if N > ARRAY_TUPLE_LIMIT { - return Vec::::name_with_type_args(args); - } - - assert_eq!( - args.len(), - 1, - "called [T; N]::name_with_type_args with {} args", - args.len() - ); - format!( "[{}]", (0..N) - .map(|_| args[0].clone()) + .map(|_| T::name()) .collect::>() .join(", ") ) @@ -592,7 +571,10 @@ impl TS for [T; N] { format!( "[{}]", - (0..N).map(|_| T::inline()).collect::>().join(", ") + (0..N) + .map(|_| T::inline()) + .collect::>() + .join(", ") ) } @@ -610,17 +592,7 @@ impl TS for [T; N] { impl TS for HashMap { fn name() -> String { - "Record".to_owned() - } - - fn name_with_type_args(args: Vec) -> String { - assert_eq!( - args.len(), - 2, - "called HashMap::name_with_type_args with {} args", - args.len() - ); - format!("Record<{}, {}>", args[0], args[1]) + format!("Record<{}, {}>", K::name(), V::name()) } fn inline() -> String { @@ -641,17 +613,7 @@ impl TS for HashMap { impl TS for Range { fn name() -> String { - panic!("called Range::name - Did you use a type alias?") - } - - fn name_with_type_args(args: Vec) -> String { - assert_eq!( - args.len(), - 1, - "called Range::name_with_type_args with {} args", - args.len() - ); - format!("{{ start: {}, end: {}, }}", &args[0], &args[0]) + format!("{{ start: {}, end: {}, }}", I::name(), I::name()) } fn dependency_types() -> impl TypeList @@ -668,17 +630,7 @@ impl TS for Range { impl TS for RangeInclusive { fn name() -> String { - panic!("called RangeInclusive::name - Did you use a type alias?") - } - - fn name_with_type_args(args: Vec) -> String { - assert_eq!( - args.len(), - 1, - "called RangeInclusive::name_with_type_args with {} args", - args.len() - ); - format!("{{ start: {}, end: {}, }}", &args[0], &args[0]) + format!("{{ start: {}, end: {}, }}", I::name(), I::name()) } fn dependency_types() -> impl TypeList diff --git a/ts-rs/tests/docs.rs b/ts-rs/tests/docs.rs index 41e5e6699..d007ec42a 100644 --- a/ts-rs/tests/docs.rs +++ b/ts-rs/tests/docs.rs @@ -132,7 +132,7 @@ fn export_a() { " *\n", " * Testing\n", " */\n", - "name: string, }" + "name: string, };" ) }; @@ -178,7 +178,7 @@ fn export_b() { " *\n", " * Testing\n", " */\n", - "name: string, }", + "name: string, };", ) }; @@ -372,11 +372,8 @@ fn export_g() { " *\n", " * Testing\n", " */\n", - "variant_field: number, } })", + "variant_field: number, } });", ) - - - }; let actual_content = fs::read_to_string("tests-out/docs/G.ts").unwrap(); diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs index 33a7af12c..1ca37a7ca 100644 --- a/ts-rs/tests/export_manually.rs +++ b/ts-rs/tests/export_manually.rs @@ -32,7 +32,7 @@ fn export_manually() { } else { concat!( "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", - "\nexport type User = { name: string, age: number, active: boolean, }" + "\nexport type User = { name: string, age: number, active: boolean, };" ) }; @@ -53,7 +53,7 @@ fn export_manually_dir() { } else { concat!( "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", - "\nexport type UserDir = { name: string, age: number, active: boolean, }" + "\nexport type UserDir = { name: string, age: number, active: boolean, };" ) }; diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 7089f951c..c8429e5b2 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -60,27 +60,27 @@ declare! { fn test() { assert_eq!( TypeGroup::decl(), - "type TypeGroup = { foo: Array, }", + "type TypeGroup = { foo: Array, };", ); assert_eq!( Generic::<()>::decl(), - "type Generic = { value: T, values: Array, }" + "type Generic = { value: T, values: Array, };" ); assert_eq!( GenericAutoBound::<()>::decl(), - "type GenericAutoBound = { value: T, values: Array, }" + "type GenericAutoBound = { value: T, values: Array, };" ); assert_eq!( GenericAutoBound2::<()>::decl(), - "type GenericAutoBound2 = { value: T, values: Array, }" + "type GenericAutoBound2 = { value: T, values: Array, };" ); assert_eq!( Container::decl(), - "type Container = { foo: Generic, bar: Array>, baz: Record>, }" + "type Container = { foo: Generic, bar: Array>, baz: Record>, };" ); } @@ -142,7 +142,7 @@ fn generic_struct() { assert_eq!( Struct::<()>::decl(), - "type Struct = { a: T, b: [T, T], c: [T, [T, T]], d: [T, T, T], e: [[T, T], [T, T], [T, T]], f: Array, g: Array>, h: Array<[[T, T], [T, T], [T, T]]>, }" + "type Struct = { a: T, b: [T, T], c: [T, [T, T]], d: [T, T, T], e: [[T, T], [T, T], [T, T]], f: Array, g: Array>, h: Array<[[T, T], [T, T], [T, T]]>, };" ) } @@ -162,10 +162,10 @@ fn inline() { t: Generic>, } - assert_eq!(Generic::<()>::decl(), "type Generic = { t: T, }"); + assert_eq!(Generic::<()>::decl(), "type Generic = { t: T, };"); assert_eq!( Container::decl(), - "type Container = { g: Generic, gi: { t: string, }, t: Array, }" + "type Container = { g: Generic, gi: { t: string, }, t: Array, };" ); } @@ -193,12 +193,12 @@ fn inline_with_bounds() { assert_eq!( Generic::<&'static str>::decl(), - "type Generic = { t: T, }" + "type Generic = { t: T, };" ); // ^^^^^^^^^^^^ Replace with something else assert_eq!( Container::decl(), - "type Container = { g: Generic, gi: { t: string, }, t: number, }" // Actual output: { g: Generic, gi: { t: T, }, t: T, } + "type Container = { g: Generic, gi: { t: string, }, t: number, };" // Actual output: { g: Generic, gi: { t: T, }, t: T, } ); } @@ -222,11 +222,11 @@ fn inline_with_default() { assert_eq!( Generic::<()>::decl(), - "type Generic = { t: T, }" + "type Generic = { t: T, };" ); assert_eq!( Container::decl(), - "type Container = { g: Generic, gi: { t: string, }, t: number, }" + "type Container = { g: Generic, gi: { t: string, }, t: number, };" ); } @@ -236,13 +236,13 @@ fn default() { struct A { t: T, } - assert_eq!(A::<()>::decl(), "type A = { t: T, }"); + assert_eq!(A::<()>::decl(), "type A = { t: T, };"); #[derive(TS)] struct B>> { u: U, } - assert_eq!(B::<()>::decl(), "type B | null> = { u: U, }"); + assert_eq!(B::<()>::decl(), "type B | null> = { u: U, };"); assert!(B::<()>::dependencies().iter().any(|dep| dep.ts_name == "A")); #[derive(TS)] @@ -256,7 +256,7 @@ fn default() { // #[ts(inline)] // xi2: X } - assert_eq!(Y::decl(), "type Y = { a1: A, a2: A, }") + assert_eq!(Y::decl(), "type Y = { a1: A, a2: A, };") } #[test] @@ -265,7 +265,7 @@ fn trait_bounds() { struct A { t: T, } - assert_eq!(A::::decl(), "type A = { t: T, }"); + assert_eq!(A::::decl(), "type A = { t: T, };"); #[derive(TS)] struct B(T); @@ -289,7 +289,7 @@ fn trait_bounds() { } let ty = format!( - "type D = {{ t: [{}], }}", + "type D = {{ t: [{}], }};", "T, ".repeat(41).trim_end_matches(", ") ); assert_eq!(D::<&str, 41>::decl(), ty) @@ -346,7 +346,7 @@ fn deeply_nested() { a_null: T1>, \ b_null: T1>>>, \ c_null: T1>, \ - }" + };" ); } @@ -376,6 +376,6 @@ fn inline_generic_enum() { "type Parent = { \ e: MyEnum, \ e1: { \"VariantA\": number } | { \"VariantB\": SomeType }, \ - }" + };" ); } \ No newline at end of file diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs index 1fc535473..8f20512c9 100644 --- a/ts-rs/tests/hashmap.rs +++ b/ts-rs/tests/hashmap.rs @@ -13,7 +13,7 @@ fn hashmap() { assert_eq!( Hashes::decl(), - "type Hashes = { map: Record, set: Array, }" + "type Hashes = { map: Record, set: Array, };" ) } @@ -33,6 +33,6 @@ fn hashmap_with_custom_hasher() { assert_eq!( Hashes::decl(), - "type Hashes = { map: Record, set: Array, }" + "type Hashes = { map: Record, set: Array, };" ) } diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs index 10abf371e..d524ceace 100644 --- a/ts-rs/tests/lifetimes.rs +++ b/ts-rs/tests/lifetimes.rs @@ -8,7 +8,7 @@ fn contains_borrow() { s: &'a str, } - assert_eq!(S::decl(), "type S = { s: string, }") + assert_eq!(S::decl(), "type S = { s: string, };") } #[test] @@ -29,6 +29,6 @@ fn contains_borrow_type_args() { assert_eq!( A::decl(), - "type A = { a: Array, b: Array>, c: Record, }" + "type A = { a: Array, b: Array>, c: Record, };" ); } diff --git a/ts-rs/tests/list.rs b/ts-rs/tests/list.rs index 92f6f2efe..f8c131ea9 100644 --- a/ts-rs/tests/list.rs +++ b/ts-rs/tests/list.rs @@ -8,5 +8,5 @@ fn list() { data: Option>, } - assert_eq!(List::decl(), "type List = { data: Array | null, }"); + assert_eq!(List::decl(), "type List = { data: Array | null, };"); } diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs index 47c4bd93e..f9d50a62d 100644 --- a/ts-rs/tests/ranges.rs +++ b/ts-rs/tests/ranges.rs @@ -22,7 +22,16 @@ struct RangeTest { fn range() { assert_eq!( RangeTest::decl(), - "type RangeTest = { a: { start: number, end: number, }, b: { start: string, end: string, }, c: { start: { start: number, end: number, }, end: { start: number, end: number, }, }, d: { start: number, end: number, }, e: { start: Inner, end: Inner, }, }" + "type RangeTest = { \ + a: { start: number, end: number, }, \ + b: { start: string, end: string, }, \ + c: { \ + start: { start: number, end: number, }, \ + end: { start: number, end: number, }, \ + }, \ + d: { start: number, end: number, }, \ + e: { start: Inner, end: Inner, }, \ + };" ); assert_eq!( RangeTest::dependencies() diff --git a/ts-rs/tests/raw_idents.rs b/ts-rs/tests/raw_idents.rs index 9fae87952..dc87d95c3 100644 --- a/ts-rs/tests/raw_idents.rs +++ b/ts-rs/tests/raw_idents.rs @@ -15,6 +15,6 @@ fn raw_idents() { let out = ::decl(); assert_eq!( out, - "type enum = { type: number, use: number, struct: number, let: number, enum: number, }" + "type enum = { type: number, use: number, struct: number, let: number, enum: number, };" ); } diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs index c7fbad45d..0357a5b64 100644 --- a/ts-rs/tests/self_referential.rs +++ b/ts-rs/tests/self_referential.rs @@ -36,7 +36,7 @@ fn named() { t_arc: T, \ self_arc: T, \ has_t: { t: T, }, \ - }" + };" ); } diff --git a/ts-rs/tests/serde-skip-with-default.rs b/ts-rs/tests/serde-skip-with-default.rs index bcb523ec3..d6a5be912 100644 --- a/ts-rs/tests/serde-skip-with-default.rs +++ b/ts-rs/tests/serde-skip-with-default.rs @@ -20,5 +20,5 @@ pub struct Foobar { #[test] fn serde_skip_with_default() { - assert_eq!(Foobar::decl(), "type Foobar = { something_else: number, }"); + assert_eq!(Foobar::decl(), "type Foobar = { something_else: number, };"); } diff --git a/ts-rs/tests/union_with_data.rs b/ts-rs/tests/union_with_data.rs index b35f3684a..43ff1a9cf 100644 --- a/ts-rs/tests/union_with_data.rs +++ b/ts-rs/tests/union_with_data.rs @@ -24,10 +24,10 @@ enum SimpleEnum { #[test] fn test_stateful_enum() { - assert_eq!(Bar::decl(), r#"type Bar = { field: number, }"#); + assert_eq!(Bar::decl(), r#"type Bar = { field: number, };"#); assert_eq!(Bar::dependencies(), vec![]); - assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, }"#); + assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, };"#); assert_eq!( Foo::dependencies(), vec![Dependency::from_ty::().unwrap()] diff --git a/ts-rs/tests/unsized.rs b/ts-rs/tests/unsized.rs index bfddf75ec..90776653b 100644 --- a/ts-rs/tests/unsized.rs +++ b/ts-rs/tests/unsized.rs @@ -15,6 +15,6 @@ fn contains_str() { assert_eq!( S::decl(), - "type S = { b: string, c: string, r: string, a: string, }" + "type S = { b: string, c: string, r: string, a: string, };" ) } From cab0908d1b664ea88bc1f8eeafec502ea0661a93 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 10:55:58 +0100 Subject: [PATCH 03/23] Dependency::from_ty - remove generic params from name --- macros/src/lib.rs | 2 +- ts-rs/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index a1991a8b3..db17ad700 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,5 +1,5 @@ #![macro_use] -//#![deny(unused)] +#![deny(unused)] use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index f37b80a5f..b215101c3 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -410,7 +410,7 @@ impl Dependency { let exported_to = T::get_export_to()?; Some(Dependency { type_id: TypeId::of::(), - ts_name: T::name(), + ts_name: T::name().split('<').next().unwrap().to_owned(), exported_to, }) } From d764f5c17ff1a8597df5335e44880146eaddbe9d Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 10:56:11 +0100 Subject: [PATCH 04/23] expand e2e test --- e2e/dependencies/consumer/src/main.rs | 8 ++++++-- e2e/dependencies/dependency1/src/lib.rs | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/e2e/dependencies/consumer/src/main.rs b/e2e/dependencies/consumer/src/main.rs index 5a79fbac4..01fadc891 100644 --- a/e2e/dependencies/consumer/src/main.rs +++ b/e2e/dependencies/consumer/src/main.rs @@ -1,10 +1,14 @@ use ts_rs::TS; -use dependency1::LibraryType; +use dependency1::*; #[derive(TS)] #[ts(export)] struct ConsumerType { - pub ty: LibraryType + pub ty1: LibraryType1, + pub ty2_1: LibraryType2, + pub ty2_2: LibraryType2<&'static Self>, + pub ty2_3: LibraryType2>>, + pub ty2_4: LibraryType2>, } fn main() {} \ No newline at end of file diff --git a/e2e/dependencies/dependency1/src/lib.rs b/e2e/dependencies/dependency1/src/lib.rs index aade3eb50..5a635f10d 100644 --- a/e2e/dependencies/dependency1/src/lib.rs +++ b/e2e/dependencies/dependency1/src/lib.rs @@ -1,6 +1,11 @@ use ts_rs::TS; #[derive(TS)] -pub struct LibraryType { +pub struct LibraryType1 { pub a: i32 -} \ No newline at end of file +} + +#[derive(TS)] +pub struct LibraryType2 { + pub t: T +} From 29248b4f49f8354b0a5a4cd7adf91242cfd06fec Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 10:56:22 +0100 Subject: [PATCH 05/23] adjust failing test --- ts-rs/tests/generics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index c8429e5b2..c7f6c713d 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -256,7 +256,7 @@ fn default() { // #[ts(inline)] // xi2: X } - assert_eq!(Y::decl(), "type Y = { a1: A, a2: A, };") + assert_eq!(Y::decl(), "type Y = { a1: A, a2: A, };") } #[test] From 4264c5f3ec088f4a5511dcd274ba9d33c131ab75 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:03:07 +0100 Subject: [PATCH 06/23] fix optional features --- ts-rs/src/chrono.rs | 6 ------ ts-rs/src/lib.rs | 2 +- ts-rs/tests/chrono.rs | 2 +- ts-rs/tests/indexmap.rs | 2 +- ts-rs/tests/semver.rs | 2 +- 5 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs index 7aee2a9b2..93c66094f 100644 --- a/ts-rs/src/chrono.rs +++ b/ts-rs/src/chrono.rs @@ -25,9 +25,6 @@ impl TS for DateTime { fn name() -> String { "string".to_owned() } - fn name_with_type_args(_: Vec) -> String { - Self::name() - } fn inline() -> String { "string".to_owned() } @@ -40,9 +37,6 @@ impl TS for Date { fn name() -> String { "string".to_owned() } - fn name_with_type_args(_: Vec) -> String { - Self::name() - } fn inline() -> String { "string".to_owned() } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index b215101c3..2fb030187 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -421,7 +421,7 @@ macro_rules! impl_primitives { ($($($ty:ty),* => $l:literal),*) => { $($( impl TS for $ty { fn name() -> String { $l.to_owned() } - fn inline() -> String { Self::name() } + fn inline() -> String { ::name() } fn transparent() -> bool { false } } )*)* }; diff --git a/ts-rs/tests/chrono.rs b/ts-rs/tests/chrono.rs index d9bc9eeaf..f4091bed4 100644 --- a/ts-rs/tests/chrono.rs +++ b/ts-rs/tests/chrono.rs @@ -27,6 +27,6 @@ fn chrono() { assert_eq!( Chrono::decl(), - "type Chrono = { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, }" + "type Chrono = { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, };" ) } diff --git a/ts-rs/tests/indexmap.rs b/ts-rs/tests/indexmap.rs index 7c8eb208c..66319b890 100644 --- a/ts-rs/tests/indexmap.rs +++ b/ts-rs/tests/indexmap.rs @@ -14,6 +14,6 @@ fn indexmap() { assert_eq!( Indexes::decl(), - "type Indexes = { map: Record, set: Array, }" + "type Indexes = { map: Record, set: Array, };" ) } diff --git a/ts-rs/tests/semver.rs b/ts-rs/tests/semver.rs index 81d307c44..61800edf0 100644 --- a/ts-rs/tests/semver.rs +++ b/ts-rs/tests/semver.rs @@ -11,5 +11,5 @@ fn semver() { version: Version, } - assert_eq!(Semver::decl(), "type Semver = { version: string, }") + assert_eq!(Semver::decl(), "type Semver = { version: string, };") } From 83b95413a40bb521e8faefb7e11252e7c3d6ac3c Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:10:06 +0100 Subject: [PATCH 07/23] fix TS_RS_EXPORT_DIR --- ts-rs/src/export.rs | 3 ++- ts-rs/src/lib.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 724129b71..8bf1e89f2 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -146,7 +146,8 @@ pub mod __private { pub fn get_export_to_path() -> Option { provided_default_dir().map_or_else( || T::EXPORT_TO.map(ToString::to_string), - |path| Some(format!("{path}/{}.ts", T::name())), + // TODO: maybe introduce T::NAME or T::ident() or something in that vein + |path| Some(format!("{path}/{}.ts", T::name().split('<').next().unwrap())), ) } } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 2fb030187..3553e0cff 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -410,6 +410,7 @@ impl Dependency { let exported_to = T::get_export_to()?; Some(Dependency { type_id: TypeId::of::(), + // TODO: maybe introduce T::NAME or T::ident() or something in that vein ts_name: T::name().split('<').next().unwrap().to_owned(), exported_to, }) From 61c9692299d3da00f59b9eca101efb2ba33e9389 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:33:06 +0100 Subject: [PATCH 08/23] add test for #70 --- ts-rs/tests/issue-70.rs | 57 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ts-rs/tests/issue-70.rs diff --git a/ts-rs/tests/issue-70.rs b/ts-rs/tests/issue-70.rs new file mode 100644 index 000000000..a90bd4d92 --- /dev/null +++ b/ts-rs/tests/issue-70.rs @@ -0,0 +1,57 @@ +#![allow(unused)] + +use std::collections::HashMap; +use ts_rs::TS; + +#[test] +fn issue_70() { + type TypeAlias = HashMap; + + #[derive(TS)] + enum Enum { + A(TypeAlias), + B(HashMap), + } + + #[derive(TS)] + struct Struct { + a: TypeAlias, + b: HashMap + } + + assert_eq!(Enum::decl(), "type Enum = { \"A\": Record } | { \"B\": Record };"); + assert_eq!(Struct::decl(), "type Struct = { a: Record, b: Record, };"); +} + +#[test] +fn generic() { + type GenericAlias = HashMap<(A, String), Vec<(B, i32)>>; + + #[derive(TS)] + struct Container { + a: GenericAlias, Vec>, + b: GenericAlias + } + assert_eq!( + Container::decl(), + "type Container = { \ + a: Record<[Array, string], Array<[Array, number]>>, \ + b: Record<[string, string], Array<[string, number]>>, \ + };" + ); + + #[derive(TS)] + struct GenericContainer { + a: GenericAlias, + b: GenericAlias, + c: GenericAlias> + } + assert_eq!( + GenericContainer::<(), ()>::decl(), + "type GenericContainer = { \ + a: Record<[string, string], Array<[string, number]>>, \ + b: Record<[A, string], Array<[B, number]>>, \ + c: Record<[A, string], Array<[Record<[A, string], Array<[B, number]>>, number]>>, \ + };" + ); +} \ No newline at end of file From ebfa532d50fe9525b10bb1c4801dc627353e7cbe Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:34:29 +0100 Subject: [PATCH 09/23] enable test for #214 --- ts-rs/tests/generics.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index c7f6c713d..fb0327bc8 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -170,11 +170,7 @@ fn inline() { } #[test] -#[ignore = "We haven't figured out how to inline generics with bounds yet"] -#[allow(unreachable_code)] fn inline_with_bounds() { - todo!("FIX ME: https://github.com/Aleph-Alpha/ts-rs/issues/214"); - #[derive(TS)] struct Generic { t: T, @@ -195,10 +191,9 @@ fn inline_with_bounds() { Generic::<&'static str>::decl(), "type Generic = { t: T, };" ); - // ^^^^^^^^^^^^ Replace with something else assert_eq!( Container::decl(), - "type Container = { g: Generic, gi: { t: string, }, t: number, };" // Actual output: { g: Generic, gi: { t: T, }, t: T, } + "type Container = { g: Generic, gi: { t: string, }, t: number, };" ); } From 129b32384b5048dd0c6e3ff034c0091d6acf83a1 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:35:08 +0100 Subject: [PATCH 10/23] remove TODO about race condition - we fixed that with a mutex. --- ts-rs/src/export.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index 8bf1e89f2..cf3232b27 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -57,11 +57,6 @@ mod recursive_export { /// Exports `T` to the file specified by the `#[ts(export_to = ..)]` attribute. /// Additionally, all dependencies of `T` will be exported as well. - /// - /// TODO: This might cause a race condition: - /// If two types `A` and `B` are `#[ts(export)]` and depend on type `C`, - /// then both tests for exporting `A` and `B` will try to write `C` to `C.ts`. - /// Since rust, by default, executes tests in paralell, this might cause `C.ts` to be corrupted. pub(crate) fn export_type_with_dependencies( ) -> Result<(), ExportError> { let mut seen = HashSet::new(); From 0a68cef24b49e84fd46408a25d12a15f8b7542b5 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:35:48 +0100 Subject: [PATCH 11/23] remove TODO about #56 - already fixed --- ts-rs/tests/generics.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index fb0327bc8..7f0264969 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -244,12 +244,6 @@ fn default() { struct Y { a1: A, a2: A, - // https://github.com/Aleph-Alpha/ts-rs/issues/56 - // TODO: fixme - // #[ts(inline)] - // xi: X, - // #[ts(inline)] - // xi2: X } assert_eq!(Y::decl(), "type Y = { a1: A, a2: A, };") } From 2b5b9a9e302ad3430c31961464157fd854dadf86 Mon Sep 17 00:00:00 2001 From: Moritz Bischof Date: Fri, 16 Feb 2024 11:38:13 +0100 Subject: [PATCH 12/23] remove "limitations" section from readme - both #56 and #70 are fixed --- README.md | 4 ---- ts-rs/src/lib.rs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/README.md b/README.md index 971eeed9d..9501631aa 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,6 @@ When running `cargo test`, the TypeScript bindings will be exported to the file - generic types - support for ESM imports -### limitations -- generic fields cannot be inlined or flattened (#56) -- type aliases must not alias generic types (#70) - ### cargo features - `serde-compat` (default) diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 3553e0cff..494526490 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -66,10 +66,6 @@ //! - generic types //! - support for ESM imports //! -//! ## limitations -//! - generic fields cannot be inlined or flattened (#56) -//! - type aliases must not alias generic types (#70) -//! //! ## cargo features //! - `serde-compat` (default) //! From 5ab4aa9d7cdecc1de9e81652c9d5f60eafc5e538 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 16 Feb 2024 08:57:42 -0300 Subject: [PATCH 13/23] Use type_params to simplify capture of Generics' identifiers --- macros/src/lib.rs | 27 ++++++--------------------- ts-rs/tests/generics.rs | 2 +- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index db17ad700..37cd12407 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -96,13 +96,8 @@ impl DerivedTS { let name = &self.ts_name; let mut generics_ts_names = self .generics - .params - .iter() - .filter_map(|g| match g { - GenericParam::Lifetime(_) => None, - GenericParam::Type(ty) => Some(&ty.ident), - GenericParam::Const(_) => None, - }) + .type_params() + .map(|ty| &ty.ident) .map(|generic| quote!(<#generic as ts_rs::TS>::name())) .peekable(); @@ -129,11 +124,7 @@ impl DerivedTS { /// impl ts_rs::TS for B { /* .. */ } /// ``` fn generate_generic_types(&self) -> TokenStream { - let generics = self.generics.params.iter().filter_map(|g| match g { - GenericParam::Lifetime(_) => None, - GenericParam::Type(t) => Some(t.ident.clone()), - GenericParam::Const(_) => None, - }); + let generics = self.generics.type_params().map(|ty| ty.ident.clone()); quote! { #( @@ -155,9 +146,7 @@ impl DerivedTS { fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option { let test_fn = format_ident!("export_bindings_{}", rust_ty.to_string().to_lowercase()); let generic_params = generics - .params - .iter() - .filter(|param| matches!(param, GenericParam::Type(_))) + .type_params() .map(|_| quote! { () }); let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>); @@ -268,12 +257,8 @@ fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream { fn add_ts_to_where_clause(generics: &Generics) -> Option { let generic_types = generics - .params - .iter() - .filter_map(|gp| match gp { - GenericParam::Type(ty) => Some(ty.ident.clone()), - _ => None, - }) + .type_params() + .map(|ty| ty.ident.clone()) .collect::>(); if generic_types.is_empty() { return generics.where_clause.clone(); diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 7f0264969..8870db3c7 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -367,4 +367,4 @@ fn inline_generic_enum() { e1: { \"VariantA\": number } | { \"VariantB\": SomeType }, \ };" ); -} \ No newline at end of file +} From 3e31a3a95a6bc70362cf77ef53429406fe5bd913 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 16 Feb 2024 11:02:00 -0300 Subject: [PATCH 14/23] Add ident method --- macros/src/lib.rs | 4 ++++ ts-rs/src/chrono.rs | 6 ++++++ ts-rs/src/lib.rs | 19 +++++++++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 37cd12407..d5c005d6a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -67,6 +67,10 @@ impl DerivedTS { #impl_start { const EXPORT_TO: Option<&'static str> = Some(#export_to); + fn ident() -> String { + stringify!(#rust_ty).to_owned() + } + #get_export_to #docs #name diff --git a/ts-rs/src/chrono.rs b/ts-rs/src/chrono.rs index 93c66094f..797289e3a 100644 --- a/ts-rs/src/chrono.rs +++ b/ts-rs/src/chrono.rs @@ -22,6 +22,9 @@ impl_primitives!(NaiveDateTime, NaiveDate, NaiveTime, Month, Weekday, Duration = impl_dummy!(Utc, Local, FixedOffset); impl TS for DateTime { + fn ident() -> String { + "string".to_owned() + } fn name() -> String { "string".to_owned() } @@ -34,6 +37,9 @@ impl TS for DateTime { } impl TS for Date { + fn ident() -> String { + "string".to_owned() + } fn name() -> String { "string".to_owned() } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 494526490..b83823e04 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -278,6 +278,16 @@ pub trait TS { const EXPORT_TO: Option<&'static str> = None; const DOCS: Option<&'static str> = None; + fn ident() -> String { + let name = Self::name(); + + if name.contains('<') { + panic!("generic types must implement ident") + } else { + name + } + } + fn get_export_to() -> Option { Self::EXPORT_TO.map(ToString::to_string) } @@ -406,8 +416,7 @@ impl Dependency { let exported_to = T::get_export_to()?; Some(Dependency { type_id: TypeId::of::(), - // TODO: maybe introduce T::NAME or T::ident() or something in that vein - ts_name: T::name().split('<').next().unwrap().to_owned(), + ts_name: T::ident(), exported_to, }) } @@ -525,6 +534,9 @@ impl TS for Result { } impl TS for Vec { + fn ident() -> String { + "Array".to_owned() + } fn name() -> String { format!("Array<{}>", T::name()) } @@ -588,6 +600,9 @@ impl TS for [T; N] { } impl TS for HashMap { + fn ident() -> String { + "Record".to_owned() + } fn name() -> String { format!("Record<{}, {}>", K::name(), V::name()) } From bae3df8a9b328b67cdd361614c1e723cea6f300d Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 16 Feb 2024 11:17:43 -0300 Subject: [PATCH 15/23] Add dummy type to allow exporting types that use ToString as a generic trait bound --- macros/src/lib.rs | 2 +- ts-rs/src/lib.rs | 19 +++++++++++++++++++ ts-rs/tests/generics.rs | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index d5c005d6a..f212be927 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -151,7 +151,7 @@ impl DerivedTS { let test_fn = format_ident!("export_bindings_{}", rust_ty.to_string().to_lowercase()); let generic_params = generics .type_params() - .map(|_| quote! { () }); + .map(|_| quote! { ts_rs::Dummy }); let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>); Some(quote! { diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index b83823e04..032ce33d3 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -727,3 +727,22 @@ impl_primitives! { } #[rustfmt::skip] pub(crate) use impl_primitives; + + +#[doc(hidden)] +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct Dummy; + +#[doc(hidden)] +impl std::fmt::Display for Dummy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[doc(hidden)] +impl TS for Dummy { + fn name() -> String { "Dummy".to_owned() } + fn transparent() -> bool { false } +} + diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index 8870db3c7..de4e5d65c 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -10,6 +10,7 @@ use std::{ use ts_rs::TS; #[derive(TS)] +#[ts(export)] struct Generic where T: TS, From a693b606838a32e3cfd495cf214c2887151ab8a8 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 16 Feb 2024 11:21:55 -0300 Subject: [PATCH 16/23] use DerivedTS::ts_name instead of rust_ty --- macros/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f212be927..1e05a9ba4 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -57,6 +57,7 @@ impl DerivedTS { docs => Some(quote!(const DOCS: Option<&'static str> = Some(#docs);)), }; + let ident = self.ts_name.clone(); let impl_start = generate_impl_block_header(&rust_ty, &generics); let name = self.generate_name_fn(); let inline = self.generate_inline_fn(); @@ -68,7 +69,7 @@ impl DerivedTS { const EXPORT_TO: Option<&'static str> = Some(#export_to); fn ident() -> String { - stringify!(#rust_ty).to_owned() + #ident.to_owned() } #get_export_to From bdb06d4b671e82ba4a297908723a5afe6dd94d8d Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 16 Feb 2024 11:26:00 -0300 Subject: [PATCH 17/23] use TS::ident --- ts-rs/src/export.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs index cf3232b27..7ecccc93a 100644 --- a/ts-rs/src/export.rs +++ b/ts-rs/src/export.rs @@ -141,8 +141,7 @@ pub mod __private { pub fn get_export_to_path() -> Option { provided_default_dir().map_or_else( || T::EXPORT_TO.map(ToString::to_string), - // TODO: maybe introduce T::NAME or T::ident() or something in that vein - |path| Some(format!("{path}/{}.ts", T::name().split('<').next().unwrap())), + |path| Some(format!("{path}/{}.ts", T::ident())), ) } } From b4f6b01b21ec4396093a9732edc4c463a286c9d7 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 19 Feb 2024 10:56:59 -0300 Subject: [PATCH 18/23] Prefer renaming Enum to using all variants --- macros/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 1e05a9ba4..f4c9f40ab 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -228,22 +228,22 @@ impl DerivedTS { // generate start of the `impl TS for #ty` block, up to (excluding) the open brace fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream { - use GenericParam::*; + use GenericParam as G; let bounds = generics.params.iter().map(|param| match param { - Type(TypeParam { + G::Type(TypeParam { ident, colon_token, bounds, .. }) => quote!(#ident #colon_token #bounds), - Lifetime(LifetimeParam { + G::Lifetime(LifetimeParam { lifetime, colon_token, bounds, .. }) => quote!(#lifetime #colon_token #bounds), - Const(ConstParam { + G::Const(ConstParam { const_token, ident, colon_token, @@ -252,8 +252,8 @@ fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream { }) => quote!(#const_token #ident #colon_token #ty), }); let type_args = generics.params.iter().map(|param| match param { - Type(TypeParam { ident, .. }) | Const(ConstParam { ident, .. }) => quote!(#ident), - Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime), + G::Type(TypeParam { ident, .. }) | G::Const(ConstParam { ident, .. }) => quote!(#ident), + G::Lifetime(LifetimeParam { lifetime, .. }) => quote!(#lifetime), }); let where_bound = add_ts_to_where_clause(generics); From 6d9a21f43cafaed20e19ab210354f42cb59a8a73 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 19 Feb 2024 11:42:40 -0300 Subject: [PATCH 19/23] Remove redundant clones --- macros/src/types/enum.rs | 2 +- macros/src/types/unit.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 1527df971..f99a4f0dc 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -201,7 +201,7 @@ fn format_variant( fn empty_enum(name: impl Into, enum_attr: EnumAttr, generics: Generics) -> DerivedTS { let name = name.into(); DerivedTS { - generics: generics.clone(), + generics, inline: quote!("never".to_owned()), docs: enum_attr.docs, inline_flattened: None, diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 9982e9fc5..5d50a5cf8 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -37,7 +37,7 @@ pub(crate) fn null(attr: &StructAttr, name: &str, generics: Generics) -> Result< check_attributes(attr)?; Ok(DerivedTS { - generics: generics.clone(), + generics, inline: quote!("null".to_owned()), inline_flattened: None, docs: attr.docs.clone(), From 3354dc744c14c0c44e8b1c36fc239146d8a8ca7e Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 19 Feb 2024 11:43:18 -0300 Subject: [PATCH 20/23] Remove unused Option --- macros/src/lib.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index f4c9f40ab..af42b54fd 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -47,9 +47,10 @@ impl DerivedTS { } }; - let export = match self.export { - true => Some(self.generate_export_test(&rust_ty, &generics)), - false => None, + let export = if self.export { + Some(self.generate_export_test(&rust_ty, &generics)) + } else { + None }; let docs = match &*self.docs { @@ -148,20 +149,20 @@ impl DerivedTS { } } - fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option { + fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream { let test_fn = format_ident!("export_bindings_{}", rust_ty.to_string().to_lowercase()); let generic_params = generics .type_params() .map(|_| quote! { ts_rs::Dummy }); let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>); - Some(quote! { + quote! { #[cfg(test)] #[test] fn #test_fn() { #ty::export().expect("could not export type"); } - }) + } } fn generate_name_fn(&self) -> TokenStream { @@ -202,15 +203,18 @@ impl DerivedTS { let name = &self.ts_name; let generic_types = self.generate_generic_types(); let ts_generics = format_generics(&mut self.dependencies, &self.generics); + + use GenericParam as G; // These are the generic parameters we'll be using. let generic_idents = self.generics.params.iter().filter_map(|p| match p { - GenericParam::Lifetime(_) => None, + G::Lifetime(_) => None, // Since we named our dummy types the same as the generic parameters, we can just keep // the identifier of the generic parameter - its name is shadowed by the dummy struct. - GenericParam::Type(TypeParam { ident, .. }) => Some(quote!(#ident)), + // // We keep const parameters as they are, since there's no sensible default value we can // use instead. This might be something to change in the future. - GenericParam::Const(ConstParam { ident, .. }) => Some(quote!(#ident)), + G::Type(TypeParam { ident, .. }) | + G::Const(ConstParam { ident, .. }) => Some(quote!(#ident)), }); quote! { fn decl_concrete() -> String { From 268742d47942a7897924ebbb3de91b33baef5431 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 19 Feb 2024 11:43:52 -0300 Subject: [PATCH 21/23] Fix inverted condition and separate the two checks --- macros/src/utils.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 4bcadf6f1..234d29d02 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -59,20 +59,24 @@ pub fn to_ts_ident(ident: &Ident) -> String { /// Convert an arbitrary name to a valid Typescript field name. /// -/// If the name contains special characters it will be wrapped in quotes. +/// If the name contains special characters or if its first character +/// is a number it will be wrapped in quotes. pub fn raw_name_to_ts_field(value: String) -> String { - let valid = value + let valid_chars = value .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '$') - && value - .chars() - .next() - .map(|first| !first.is_numeric()) - .unwrap_or(true); - if !valid { - format!(r#""{value}""#) - } else { + .all(|c| c.is_alphanumeric() || c == '_' || c == '$'); + + let does_not_start_with_digit = value + .chars() + .next() + .map_or(true, |first| !first.is_numeric()); + + let valid = valid_chars && does_not_start_with_digit; + + if valid { value + } else { + format!(r#""{value}""#) } } From 247276a2fdfc2d362939cce4d3483ccf08d971f1 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 19 Feb 2024 11:59:47 -0300 Subject: [PATCH 22/23] Remove redundant #[doc(hidden)] --- ts-rs/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 032ce33d3..9146b2873 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -733,14 +733,12 @@ pub(crate) use impl_primitives; #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct Dummy; -#[doc(hidden)] impl std::fmt::Display for Dummy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } -#[doc(hidden)] impl TS for Dummy { fn name() -> String { "Dummy".to_owned() } fn transparent() -> bool { false } From 01615feb3a2c93838fc3375982d888c2acd9fc66 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Wed, 21 Feb 2024 14:59:00 -0300 Subject: [PATCH 23/23] Replace if Some else None with bool::then --- macros/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/macros/src/lib.rs b/macros/src/lib.rs index af42b54fd..7844fa77b 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -47,11 +47,9 @@ impl DerivedTS { } }; - let export = if self.export { - Some(self.generate_export_test(&rust_ty, &generics)) - } else { - None - }; + let export = self.export.then( + || self.generate_export_test(&rust_ty, &generics) + ); let docs = match &*self.docs { "" => None,