Repository: Aleph-Alpha/ts-rs
Pull request #225 (2024-02-05 12:05:05)

Add support for `#[serde(rename_all_fields = "...")]` attribute
Adds support for the `#[serde(rename_all_fields = "...")]` enum attribute, as well as a `#[ts(rename_all_fields = "...")]` equivalent.
This attribute behaves the same as adding `#[ts(rename_all = "...")]` to each **struct variant** of an enum, **but not on the enum itself**, so the variant names will not be affected, for that `#[ts(rename_all = "...")]` still must be used on the enum


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70cf05e..3b4188a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 - Add `import-esm` cargo feature to import files with a `.js` extension ([#192](https://github.com/Aleph-Alpha/ts-rs/pull/192))
 - Implement `#[ts(...)]` equivalents for `#[serde(tag = "...")]`, `#[serde(tag = "...", content = "...")]` and `#[serde(untagged)]` ([#227](https://github.com/Aleph-Alpha/ts-rs/pull/227))
 - Support `#[serde(untagged)]` on individual enum variants ((#226)[https://github.com/Aleph-Alpha/ts-rs/pull/226])
+- Support for `#[serde(rename_all_fields = "...")]` ([#225](https://github.com/Aleph-Alpha/ts-rs/pull/225))
 
 ### Fixes
 - `rename_all` with `camelCase` produces wrong names if fields were already in camelCase ([#198](https://github.com/Aleph-Alpha/ts-rs/pull/198))
diff --git a/README.md b/README.md
index 03b41ea..971eeed 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,7 @@ With the `serde-compat` feature (enabled by default), serde attributes can be pa
 Supported serde attributes:
 - `rename`
 - `rename-all`
+- `rename-all-fields`
 - `tag`
 - `content`
 - `untagged`
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index fb8ad5e..4ed6c0f 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -8,6 +8,7 @@ use crate::{
 #[derive(Default)]
 pub struct EnumAttr {
     pub rename_all: Option<Inflection>,
+    pub rename_all_fields: Option<Inflection>,
     pub rename: Option<String>,
     pub export_to: Option<String>,
     pub export: bool,
@@ -53,6 +54,7 @@ impl EnumAttr {
         &mut self,
         EnumAttr {
             rename_all,
+            rename_all_fields,
             rename,
             tag,
             content,
@@ -63,6 +65,7 @@ impl EnumAttr {
     ) {
         self.rename = self.rename.take().or(rename);
         self.rename_all = self.rename_all.take().or(rename_all);
+        self.rename_all_fields = self.rename_all_fields.take().or(rename_all_fields);
         self.tag = self.tag.take().or(tag);
         self.untagged = self.untagged || untagged;
         self.content = self.content.take().or(content);
@@ -75,6 +78,7 @@ impl_parse! {
     EnumAttr(input, out) {
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
+        "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?),
         "export_to" => out.export_to = Some(parse_assign_str(input)?),
         "export" => out.export = true,
         "tag" => out.tag = Some(parse_assign_str(input)?),
@@ -88,6 +92,7 @@ impl_parse! {
     SerdeEnumAttr(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
+        "rename_all_fields" => out.0.rename_all_fields = Some(parse_assign_inflection(input)?),
         "tag" => out.0.tag = Some(parse_assign_str(input)?),
         "content" => out.0.content = Some(parse_assign_str(input)?),
         "untagged" => out.0.untagged = true
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index a3f54b0..52bcbfa 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -1,5 +1,6 @@
 use syn::{Attribute, Ident, Result};
 
+use super::EnumAttr;
 use crate::{
     attr::{parse_assign_inflection, parse_assign_str, Inflection},
     utils::parse_attrs,
@@ -19,9 +20,10 @@ pub struct VariantAttr {
 pub struct SerdeVariantAttr(VariantAttr);
 
 impl VariantAttr {
-    pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
+    pub fn new(attrs: &[Attribute], enum_attr: &EnumAttr) -> Result<Self> {
         let mut result = Self::default();
         parse_attrs(attrs)?.for_each(|a| result.merge(a));
+        result.rename_all = result.rename_all.or(enum_attr.rename_all_fields);
         #[cfg(feature = "serde-compat")]
         if !result.skip {
             crate::utils::parse_serde_attrs::<SerdeVariantAttr>(attrs)
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index fd5ab0f..0c78f64 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -67,7 +67,7 @@ fn format_variant(
     variant: &Variant,
     generics: &Generics,
 ) -> syn::Result<()> {
-    let variant_attr = VariantAttr::from_attrs(&variant.attrs)?;
+    let variant_attr = VariantAttr::new(&variant.attrs, enum_attr)?;
 
     if variant_attr.skip {
         return Ok(());
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index dbfd7f8..4d391be 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -128,6 +128,7 @@
 //! Supported serde attributes:
 //! - `rename`
 //! - `rename-all`
+//! - `rename-all-fields`
 //! - `tag`
 //! - `content`
 //! - `untagged`
@@ -254,6 +255,10 @@ pub mod typelist;
 /// - `#[ts(rename_all = "..")]`:  
 ///   Rename all variants of this enum.  
 ///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
+///
+/// - `#[ts(rename_all_fieds = "..")]`
+///   Renames the fields of all the struct variants of this enum.
+///   Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
 ///  
 /// ### enum variant attributes
 ///


Test patch:
diff --git a/ts-rs/tests/enum_struct_rename_all.rs b/ts-rs/tests/enum_struct_rename_all.rs
index 85f1821..59254b6 100644
--- a/ts-rs/tests/enum_struct_rename_all.rs
+++ b/ts-rs/tests/enum_struct_rename_all.rs
@@ -28,3 +28,28 @@ pub fn enum_struct_rename_all() {
         r#"{ "running": { startedTime: string, } } | { "terminated": { status: number, stdout: string, stderr: string, } }"#
     )
 }
+
+#[cfg_attr(feature = "serde-compat", derive(Serialize))]
+#[derive(TS, Clone)]
+#[ts(export)]
+#[cfg_attr(feature = "serde-compat", serde(rename_all_fields = "kebab-case"))]
+#[cfg_attr(not(feature = "serde-compat"), ts(rename_all_fields = "kebab-case"))]
+pub enum TaskStatus2 {
+    Running {
+        started_time: String,
+    },
+
+    Terminated {
+        status: i32,
+        stdout: String,
+        stderr: String,
+    },
+}
+
+#[test]
+pub fn enum_struct_rename_all_fields() {
+    assert_eq!(
+        TaskStatus2::inline(),
+        r#"{ "Running": { "started-time": string, } } | { "Terminated": { status: number, stdout: string, stderr: string, } }"#
+    )
+}