Skip to content

Commit

Permalink
Fix bug in enum flattening (#282)
Browse files Browse the repository at this point in the history
* When a struct containing a flattened enum was itself flattened, the type generated would be missing the enum
  • Loading branch information
escritorio-gustavo authored Mar 22, 2024
1 parent 88c2ae8 commit 6f7311d
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 7 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# master

### Breaking

### Features
- Add `#[ts(crate = "..")]` to allow usage of `#[derive(TS)]` from other proc-macro crates ([#274](https://github.com/Aleph-Alpha/ts-rs/pull/274))

- Add `#[ts(crate = "..")]` to allow usage of `#[derive(TS)]` from other proc-macro crates ([#274](https://github.com/Aleph-Alpha/ts-rs/pull/274))
- Add support types from `serde_json` behind cargo feature `serde-json-impl` ([#276](https://github.com/Aleph-Alpha/ts-rs/pull/276))

### Fixes

- Macro expansion for types with generic parameters now works without the `TS` trait in scope ([#281](https://github.com/Aleph-Alpha/ts-rs/pull/281))
- Fix enum flattening a struct that contains a flattened enum ([#282](https://github.com/Aleph-Alpha/ts-rs/pull/282))

# v8.0.0

Expand Down
9 changes: 8 additions & 1 deletion macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,19 @@ pub(crate) fn named(attr: &StructAttr, name: &str, fields: &FieldsNamed) -> Resu
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
(0, 0) => quote!("{ }".to_owned()),
(_, 0) => quote!(format!("{{ {} }}", #fields)),
(0, _) => quote!(#flattened),
(_, _) => quote!(format!("{{ {} }} & {}", #fields, #flattened)),
};

Ok(DerivedTS {
crate_rename,
// the `replace` combines `{ ... } & { ... }` into just one `{ ... }`. Not necessary, but it
// results in simpler type definitions.
inline: quote!(#inline.replace(" } & { ", " ")),
inline_flattened: Some(quote!(format!("{{ {} }}", #fields))),
inline_flattened: Some(quote!(#inline_flattened.replace(" } & { ", " "))),
docs: attr.docs.clone(),
dependencies,
export: attr.export,
Expand Down
64 changes: 59 additions & 5 deletions ts-rs/tests/enum_flattening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,26 @@ enum BarExternally {
Buz { c: String, d: Option<i32> },
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening/externally_tagged/")]
struct NestedExternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooExternally,
u: u32,
}

#[test]
fn externally_tagged() {
assert_eq!(
FooExternally::inline(),
r#"{ qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
)
);
assert_eq!(
NestedExternally::inline(),
r#"{ u: number, qux: number, biz: string | null, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
);
}

#[derive(TS)]
Expand Down Expand Up @@ -65,12 +79,25 @@ enum BarAdjecently {
},
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedAdjecently {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooAdjecently,
u: u32,
}

#[test]
fn adjacently_tagged() {
assert_eq!(
FooAdjecently::inline(),
r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
)
);
assert_eq!(
NestedAdjecently::inline(),
r#"{ u: number, one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
);
}

#[derive(TS)]
Expand All @@ -95,23 +122,46 @@ enum BarInternally {
Buz { c: String, d: Option<i32> },
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedInternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooInternally,
u: u32,
}

#[test]
fn internally_tagged() {
assert_eq!(
FooInternally::inline(),
r#"{ qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
)
);
assert_eq!(
NestedInternally::inline(),
r#"{ u: number, qux: string | null, } & ({ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, })"#
);
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct FooUntagged {
one: u32,
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarUntagged,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedUntagged {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooUntagged,
u: u32,
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
Expand All @@ -127,6 +177,10 @@ enum BarUntagged {
fn untagged() {
assert_eq!(
FooUntagged::inline(),
r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
)
r#"{ one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"#
);
assert_eq!(
NestedUntagged::inline(),
r#"{ u: number, one: number, } & ({ a: number, a2: string, } | { b: boolean, } | { c: string, })"#
);
}
243 changes: 243 additions & 0 deletions ts-rs/tests/enum_flattening_nested.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#![allow(dead_code)]

#[cfg(feature = "serde-compat")]
use serde::Serialize;
use ts_rs::TS;

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct FooExternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarExternally,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
enum BarExternally {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String, d: Option<i32> },
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct NestedExternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooExternally,
u: u32,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct NestedExternallyLonely {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooExternally,
}

#[test]
fn externally_tagged() {
// Notice here that baz is the only field inside `FooExternally`, so the parenthesis
// aren't needed
assert_eq!(
FooExternally::inline(),
r#"{ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } }"#
);

// But when flattening, the parenthesis are needed due to type intesections
assert_eq!(
NestedExternally::inline(),
r#"{ u: number, } & ({ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } })"#
);

// And here, they are, again, unecessary
assert_eq!(
NestedExternallyLonely::inline(),
r#"{ "Baz": { a: number, a2: string, } } | { "Biz": { b: boolean, } } | { "Buz": { c: string, d: number | null, } }"#
);
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/adjacently_tagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct FooAdjecently {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarAdjecently,
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/adjacently_tagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))]
#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))]
enum BarAdjecently {
Baz {
a: i32,
a2: String,
},
Biz {
b: bool,
},

#[cfg_attr(feature = "serde-compat", serde(untagged))]
#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
Buz {
c: String,
d: Option<i32>,
},
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedAdjecently {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooAdjecently,
u: u32,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct NestedAdjecentlyLonely {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooAdjecently,
}

#[test]
fn adjacently_tagged() {
assert_eq!(
FooAdjecently::inline(),
r#"{ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, }"#
);

assert_eq!(
NestedAdjecently::inline(),
r#"{ u: number, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
);

assert_eq!(
NestedAdjecentlyLonely::inline(),
r#"{ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, }"#
);
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/internally_tagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct FooInternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarInternally,
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/internally_tagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[cfg_attr(feature = "serde-compat", serde(tag = "type"))]
#[cfg_attr(not(feature = "serde-compat"), ts(tag = "type"))]
enum BarInternally {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String, d: Option<i32> },
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedInternally {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooInternally,
u: u32,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct NestedInternallyLonely {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooInternally,
}

#[test]
fn internally_tagged() {
assert_eq!(
FooInternally::inline(),
r#"{ "type": "Baz", a: number, a2: string, } | { "type": "Biz", b: boolean, } | { "type": "Buz", c: string, d: number | null, }"#
);

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

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

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct FooUntagged {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
baz: BarUntagged,
}

#[derive(TS)]
#[ts(export, export_to = "enum_flattening_nested/untagged/")]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[cfg_attr(feature = "serde-compat", serde(untagged))]
#[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
enum BarUntagged {
Baz { a: i32, a2: String },
Biz { b: bool },
Buz { c: String },
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
struct NestedUntagged {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooUntagged,
u: u32,
}

#[derive(TS)]
#[cfg_attr(feature = "serde-compat", derive(Serialize))]
#[ts(export, export_to = "enum_flattening_nested/externally_tagged/")]
struct NestedUntaggedLonely {
#[cfg_attr(feature = "serde-compat", serde(flatten))]
#[cfg_attr(not(feature = "serde-compat"), ts(flatten))]
a: FooUntagged,
}

#[test]
fn untagged() {
assert_eq!(
FooUntagged::inline(),
r#"{ a: number, a2: string, } | { b: boolean, } | { c: string, }"#
);

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

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

0 comments on commit 6f7311d

Please sign in to comment.