diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 00613489..53b00df1 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -30,6 +30,7 @@ struct DerivedTS { concrete: HashMap, bound: Option>, ts_enum: Option, + is_enum: TokenStream, export: bool, export_to: Option, @@ -85,6 +86,7 @@ impl DerivedTS { ); let assoc_type = generate_assoc_type(&rust_ty, &crate_rename, &generics, &self.concrete); let name = self.generate_name_fn(&generics); + let is_enum = self.is_enum.clone(); let inline = self.generate_inline_fn(); let decl = self.generate_decl_fn(&rust_ty, &generics); let dependencies = &self.dependencies; @@ -95,6 +97,8 @@ impl DerivedTS { #assoc_type type OptionInnerType = Self; + const IS_ENUM: bool = #is_enum; + fn ident() -> String { (#ident).to_string() } diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index c53df1c4..09711c15 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -65,6 +65,7 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { concrete: enum_attr.concrete, bound: enum_attr.bound, ts_enum: enum_attr.repr, + is_enum: quote!(true), }) } @@ -234,5 +235,6 @@ fn empty_enum(ts_name: Expr, enum_attr: EnumAttr) -> DerivedTS { concrete: enum_attr.concrete, bound: enum_attr.bound, ts_enum: enum_attr.repr, + is_enum: quote!(false), } } diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index 47ada7cf..31c36eba 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -73,6 +73,7 @@ pub(crate) fn named(attr: &StructAttr, ts_name: Expr, fields: &FieldsNamed) -> R concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), }) } diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index 2daf0ca6..dff6fdad 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -40,7 +40,7 @@ pub(crate) fn newtype( }; Ok(DerivedTS { - crate_rename, + crate_rename: crate_rename.clone(), inline: inline_def, inline_flattened: None, docs: attr.docs.clone(), @@ -51,5 +51,10 @@ pub(crate) fn newtype( concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: if field_attr.type_override.is_none() { + quote!(<#inner_ty as #crate_rename::TS>::IS_ENUM) + } else { + quote!(false) + }, }) } diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index d2919a9f..c9d0fbc1 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -40,6 +40,7 @@ pub(crate) fn tuple(attr: &StructAttr, ts_name: Expr, fields: &FieldsUnnamed) -> concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), }) } diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs index 45df18df..963a41a8 100644 --- a/macros/src/types/type_as.rs +++ b/macros/src/types/type_as.rs @@ -29,6 +29,7 @@ pub(crate) fn type_as_struct( concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(<#type_as as #crate_rename::TS>::IS_ENUM), }) } @@ -50,5 +51,6 @@ pub(crate) fn type_as_enum(attr: &EnumAttr, ts_name: Expr, type_as: &Type) -> Re concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(<#type_as as #crate_rename::TS>::IS_ENUM), }) } diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs index 3dea047c..00b4a553 100644 --- a/macros/src/types/type_override.rs +++ b/macros/src/types/type_override.rs @@ -26,6 +26,7 @@ pub(crate) fn type_override_struct( concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), // we dont know what the override is, so we preserve is_enum }) } @@ -48,5 +49,6 @@ pub(crate) fn type_override_enum( concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(true), // we dont know what the override is, so we preserve is_enum }) } diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 4b4f88de..9e467159 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -22,6 +22,7 @@ pub(crate) fn empty_object(attr: &StructAttr, ts_name: Expr) -> DerivedTS { concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), } } @@ -40,6 +41,7 @@ pub(crate) fn empty_array(attr: &StructAttr, ts_name: Expr) -> DerivedTS { concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), } } @@ -58,5 +60,6 @@ pub(crate) fn null(attr: &StructAttr, ts_name: Expr) -> DerivedTS { concrete: attr.concrete.clone(), bound: attr.bound.clone(), ts_enum: None, + is_enum: quote!(false), } } diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index 5c670484..fb035a26 100644 --- a/ts-rs/src/lib.rs +++ b/ts-rs/src/lib.rs @@ -432,6 +432,9 @@ pub trait TS { #[doc(hidden)] const IS_OPTION: bool = false; + #[doc(hidden)] + const IS_ENUM: bool = false; + /// JSDoc comment to describe this type in TypeScript - when `TS` is derived, docs are /// automatically read from your doc comments or `#[doc = ".."]` attributes fn docs() -> Option { @@ -986,17 +989,19 @@ impl TS for HashMap { fn name() -> String { format!( - "{{ [key in {}]?: {} }}", + "{{ [key in {}]{}: {} }}", ::name(), - ::name() + if ::IS_ENUM { "?" } else { "" }, + ::name(), ) } fn inline() -> String { format!( - "{{ [key in {}]?: {} }}", + "{{ [key in {}]{}: {} }}", ::inline(), - ::inline() + if ::IS_ENUM { "?" } else { "" }, + ::inline(), ) } @@ -1027,11 +1032,7 @@ impl TS for HashMap { } fn inline_flattened() -> String { - format!( - "({{ [key in {}]?: {} }})", - ::inline(), - ::inline() - ) + format!("({})", ::inline()) } } diff --git a/ts-rs/tests/integration/flatten.rs b/ts-rs/tests/integration/flatten.rs index 01b8c66f..a8837d3b 100644 --- a/ts-rs/tests/integration/flatten.rs +++ b/ts-rs/tests/integration/flatten.rs @@ -33,6 +33,6 @@ struct C { fn test_def() { assert_eq!( C::inline(), - "{ b: { c: number, a: number, b: number, } & ({ [key in string]?: number }), d: number, }" + "{ b: { c: number, a: number, b: number, } & ({ [key in string]: number }), d: number, }" ); } diff --git a/ts-rs/tests/integration/generics.rs b/ts-rs/tests/integration/generics.rs index 7911ed97..2e876b5d 100644 --- a/ts-rs/tests/integration/generics.rs +++ b/ts-rs/tests/integration/generics.rs @@ -84,7 +84,7 @@ fn test() { assert_eq!( Container::decl(), - "type Container = { foo: Generic, bar: Array>, baz: { [key in string]?: Generic }, };" + "type Container = { foo: Generic, bar: Array>, baz: { [key in string]: Generic }, };" ); } diff --git a/ts-rs/tests/integration/hashmap.rs b/ts-rs/tests/integration/hashmap.rs index 6d662f19..7c13a633 100644 --- a/ts-rs/tests/integration/hashmap.rs +++ b/ts-rs/tests/integration/hashmap.rs @@ -15,7 +15,7 @@ struct Hashes { fn hashmap() { assert_eq!( Hashes::decl(), - "type Hashes = { map: { [key in string]?: string }, set: Array, };" + "type Hashes = { map: { [key in string]: string }, set: Array, };" ) } @@ -35,7 +35,7 @@ struct HashesHasher { fn hashmap_with_custom_hasher() { assert_eq!( HashesHasher::decl(), - "type HashesHasher = { map: { [key in string]?: string }, set: Array, };" + "type HashesHasher = { map: { [key in string]: string }, set: Array, };" ) } @@ -59,6 +59,13 @@ struct BTreeMapWithCustomTypes { map: BTreeMap, } +#[derive(TS)] +#[ts(export, export_to = "hashmap/")] +enum EnumKey { + Foo, + Bar, +} + #[test] fn with_custom_types() { assert_eq!( @@ -67,6 +74,14 @@ fn with_custom_types() { ); assert_eq!( HashMapWithCustomTypes::decl(), - "type HashMapWithCustomTypes = { map: { [key in CustomKey]?: CustomValue }, };" + "type HashMapWithCustomTypes = { map: { [key in CustomKey]: CustomValue }, };" + ); + assert_eq!( + HashMap::::name(), + "{ [key in EnumKey]?: string }" + ); + assert_eq!( + HashMap::::inline(), + r#"{ [key in "Foo" | "Bar"]?: string }"# ); } diff --git a/ts-rs/tests/integration/indexmap.rs b/ts-rs/tests/integration/indexmap.rs index cb5f88e8..d31cb0c5 100644 --- a/ts-rs/tests/integration/indexmap.rs +++ b/ts-rs/tests/integration/indexmap.rs @@ -15,6 +15,6 @@ struct Indexes { fn indexmap() { assert_eq!( Indexes::decl(), - "type Indexes = { map: { [key in string]?: string }, set: Array, };" + "type Indexes = { map: { [key in string]: string }, set: Array, };" ) } diff --git a/ts-rs/tests/integration/issue_168.rs b/ts-rs/tests/integration/issue_168.rs index fe86c920..a3043d49 100644 --- a/ts-rs/tests/integration/issue_168.rs +++ b/ts-rs/tests/integration/issue_168.rs @@ -45,13 +45,13 @@ fn issue_168() { FooInlined::export_to_string().unwrap(), "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ \n\ - export type FooInlined = { map: { [key in number]?: { map: { [key in number]?: { map: { [key in number]?: string }, } }, } }, };\n" + export type FooInlined = { map: { [key in number]: { map: { [key in number]: { map: { [key in number]: string }, } }, } }, };\n" ); assert_eq!( Foo::export_to_string().unwrap(), format!("// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\ import type {{ Bar }} from \"./Bar{extension}\";\n\ \n\ - export type Foo = {{ map: {{ [key in number]?: Bar }}, }};\n") + export type Foo = {{ map: {{ [key in number]: Bar }}, }};\n") ); } diff --git a/ts-rs/tests/integration/issue_70.rs b/ts-rs/tests/integration/issue_70.rs index 4078b958..1739fb29 100644 --- a/ts-rs/tests/integration/issue_70.rs +++ b/ts-rs/tests/integration/issue_70.rs @@ -24,11 +24,11 @@ struct Struct { fn issue_70() { assert_eq!( Enum::decl(), - "type Enum = { \"A\": { [key in string]?: string } } | { \"B\": { [key in string]?: string } };" + "type Enum = { \"A\": { [key in string]: string } } | { \"B\": { [key in string]: string } };" ); assert_eq!( Struct::decl(), - "type Struct = { a: { [key in string]?: string }, b: { [key in string]?: string }, };" + "type Struct = { a: { [key in string]: string }, b: { [key in string]: string }, };" ); } diff --git a/ts-rs/tests/integration/lifetimes.rs b/ts-rs/tests/integration/lifetimes.rs index 41ec73a2..74e1c495 100644 --- a/ts-rs/tests/integration/lifetimes.rs +++ b/ts-rs/tests/integration/lifetimes.rs @@ -31,6 +31,6 @@ fn contains_borrow() { fn contains_borrow_type_args() { assert_eq!( A::decl(), - "type A = { a: Array, b: Array>, c: { [key in string]?: boolean }, };" + "type A = { a: Array, b: Array>, c: { [key in string]: boolean }, };" ); } diff --git a/ts-rs/tests/integration/self_referential.rs b/ts-rs/tests/integration/self_referential.rs index 4c7e65f1..0f6c37dd 100644 --- a/ts-rs/tests/integration/self_referential.rs +++ b/ts-rs/tests/integration/self_referential.rs @@ -83,8 +83,8 @@ fn enum_externally_tagged() { { \"C\": E } | \ { \"D\": E } | \ { \"E\": [E, E, E, E] } | \ - { \"F\": { a: E, b: E, c: { [key in string]?: E }, d: E | null, e?: E | null, f?: E, } } | \ - { \"G\": [Array, Array, { [key in string]?: E }] };" + { \"F\": { a: E, b: E, c: { [key in string]: E }, d: E | null, e?: E | null, f?: E, } } | \ + { \"G\": [Array, Array, { [key in string]: E }] };" ); } @@ -170,7 +170,7 @@ fn enum_adjacently_tagged() { \"content\": { \ a: A, \ b: A, \ - c: { [key in string]?: A }, \ + c: { [key in string]: A }, \ d: A | null, \ e?: A | null, \ f?: A, \ @@ -181,7 +181,7 @@ fn enum_adjacently_tagged() { \"content\": [\ Array, \ [A, A, A, A], \ - { [key in string]?: A }\ + { [key in string]: A }\ ] \ };" ); diff --git a/ts-rs/tests/integration/serde_json.rs b/ts-rs/tests/integration/serde_json.rs index 6c68b8ae..aa8fa49f 100644 --- a/ts-rs/tests/integration/serde_json.rs +++ b/ts-rs/tests/integration/serde_json.rs @@ -20,22 +20,22 @@ fn using_serde_json() { assert_eq!(serde_json::Number::inline(), "number"); assert_eq!( serde_json::Map::::inline(), - "{ [key in string]?: number }" + "{ [key in string]: number }" ); assert_eq!( serde_json::Value::decl(), - "type JsonValue = number | string | boolean | Array | { [key in string]?: JsonValue } | null;", + "type JsonValue = number | string | boolean | Array | { [key in string]: JsonValue } | null;", ); assert_eq!( UsingSerdeJson::decl(), "type UsingSerdeJson = { \ num: number, \ - map1: { [key in string]?: number }, \ - map2: { [key in string]?: UsingSerdeJson }, \ - map3: { [key in string]?: { [key in string]?: number } }, \ - map4: { [key in string]?: number }, \ - map5: { [key in string]?: JsonValue }, \ + map1: { [key in string]: number }, \ + map2: { [key in string]: UsingSerdeJson }, \ + map3: { [key in string]: { [key in string]: number } }, \ + map4: { [key in string]: number }, \ + map5: { [key in string]: JsonValue }, \ any: JsonValue, \ };" ) @@ -53,7 +53,7 @@ fn inlined_value() { assert_eq!( InlinedValue::decl(), "type InlinedValue = { \ - any: number | string | boolean | Array | { [key in string]?: JsonValue } | null, \ + any: number | string | boolean | Array | { [key in string]: JsonValue } | null, \ };" ); } diff --git a/ts-rs/tests/integration/top_level_type_as.rs b/ts-rs/tests/integration/top_level_type_as.rs index c62938fe..978bb05b 100644 --- a/ts-rs/tests/integration/top_level_type_as.rs +++ b/ts-rs/tests/integration/top_level_type_as.rs @@ -53,3 +53,72 @@ pub struct Bar { as = "HashMap::" )] pub struct Biz(String); + +// -- test that TS::IS_ENUM is preserved correctly -- + +pub struct Unsupported; + +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/")] +pub enum NormalEnum { + A, + B, +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/")] +pub struct NormalStruct { + x: u32, +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "String")] +pub enum EnumAsString { + A(Unsupported), + B(Unsupported), +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "NormalEnum")] +pub enum EnumAsEnum { + A(Unsupported), + B(Unsupported), +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "NormalStruct")] +pub enum EnumAsStruct { + A(Unsupported), + B(Unsupported), +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "String")] +pub struct StructAsString { + x: Unsupported, +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "NormalEnum")] +pub struct StructAsEnum { + x: Unsupported, +} +#[derive(TS)] +#[ts(export, export_to = "top_level_type_as/", as = "NormalStruct")] +pub struct StructAsStruct { + x: Unsupported, +} + +#[test] +fn preserves_is_enum() { + assert!(NormalEnum::IS_ENUM); + assert!(!NormalStruct::IS_ENUM); + + assert_eq!(EnumAsString::inline(), String::inline()); + assert!(!EnumAsString::IS_ENUM); + assert_eq!(EnumAsEnum::inline(), NormalEnum::inline()); + assert!(EnumAsEnum::IS_ENUM); + assert_eq!(EnumAsStruct::inline(), NormalStruct::inline()); + assert!(!EnumAsStruct::IS_ENUM); + + assert_eq!(StructAsString::inline(), String::inline()); + assert!(!StructAsString::IS_ENUM); + assert_eq!(StructAsEnum::inline(), NormalEnum::inline()); + assert!(StructAsEnum::IS_ENUM); + assert_eq!(StructAsStruct::inline(), NormalStruct::inline()); + assert!(!StructAsStruct::IS_ENUM); +}