Repository: Aleph-Alpha/ts-rs
Pull request #122 (2022-10-12 01:25:12)

Support serde's "rename_all" on enum variants
Adds tests and implementation resolving #72 and #119, though the latter was already working AFAICT.


Patch:
diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs
index e555948..12ba2ed 100644
--- a/macros/src/attr/mod.rs
+++ b/macros/src/attr/mod.rs
@@ -7,10 +7,12 @@ use syn::{
     parse::{Parse, ParseStream},
     Error, Lit, Result, Token,
 };
+pub use variant::*;
 
 mod r#enum;
 mod field;
 mod r#struct;
+mod variant;
 
 #[derive(Copy, Clone, Debug)]
 pub enum Inflection {
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index 6593b84..b622de7 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -3,7 +3,7 @@ use std::convert::TryFrom;
 use syn::{Attribute, Ident, Result};
 
 use crate::{
-    attr::{parse_assign_str, Inflection},
+    attr::{parse_assign_str, Inflection, VariantAttr},
     utils::parse_attrs,
 };
 
@@ -47,6 +47,17 @@ impl StructAttr {
     }
 }
 
+impl From<VariantAttr> for StructAttr {
+    fn from(v: VariantAttr) -> Self {
+        Self {
+            rename: v.rename.clone(),
+            rename_all: v.rename_all.clone(),
+            // inline and skip are not supported on StructAttr
+            ..Self::default()
+        }
+    }
+}
+
 impl_parse! {
     StructAttr(input, out) {
         "rename" => out.rename = Some(parse_assign_str(input)?),
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
new file mode 100644
index 0000000..0f67f1e
--- /dev/null
+++ b/macros/src/attr/variant.rs
@@ -0,0 +1,63 @@
+use syn::{Attribute, Ident, Result};
+
+use crate::{
+    attr::{parse_assign_inflection, parse_assign_str, Inflection},
+    utils::parse_attrs,
+};
+
+#[derive(Default)]
+pub struct VariantAttr {
+    pub rename: Option<String>,
+    pub rename_all: Option<Inflection>,
+    pub inline: bool,
+    pub skip: bool,
+}
+
+#[cfg(feature = "serde-compat")]
+#[derive(Default)]
+pub struct SerdeVariantAttr(VariantAttr);
+
+impl VariantAttr {
+    pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
+        let mut result = Self::default();
+        parse_attrs(attrs)?.for_each(|a| result.merge(a));
+        #[cfg(feature = "serde-compat")]
+        crate::utils::parse_serde_attrs::<SerdeVariantAttr>(attrs).for_each(|a| result.merge(a.0));
+        Ok(result)
+    }
+
+    fn merge(
+        &mut self,
+        VariantAttr {
+            rename,
+            rename_all,
+            inline,
+            skip,
+        }: VariantAttr,
+    ) {
+        self.rename = self.rename.take().or(rename);
+        self.rename_all = self.rename_all.take().or(rename_all);
+        self.inline = self.inline || inline;
+        self.skip = self.skip || skip;
+    }
+}
+
+impl_parse! {
+    VariantAttr(input, out) {
+        "rename" => out.rename = Some(parse_assign_str(input)?),
+        "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
+        "inline" => out.inline = true,
+        "skip" => out.skip = true,
+    }
+}
+
+#[cfg(feature = "serde-compat")]
+impl_parse! {
+    SerdeVariantAttr(input, out) {
+        "rename" => out.0.rename = Some(parse_assign_str(input)?),
+        "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
+        "skip" => out.0.skip = true,
+        "skip_serializing" => out.0.skip = true,
+        "skip_deserializing" => out.0.skip = true,
+    }
+}
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 94486df..b831726 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -3,7 +3,7 @@ use quote::{format_ident, quote};
 use syn::{Fields, Generics, ItemEnum, Variant};
 
 use crate::{
-    attr::{EnumAttr, FieldAttr, StructAttr, Tagged},
+    attr::{EnumAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
     types,
     types::generics::{format_generics, format_type},
@@ -65,31 +65,20 @@ fn format_variant(
     variant: &Variant,
     generics: &Generics,
 ) -> syn::Result<()> {
-    let FieldAttr {
-        type_override,
-        rename,
-        inline,
-        skip,
-        optional,
-        flatten,
-    } = FieldAttr::from_attrs(&variant.attrs)?;
+    let variant_attr = VariantAttr::from_attrs(&variant.attrs)?;
 
-    match (skip, &type_override, inline, optional, flatten) {
-        (true, ..) => return Ok(()),
-        (_, Some(_), ..) => syn_err!("`type` is not applicable to enum variants"),
-        (_, _, _, true, ..) => syn_err!("`optional` is not applicable to enum variants"),
-        (_, _, _, _, true) => syn_err!("`flatten` is not applicable to enum variants"),
-        _ => {}
-    };
+    if variant_attr.skip {
+        return Ok(());
+    }
 
-    let name = match (rename, &enum_attr.rename_all) {
+    let name = match (variant_attr.rename.clone(), &enum_attr.rename_all) {
         (Some(rn), _) => rn,
         (None, None) => variant.ident.to_string(),
         (None, Some(rn)) => rn.apply(&variant.ident.to_string()),
     };
 
     let variant_type = types::type_def(
-        &StructAttr::default(),
+        &StructAttr::from(variant_attr),
         // since we are generating the variant as a struct, it doesn't have a name
         &format_ident!("_"),
         &variant.fields,


Test patch:
diff --git a/ts-rs/tests/enum_variant_annotation.rs b/ts-rs/tests/enum_variant_annotation.rs
new file mode 100644
index 0000000..72a9b96
--- /dev/null
+++ b/ts-rs/tests/enum_variant_annotation.rs
@@ -0,0 +1,66 @@
+#![allow(dead_code)]
+
+use serde::{Deserialize, Serialize};
+use ts_rs::TS;
+
+#[derive(Serialize, Deserialize, TS)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+#[ts(export)]
+enum A {
+    MessageOne {
+        sender_id: String,
+        number_of_snakes: u64,
+    },
+    #[serde(rename_all = "camelCase")]
+    MessageTwo {
+        sender_id: String,
+        number_of_camels: u64,
+    },
+}
+
+#[test]
+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, } }",
+    );
+}
+
+#[derive(Serialize, Deserialize, TS)]
+#[ts(export)]
+enum B {
+    #[serde(rename = "SnakeMessage")]
+    MessageOne {
+        sender_id: String,
+        number_of_snakes: u64,
+    },
+    #[serde(rename = "CamelMessage")]
+    MessageTwo {
+        sender_id: String,
+        number_of_camels: u64,
+    },
+}
+
+#[test]
+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, } }",
+    );
+}
+
+#[derive(Serialize, Deserialize, TS)]
+#[serde(tag = "kind")]
+#[ts(export)]
+pub enum C {
+    #[serde(rename = "SQUARE_THING")]
+    SquareThing {
+        name: String,
+        // ...
+    },
+}
+
+#[test]
+fn test_enum_variant_with_tag() {
+    assert_eq!(C::inline(), "{ kind: \"SQUARE_THING\", name: string, }");
+}