Repository: Aleph-Alpha/ts-rs
Pull request #129 (2022-10-24 05:13:27)

Quote enum tag and content in generated TS
Currently
```
#[derive(Serialize, TS)]
#[serde(tag = "kebab-cased-tag", content = "whitespace in content")]
enum E {
    V { f: String },
}
```
generates the following E::decl()
```ts
{ kebab-cased-tag: "V", whitespace in content: { f: string, } }
```
which causes an error due to the keys not being quoted. This PR fixes this by always quoting the tag and content keys.


Patch:
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index b831726..2353f7f 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -91,20 +91,22 @@ fn format_variant(
         Tagged::Untagged => quote!(#inline_type),
         Tagged::Externally => match &variant.fields {
             Fields::Unit => quote!(format!("\"{}\"", #name)),
-            _ => quote!(format!("{{ {}: {} }}", #name, #inline_type)),
+            _ => quote!(format!("{{ \"{}\": {} }}", #name, #inline_type)),
         },
         Tagged::Adjacently { tag, content } => match &variant.fields {
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                 let ty = format_type(&unnamed.unnamed[0].ty, dependencies, generics);
-                quote!(format!("{{ {}: \"{}\", {}: {} }}", #tag, #name, #content, #ty))
+                quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
             }
-            Fields::Unit => quote!(format!("{{ {}: \"{}\" }}", #tag, #name)),
-            _ => quote!(format!("{{ {}: \"{}\", {}: {} }}", #tag, #name, #content, #inline_type)),
+            Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
+            _ => quote!(
+                format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #inline_type)
+            ),
         },
         Tagged::Internally { tag } => match variant_type.inline_flattened {
             Some(inline_flattened) => quote! {
                 format!(
-                    "{{ {}: \"{}\", {} }}",
+                    "{{ \"{}\": \"{}\", {} }}",
                     #tag,
                     #name,
                     #inline_flattened
@@ -113,11 +115,11 @@ fn format_variant(
             None => match &variant.fields {
                 Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                     let ty = format_type(&unnamed.unnamed[0].ty, dependencies, generics);
-                    quote!(format!("{{ {}: \"{}\" }} & {}", #tag, #name, #ty))
+                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
                 }
-                Fields::Unit => quote!(format!("{{ {}: \"{}\" }}", #tag, #name)),
+                Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
                 _ => {
-                    quote!(format!("{{ {}: \"{}\" }} & {}", #tag, #name, #inline_type))
+                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #inline_type))
                 }
             },
         },


Test patch:
diff --git a/ts-rs/tests/enum_variant_annotation.rs b/ts-rs/tests/enum_variant_annotation.rs
index 3eba866..a87570f 100644
--- a/ts-rs/tests/enum_variant_annotation.rs
+++ b/ts-rs/tests/enum_variant_annotation.rs
@@ -24,7 +24,7 @@ enum A {
 fn test_enum_variant_rename_all() {
     assert_eq!(
         A::inline(),
-        "{ MESSAGE_ONE: { sender_id: string, number_of_snakes: bigint, } } | { MESSAGE_TWO: { senderId: string, numberOfCamels: bigint, } }",
+        r#"{ "MESSAGE_ONE": { sender_id: string, number_of_snakes: bigint, } } | { "MESSAGE_TWO": { senderId: string, numberOfCamels: bigint, } }"#,
     );
 }
 
@@ -49,7 +49,7 @@ enum B {
 fn test_enum_variant_rename() {
     assert_eq!(
         B::inline(),
-        "{ SnakeMessage: { sender_id: string, number_of_snakes: bigint, } } | { CamelMessage: { sender_id: string, number_of_camels: bigint, } }",
+        r#"{ "SnakeMessage": { sender_id: string, number_of_snakes: bigint, } } | { "CamelMessage": { sender_id: string, number_of_camels: bigint, } }"#,
     );
 }
 
@@ -70,3 +70,28 @@ pub enum C {
 fn test_enum_variant_with_tag() {
     assert_eq!(C::inline(), "{ kind: \"SQUARE_THING\", name: string, }");
 }
+
+#[cfg(feature = "serde-compat")]
+#[test]
+fn test_tag_and_content_quoted() {
+    #[derive(Serialize, TS)]
+    #[serde(tag = "kebab-cased-tag", content = "whitespace in content")]
+    enum E {
+        V { f: String },
+    }
+    assert_eq!(
+        E::inline(),
+        r#"{ "kebab-cased-tag": "V", "whitespace in content": { f: string, } }"#
+    )
+}
+
+#[cfg(feature = "serde-compat")]
+#[test]
+fn test_variant_quoted() {
+    #[derive(Serialize, TS)]
+    #[serde(rename_all = "kebab-case")]
+    enum E {
+        VariantName { f: String },
+    }
+    assert_eq!(E::inline(), r#"{ "variant-name": { f: string, } }"#)
+}
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index e9b46bb..c7ce9b6 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -99,7 +99,7 @@ fn generic_enum() {
 
     assert_eq!(
         Generic::<(), (), ()>::decl(),
-        r#"type Generic<A, B, C> = { A: A } | { B: [B, B, B] } | { C: Array<C> } | { D: Array<Array<Array<A>>> } | { E: { a: A, b: B, c: C, } } | { X: Array<number> } | { Y: number } | { Z: Array<Array<number>> };"#
+        r#"type Generic<A, B, C> = { "A": A } | { "B": [B, B, B] } | { "C": Array<C> } | { "D": Array<Array<Array<A>>> } | { "E": { a: A, b: B, c: C, } } | { "X": Array<number> } | { "Y": number } | { "Z": Array<Array<number>> };"#
     )
 }
 
@@ -223,7 +223,7 @@ fn trait_bounds() {
     }
     assert_eq!(
         C::<&'static str, i32>::decl(),
-        "type C<T, K = number> = { A: { t: T, } } | { B: T } | \"C\" | { D: [T, K] };"
+        r#"type C<T, K = number> = { "A": { t: T, } } | { "B": T } | "C" | { "D": [T, K] };"#
     );
 
     #[derive(TS)]
diff --git a/ts-rs/tests/imports.rs b/ts-rs/tests/imports.rs
index 805f066..7674e09 100644
--- a/ts-rs/tests/imports.rs
+++ b/ts-rs/tests/imports.rs
@@ -37,9 +37,9 @@ fn test_def() {
             "import type { TestTypeA } from \"./ts_rs_test_type_a\";\n",
             "import type { TestTypeB } from \"./ts_rs_test_type_b\";\n",
             "\n",
-            "export type TestEnum = { C: { value: TestTypeB<number> } } | {\n",
-            "  A1: { value: TestTypeA<number> };\n",
-            "} | { A2: { value: TestTypeA<number> } };\n"
+            "export type TestEnum = { \"C\": { value: TestTypeB<number> } } | {\n",
+            "  \"A1\": { value: TestTypeA<number> };\n",
+            "} | { \"A2\": { value: TestTypeA<number> } };\n"
         )
     );
 
diff --git a/ts-rs/tests/union_serde.rs b/ts-rs/tests/union_serde.rs
index 3e08cf1..6996e31 100644
--- a/ts-rs/tests/union_serde.rs
+++ b/ts-rs/tests/union_serde.rs
@@ -33,11 +33,11 @@ enum Untagged {
 fn test_serde_enum() {
     assert_eq!(
         SimpleEnum::decl(),
-        r#"type SimpleEnum = { kind: "A" } | { kind: "B" };"#
+        r#"type SimpleEnum = { "kind": "A" } | { "kind": "B" };"#
     );
     assert_eq!(
         ComplexEnum::decl(),
-        r#"type ComplexEnum = { kind: "A" } | { kind: "B", data: { foo: string, bar: number, } } | { kind: "W", data: SimpleEnum } | { kind: "F", data: { nested: SimpleEnum, } } | { kind: "T", data: [number, SimpleEnum] };"#
+        r#"type ComplexEnum = { "kind": "A" } | { "kind": "B", "data": { foo: string, bar: number, } } | { "kind": "W", "data": SimpleEnum } | { "kind": "F", "data": { nested: SimpleEnum, } } | { "kind": "T", "data": [number, SimpleEnum] };"#
     );
 
     assert_eq!(
diff --git a/ts-rs/tests/union_with_data.rs b/ts-rs/tests/union_with_data.rs
index f96b174..2c69739 100644
--- a/ts-rs/tests/union_with_data.rs
+++ b/ts-rs/tests/union_with_data.rs
@@ -35,7 +35,7 @@ fn test_stateful_enum() {
 
     assert_eq!(
         SimpleEnum::decl(),
-        r#"type SimpleEnum = { A: string } | { B: number } | "C" | { D: [string, number] } | { E: Foo } | { F: { a: number, b: string, } };"#
+        r#"type SimpleEnum = { "A": string } | { "B": number } | "C" | { "D": [string, number] } | { "E": Foo } | { "F": { a: number, b: string, } };"#
     );
     assert!(SimpleEnum::dependencies()
         .into_iter()
diff --git a/ts-rs/tests/union_with_internal_tag.rs b/ts-rs/tests/union_with_internal_tag.rs
index 277eefa..e0fc0b7 100644
--- a/ts-rs/tests/union_with_internal_tag.rs
+++ b/ts-rs/tests/union_with_internal_tag.rs
@@ -32,11 +32,11 @@ enum EnumWithInternalTag2 {
 fn test_enums_with_internal_tags() {
     assert_eq!(
         EnumWithInternalTag::decl(),
-        r#"type EnumWithInternalTag = { type: "A", foo: string, } | { type: "B", bar: number, };"#
+        r#"type EnumWithInternalTag = { "type": "A", foo: string, } | { "type": "B", bar: number, };"#
     );
 
     assert_eq!(
         EnumWithInternalTag2::decl(),
-        r#"type EnumWithInternalTag2 = { type: "A" } & InnerA | { type: "B" } & InnerB;"#
+        r#"type EnumWithInternalTag2 = { "type": "A" } & InnerA | { "type": "B" } & InnerB;"#
     );
 }