Skip to content

Commit

Permalink
Merge pull request #233 from Aleph-Alpha/bug/flatten-generic-enum
Browse files Browse the repository at this point in the history
Bug: Flattening generic type also flattens all generic parameters
  • Loading branch information
escritorio-gustavo authored Feb 22, 2024
2 parents 1d6eec7 + 01615fe commit 4c3349c
Show file tree
Hide file tree
Showing 32 changed files with 495 additions and 486 deletions.
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()
.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 });
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

0 comments on commit 4c3349c

Please sign in to comment.