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

Convert fixed size Rust array to fixed size TS tuple #208

Closed
wants to merge 11 commits into from
16 changes: 14 additions & 2 deletions macros/src/types/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
Ok(DerivedTS {
inline: quote!([#(#formatted_variants),*].join(" | ")),
decl: quote!(format!("type {}{} = {};", #name, #generic_args, Self::inline())),
inline_flattened: None,
inline_flattened: Some(
quote!(
format!("({})", [#(#formatted_variants),*].join(" | "))
)
),
dependencies,
name,
export: enum_attr.export,
Expand Down Expand Up @@ -130,7 +134,15 @@ fn format_variant(
"{{ \"{}\": \"{}\", {} }}",
#tag,
#name,
#inline_flattened
// At this point inline_flattened looks like
// { /* ...data */ }
//
// To be flattened, an internally tagged enum must not be
// surrounded by braces, otherwise each variant will look like
// { "tag": "name", { /* ...data */ } }
// when we want it to look like
// { "tag": "name", /* ...data */ }
#inline_flattened.trim_matches(&['{', '}', ' '])
)
},
None => match &variant.fields {
Expand Down
16 changes: 15 additions & 1 deletion macros/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
GenericArgument, GenericParam, Generics, ItemStruct, PathArguments, Type, TypeArray, TypeGroup,
TypeReference, TypeSlice, TypeTuple,
TypeReference, TypeSlice, TypeTuple, Expr, ExprLit, Lit,
};

use crate::{attr::StructAttr, deps::Dependencies};
Expand Down Expand Up @@ -62,6 +62,20 @@ pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generi

// special treatment for arrays and tuples
match ty {
// When possible, convert a Rust array into a fixed size TS tuple
// this is only possible when the length of the array is a numeric literal,
// rather than the name of a constant
Type::Array(TypeArray {
ref elem,
len: Expr::Lit(ExprLit { lit: Lit::Int(lit_int), .. }),
..
}) => {
let inner_ty = elem;
let len = lit_int.base10_parse::<usize>().unwrap();
let item_ty = (0..len).map(|_| quote!(#inner_ty));
let tuple_ty = syn::parse2::<Type>(quote!{ (#(#item_ty),*) }).unwrap();
return format_type(&tuple_ty, dependencies, generics)
}
// The field is an array (`[T; n]`) or a slice (`[T]`) so it technically doesn't have a
// generic argument. Therefore, we handle it explicitly here like a `Vec<T>`
Type::Array(TypeArray { ref elem, .. }) | Type::Slice(TypeSlice { ref elem, .. }) => {
Expand Down
32 changes: 24 additions & 8 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub(crate) fn named(
generics: &Generics,
) -> Result<DerivedTS> {
let mut formatted_fields = Vec::new();
let mut flattened_fields = Vec::new();
let mut dependencies = Dependencies::default();
if let Some(tag) = &attr.tag {
let formatted = format!("{}: \"{}\",", tag, name);
Expand All @@ -28,6 +29,7 @@ pub(crate) fn named(
for field in &fields.named {
format_field(
&mut formatted_fields,
&mut flattened_fields,
&mut dependencies,
field,
&attr.rename_all,
Expand All @@ -36,17 +38,21 @@ 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()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, 1) => quote!(#flattened.trim_matches(|c| c == '(' || c == ')').to_owned()),
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

Ok(DerivedTS {
inline: quote! {
format!(
"{{ {} }}",
#fields,
)
},
inline: quote!(#inline.replace(" } & { ", " ")),
decl: quote!(format!("type {}{} = {}", #name, #generic_args, Self::inline())),
inline_flattened: Some(fields),
inline_flattened: Some(quote!(format!("{{ {} }}", #fields))),
name: name.to_owned(),
dependencies,
export: attr.export,
Expand All @@ -55,8 +61,18 @@ pub(crate) fn named(
}

// build an expresion which expands to a string, representing a single field of a struct.
//
// formatted_fields will contain all the fields that do not contain the flatten
// attribute, in the format
// key: type,
//
// flattened_fields will contain all the fields that contain the flatten attribute
// in their respective formats, which for a named struct is the same as formatted_fields,
// but for enums is
// ({ /* variant data */ } | { /* variant data */ })
fn format_field(
formatted_fields: &mut Vec<TokenStream>,
flattened_fields: &mut Vec<TokenStream>,
dependencies: &mut Dependencies,
field: &Field,
rename_all: &Option<Inflection>,
Expand Down Expand Up @@ -88,7 +104,7 @@ fn format_field(
_ => {}
}

formatted_fields.push(quote!(<#ty as ts_rs::TS>::inline_flattened()));
flattened_fields.push(quote!(<#ty as ts_rs::TS>::inline_flattened()));
dependencies.append_from(ty);
return Ok(());
}
Expand Down
29 changes: 27 additions & 2 deletions ts-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,30 @@ impl<T: TS> TS for Vec<T> {
"Array".to_owned()
}

fn inline() -> String {
format!("Array<{}>", T::inline())
}

fn dependencies() -> Vec<Dependency>
where
Self: 'static,
{
[Dependency::from_ty::<T>()].into_iter().flatten().collect()
}

fn transparent() -> bool {
true
}
}

impl<T: TS, const N: usize> TS for [T; N] {
fn name() -> String {
format!(
"[{}]",
(0..N).map(|_| T::name()).collect::<Box<[_]>>().join(", ")
)
}

fn name_with_type_args(args: Vec<String>) -> String {
assert_eq!(
args.len(),
Expand All @@ -506,7 +530,8 @@ impl<T: TS> TS for Vec<T> {
}

fn inline() -> String {
format!("Array<{}>", T::inline())
Self::name()
// format!("Array<{}>", T::inline())
}

fn dependencies() -> Vec<Dependency>
Expand Down Expand Up @@ -613,7 +638,7 @@ impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T);
impl_shadow!(as Vec<T>: impl<T: TS, H> TS for HashSet<T, H>);
impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for [T; N]);
// impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for [T; N]);
impl_shadow!(as Vec<T>: impl<T: TS> TS for [T]);

impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
Expand Down
10 changes: 5 additions & 5 deletions ts-rs/tests/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ use ts_rs::TS;

#[test]
fn free() {
assert_eq!(<[String; 10]>::inline(), "Array<string>")
assert_eq!(<[String; 4]>::inline(), "[string, string, string, string]")
}

#[test]
fn interface() {
#[derive(TS)]
struct Interface {
#[allow(dead_code)]
a: [i32; 10],
a: [i32; 4],
}

assert_eq!(Interface::inline(), "{ a: Array<number>, }")
assert_eq!(Interface::inline(), "{ a: [number, number, number, number], }")
}

#[test]
fn newtype() {
#[derive(TS)]
struct Newtype(#[allow(dead_code)] [i32; 10]);
struct Newtype(#[allow(dead_code)] [i32; 4]);

assert_eq!(Newtype::inline(), "Array<number>")
assert_eq!(Newtype::inline(), "[number, number, number, number]")
}
108 changes: 108 additions & 0 deletions ts-rs/tests/enum_flattening.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#[cfg(feature = "serde-compat")]
use serde::Serialize;
use ts_rs::TS;

#[test]
fn externally_tagged() {
#[allow(dead_code)]
#[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
#[cfg_attr(not(feature = "serde-compat"), derive(TS))]
struct Foo {
qux: i32,
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: Bar,
biz: Option<String>,
}

#[cfg_attr(feature = "serde-compat", derive(Serialize, TS))]
#[cfg_attr(not(feature = "serde-compat"), derive(TS))]
#[allow(dead_code)]
enum Bar {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String, d: Option<i32> },
}

assert_eq!(
Foo::inline(),
r#"{ qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
)
}

#[test]
#[cfg(feature = "serde-compat")]
fn adjacently_tagged() {
#[derive(Serialize, TS)]
struct Foo {
one: i32,
#[serde(flatten)]
baz: Bar,
qux: Option<String>,
}

#[derive(Serialize, TS)]
#[allow(dead_code)]
#[serde(tag = "type", content = "stuff")]
enum Bar {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String, d: Option<i32> },
}

assert_eq!(
Foo::inline(),
r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { "type": "Buz", "stuff": { c: string, d: number | null, } })"#
)
}

#[test]
#[cfg(feature = "serde-compat")]
fn internally_tagged() {
#[derive(Serialize, TS)]
struct Foo {
qux: Option<String>,

#[serde(flatten)]
baz: Bar,
}

#[derive(Serialize, TS)]
#[allow(dead_code)]
#[serde(tag = "type")]
enum Bar {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String, d: Option<i32> },
}

assert_eq!(
Foo::inline(),
r#"{ qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
)
}

#[test]
#[cfg(feature = "serde-compat")]
fn untagged() {
#[derive(Serialize, TS)]
struct Foo {
#[serde(flatten)]
baz: Bar,
}

#[derive(Serialize, TS)]
#[allow(dead_code)]
#[serde(untagged)]
enum Bar {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String },
}

assert_eq!(
Foo::inline(),
r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
)
}

2 changes: 1 addition & 1 deletion ts-rs/tests/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ struct C {
fn test_def() {
assert_eq!(
C::inline(),
"{ b: { a: number, b: number, c: number, }, d: number, }"
"{ b: { c: number, a: number, b: number, }, d: number, }"
);
}
8 changes: 4 additions & 4 deletions ts-rs/tests/generic_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn named() {
}
assert_eq!(
Struct::inline(),
"{ a: Array<string>, b: [Array<string>, Array<string>], c: Array<Array<string>>, }"
"{ a: Array<string>, b: [Array<string>, Array<string>], c: [Array<string>, Array<string>, Array<string>], }"
);
}

Expand All @@ -52,7 +52,7 @@ fn named_nested() {
b: (Vec<Vec<String>>, Vec<Vec<String>>),
c: [Vec<Vec<String>>; 3],
}
assert_eq!(Struct::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: Array<Array<Array<string>>>, }");
assert_eq!(Struct::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: [Array<Array<string>>, Array<Array<string>>, Array<Array<string>>], }");
}

#[test]
Expand All @@ -61,7 +61,7 @@ fn tuple() {
struct Tuple(Vec<i32>, (Vec<i32>, Vec<i32>), [Vec<i32>; 3]);
assert_eq!(
Tuple::inline(),
"[Array<number>, [Array<number>, Array<number>], Array<Array<number>>]"
"[Array<number>, [Array<number>, Array<number>], [Array<number>, Array<number>, Array<number>]]"
);
}

Expand All @@ -75,6 +75,6 @@ fn tuple_nested() {
);
assert_eq!(
Tuple::inline(),
"[Array<Array<number>>, [Array<Array<number>>, Array<Array<number>>], Array<Array<Array<number>>>]"
"[Array<Array<number>>, [Array<Array<number>>, Array<Array<number>>], [Array<Array<number>>, Array<Array<number>>, Array<Array<number>>]]"
);
}
2 changes: 1 addition & 1 deletion ts-rs/tests/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ fn generic_struct() {

assert_eq!(
Struct::<()>::decl(),
"type Struct<T> = { a: T, b: [T, T], c: [T, [T, T]], d: Array<T>, e: Array<[T, T]>, f: Array<T>, g: Array<Array<T>>, h: Array<Array<[T, T]>>, }"
"type Struct<T> = { a: T, b: [T, T], c: [T, [T, T]], d: [T, T, T], e: [[T, T], [T, T], [T, T]], f: Array<T>, g: Array<Array<T>>, h: Array<[[T, T], [T, T], [T, T]]>, }"
)
}

Expand Down
Loading