Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Flattening generic type also flattens all generic parameters #233

Merged
merged 24 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
495a6bf
add test
NyxCode Feb 12, 2024
5daeed2
re-implement handeling of generics
NyxCode Feb 15, 2024
cab0908
Dependency::from_ty - remove generic params from name
NyxCode Feb 16, 2024
d764f5c
expand e2e test
NyxCode Feb 16, 2024
29248b4
adjust failing test
NyxCode Feb 16, 2024
4264c5f
fix optional features
NyxCode Feb 16, 2024
83b9541
fix TS_RS_EXPORT_DIR
NyxCode Feb 16, 2024
61c9692
add test for #70
NyxCode Feb 16, 2024
ebfa532
enable test for #214
NyxCode Feb 16, 2024
129b323
remove TODO about race condition - we fixed that with a mutex.
NyxCode Feb 16, 2024
0a68cef
remove TODO about #56 - already fixed
NyxCode Feb 16, 2024
2b5b9a9
remove "limitations" section from readme - both #56 and #70 are fixed
NyxCode Feb 16, 2024
5ab4aa9
Use type_params to simplify capture of Generics' identifiers
escritorio-gustavo Feb 16, 2024
3e31a3a
Add ident method
escritorio-gustavo Feb 16, 2024
bae3df8
Add dummy type to allow exporting types that use ToString as a generi…
escritorio-gustavo Feb 16, 2024
a693b60
use DerivedTS::ts_name instead of rust_ty
escritorio-gustavo Feb 16, 2024
bdb06d4
use TS::ident
escritorio-gustavo Feb 16, 2024
b4f6b01
Prefer renaming Enum to using all variants
escritorio-gustavo Feb 19, 2024
6d9a21f
Remove redundant clones
escritorio-gustavo Feb 19, 2024
3354dc7
Remove unused Option
escritorio-gustavo Feb 19, 2024
268742d
Fix inverted condition and separate the two checks
escritorio-gustavo Feb 19, 2024
3606c48
Merge branch 'main' into bug/flatten-generic-enum
escritorio-gustavo Feb 19, 2024
247276a
Remove redundant #[doc(hidden)]
escritorio-gustavo Feb 19, 2024
01615fe
Replace if Some else None with bool::then
escritorio-gustavo Feb 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 6 additions & 2 deletions e2e/dependencies/consumer/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
pub ty2_2: LibraryType2<&'static Self>,
pub ty2_3: LibraryType2<LibraryType2<Box<ConsumerType>>>,
pub ty2_4: LibraryType2<LibraryType2<LibraryType1>>,
}

fn main() {}
9 changes: 7 additions & 2 deletions e2e/dependencies/dependency1/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use ts_rs::TS;

#[derive(TS)]
pub struct LibraryType {
pub struct LibraryType1 {
pub a: i32
}
}

#[derive(TS)]
pub struct LibraryType2<T> {
pub t: T
}
6 changes: 3 additions & 3 deletions macros/src/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>()]);
}

Expand Down
245 changes: 168 additions & 77 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use syn::{
};

use crate::deps::Dependencies;
use crate::utils::format_generics;

#[macro_use]
mod utils;
Expand All @@ -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<TokenStream>,
dependencies: Dependencies,

Expand All @@ -29,29 +30,11 @@ struct DerivedTS {
}

impl DerivedTS {
fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> Option<TokenStream> {
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 => {
Expand All @@ -60,60 +43,39 @@ impl DerivedTS {
ts_rs::__private::get_export_to_path::<Self>()
}
};
format!("bindings/{}.ts", self.name)
format!("bindings/{}.ts", self.ts_name)
}
};

let export = match self.export {
true => Some(self.generate_export_test(&rust_ty, &generics)),
false => None,
};

let DerivedTS {
name,
docs,
inline,
decl,
inline_flattened,
dependencies,
..
} = self;
let export = self.export.then(
|| self.generate_export_test(&rust_ty, &generics)
);

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 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();
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

#docs

fn decl() -> String {
#decl
}
fn name() -> String {
#name.to_owned()
fn ident() -> String {
#ident.to_owned()
}
fn inline() -> String {
#inline
}
#inline_flattened

#get_export_to
#docs
#name
#decl
#inline

#[allow(clippy::unused_unit)]
fn dependency_types() -> impl ts_rs::typelist::TypeList
Expand All @@ -131,26 +93,159 @@ 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
.type_params()
NyxCode marked this conversation as resolved.
Show resolved Hide resolved
.map(|ty| &ty.ident)
.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<A, B, const C: usize> { /* ... */ }
/// ```
/// 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.type_params().map(|ty| ty.ident.clone());

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) -> 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 });
escritorio-gustavo marked this conversation as resolved.
Show resolved Hide resolved
let ty = quote!(<#rust_ty<#(#generic_params),*> as ts_rs::TS>);

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);

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 {
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.
//
// 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.
G::Type(TypeParam { ident, .. }) |
G::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 {
use GenericParam::*;
fn generate_impl_block_header(ty: &Ident, generics: &Generics) -> TokenStream {
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,
Expand All @@ -159,8 +254,8 @@ fn generate_impl(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);
Expand All @@ -169,12 +264,8 @@ fn generate_impl(ty: &Ident, generics: &Generics) -> TokenStream {

fn add_ts_to_where_clause(generics: &Generics) -> Option<WhereClause> {
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::<Vec<_>>();
if generic_types.is_empty() {
return generics.where_clause.clone();
Expand Down
Loading
Loading