diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 4c73b756a..0cfcfd9b2 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -50,7 +50,11 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { 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, @@ -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 { diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs index 4766de970..9c3ca6d42 100644 --- a/macros/src/types/generics.rs +++ b/macros/src/types/generics.rs @@ -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}; @@ -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::().unwrap(); + let item_ty = (0..len).map(|_| quote!(#inner_ty)); + let tuple_ty = syn::parse2::(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` Type::Array(TypeArray { ref elem, .. }) | Type::Slice(TypeSlice { ref elem, .. }) => { diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 1a3249b42..3ec97d014 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -17,6 +17,7 @@ pub(crate) fn named( generics: &Generics, ) -> Result { 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); @@ -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, @@ -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, @@ -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, + flattened_fields: &mut Vec, dependencies: &mut Dependencies, field: &Field, rename_all: &Option, @@ -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(()); } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 464e44cb5..72f125f59 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -495,6 +495,30 @@ impl TS for Vec { "Array".to_owned() } + fn inline() -> String { + format!("Array<{}>", T::inline()) + } + + fn dependencies() -> Vec + where + Self: 'static, + { + [Dependency::from_ty::()].into_iter().flatten().collect() + } + + fn transparent() -> bool { + true + } +} + +impl TS for [T; N] { + fn name() -> String { + format!( + "[{}]", + (0..N).map(|_| T::name()).collect::>().join(", ") + ) + } + fn name_with_type_args(args: Vec) -> String { assert_eq!( args.len(), @@ -506,7 +530,8 @@ impl TS for Vec { } fn inline() -> String { - format!("Array<{}>", T::inline()) + Self::name() + // format!("Array<{}>", T::inline()) } fn dependencies() -> Vec @@ -613,7 +638,7 @@ impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T); impl_shadow!(as Vec: impl TS for HashSet); impl_shadow!(as Vec: impl TS for BTreeSet); impl_shadow!(as HashMap: impl TS for BTreeMap); -impl_shadow!(as Vec: impl TS for [T; N]); +// impl_shadow!(as Vec: impl TS for [T; N]); impl_shadow!(as Vec: impl TS for [T]); impl_wrapper!(impl TS for Box); diff --git a/ts-rs/tests/arrays.rs b/ts-rs/tests/arrays.rs index fe37a33d3..f974b3830 100644 --- a/ts-rs/tests/arrays.rs +++ b/ts-rs/tests/arrays.rs @@ -2,7 +2,7 @@ use ts_rs::TS; #[test] fn free() { - assert_eq!(<[String; 10]>::inline(), "Array") + assert_eq!(<[String; 4]>::inline(), "[string, string, string, string]") } #[test] @@ -10,16 +10,16 @@ fn interface() { #[derive(TS)] struct Interface { #[allow(dead_code)] - a: [i32; 10], + a: [i32; 4], } - assert_eq!(Interface::inline(), "{ a: Array, }") + 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") + assert_eq!(Newtype::inline(), "[number, number, number, number]") } diff --git a/ts-rs/tests/enum_flattening.rs b/ts-rs/tests/enum_flattening.rs new file mode 100644 index 000000000..c26ef059a --- /dev/null +++ b/ts-rs/tests/enum_flattening.rs @@ -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, + } + + #[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 }, + } + + 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, + } + + #[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 }, + } + + 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, + + #[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 }, + } + + 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, }"# + ) +} + diff --git a/ts-rs/tests/flatten.rs b/ts-rs/tests/flatten.rs index 837c9b65b..56212498f 100644 --- a/ts-rs/tests/flatten.rs +++ b/ts-rs/tests/flatten.rs @@ -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, }" ); } diff --git a/ts-rs/tests/generic_fields.rs b/ts-rs/tests/generic_fields.rs index a6f362d54..e2100fab9 100644 --- a/ts-rs/tests/generic_fields.rs +++ b/ts-rs/tests/generic_fields.rs @@ -40,7 +40,7 @@ fn named() { } assert_eq!( Struct::inline(), - "{ a: Array, b: [Array, Array], c: Array>, }" + "{ a: Array, b: [Array, Array], c: [Array, Array, Array], }" ); } @@ -52,7 +52,7 @@ fn named_nested() { b: (Vec>, Vec>), c: [Vec>; 3], } - assert_eq!(Struct::inline(), "{ a: Array>, b: [Array>, Array>], c: Array>>, }"); + assert_eq!(Struct::inline(), "{ a: Array>, b: [Array>, Array>], c: [Array>, Array>, Array>], }"); } #[test] @@ -61,7 +61,7 @@ fn tuple() { struct Tuple(Vec, (Vec, Vec), [Vec; 3]); assert_eq!( Tuple::inline(), - "[Array, [Array, Array], Array>]" + "[Array, [Array, Array], [Array, Array, Array]]" ); } @@ -75,6 +75,6 @@ fn tuple_nested() { ); assert_eq!( Tuple::inline(), - "[Array>, [Array>, Array>], Array>>]" + "[Array>, [Array>, Array>], [Array>, Array>, Array>]]" ); } diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs index cd8cd6875..184d50eb7 100644 --- a/ts-rs/tests/generics.rs +++ b/ts-rs/tests/generics.rs @@ -142,7 +142,7 @@ fn generic_struct() { assert_eq!( Struct::<()>::decl(), - "type Struct = { a: T, b: [T, T], c: [T, [T, T]], d: Array, e: Array<[T, T]>, f: Array, g: Array>, h: Array>, }" + "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]]>, }" ) }