Repository: Aleph-Alpha/ts-rs
Pull request #284 (2024-03-22 15:06:32)

Support `#[ts(as = "...")]` and `#[ts(type = "...")]` on enum variants
## Goal

Add support for using  `#[ts(as = "...")]` and `#[ts(type = "...")]` on enum variants to make support for `#[serde(with = "...")]` (#280) possible here too

## Changes

`#[ts(as = "...")]` and `#[ts(type = "...")]` are now parsed and processed in enum variants.

Using them currently respects the tag enum representation. That is, neither `#[ts(as = "...")]` nor `#[ts(type = "...")]` will eliminate the "tag" or "content" properties the variant would have without them, as that can be achieved by using `#[ts(untagged)]` on the variant. I don't yet know if this is how serde works, so this can be changed to maintain serde compatibility

## Checklist

- [x] I have followed the steps listed in the [Contributing guide](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md).
- [ ] If necessary, I have added documentation related to the changes made.
- [ ] I have added or updated the tests related to the changes made.


escritorio-gustavo: I may need some help on the tests for this. I can't even get `#[serde(with = "...")]` on variants to compile atm
NyxCode: would love to take a loot at this once i find some time 👍 
escritorio-gustavo: @NyxCode, I think I figured out how `#{serde(with = ...)]` is used in variants. Looking at the last commit, do you think the current behavior of `#[ts(as = ...)]` is correct? I think it is, but I still don't quite understand `#{serde(with = ...)]` deeply enough to be sure
JonasFocke01: I tested my usecase from #352 and can confirm that this works as expected
gustavo-shigueo: Hi @JonasFocke01, I've just fixed a merge conflict and caused a couple of compiler errors in the process, can you check if it still works?
JonasFocke01:  @gustavo-shigueo Workds still as expected :+1: 

Patch:
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index cd1fee9..51d1da4 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -1,13 +1,15 @@
-use syn::{Attribute, Fields, Ident, Result, Variant};
+use syn::{Attribute, Fields, Ident, Result, Type, Variant};
 
 use super::{Attr, Serde};
 use crate::{
-    attr::{parse_assign_inflection, parse_assign_str, Inflection},
+    attr::{parse_assign_from_str, parse_assign_inflection, parse_assign_str, Inflection},
     utils::parse_attrs,
 };
 
 #[derive(Default)]
 pub struct VariantAttr {
+    pub type_as: Option<Type>,
+    pub type_override: Option<String>,
     pub rename: Option<String>,
     pub rename_all: Option<Inflection>,
     pub inline: bool,
@@ -31,6 +33,8 @@ impl Attr for VariantAttr {
 
     fn merge(self, other: Self) -> Self {
         Self {
+            type_as: self.type_as.or(other.type_as),
+            type_override: self.type_override.or(other.type_override),
             rename: self.rename.or(other.rename),
             rename_all: self.rename_all.or(other.rename_all),
             inline: self.inline || other.inline,
@@ -40,6 +44,38 @@ impl Attr for VariantAttr {
     }
 
     fn assert_validity(&self, item: &Self::Item) -> Result<()> {
+        if self.type_as.is_some() {
+            if self.type_override.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`as` is not compatible with `type`"
+                )
+            }
+
+            if self.rename_all.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`as` is not compatible with `rename_all`"
+                )
+            }
+        }
+
+        if self.type_override.is_some() {
+            if self.rename_all.is_some() {
+                syn_err_spanned!(
+                    item;
+                    "`type` is not compatible with `rename_all`"
+                )
+            }
+
+            if self.inline {
+                syn_err_spanned!(
+                    item;
+                    "`type` is not compatible with `inline`"
+                )
+            }
+        }
+
         if !matches!(item.fields, Fields::Named(_)) && self.rename_all.is_some() {
             syn_err_spanned!(
                 item;
@@ -53,6 +89,8 @@ impl Attr for VariantAttr {
 
 impl_parse! {
     VariantAttr(input, out) {
+        "as" => out.type_as = Some(parse_assign_from_str(input)?),
+        "type" => out.type_override = Some(parse_assign_str(input)?),
         "rename" => out.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "inline" => out.inline = true,
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 009741e..95cd38d 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -1,210 +1,210 @@
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::{Fields, ItemEnum, Variant};
-
-use crate::{
-    attr::{Attr, EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
-    deps::Dependencies,
-    types::{self, type_as, type_override},
-    DerivedTS,
-};
-
-pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
-    let enum_attr: EnumAttr = EnumAttr::from_attrs(&s.attrs)?;
-
-    enum_attr.assert_validity(s)?;
-
-    let crate_rename = enum_attr.crate_rename();
-
-    let name = match &enum_attr.rename {
-        Some(existing) => existing.clone(),
-        None => s.ident.to_string(),
-    };
-
-    if let Some(attr_type_override) = &enum_attr.type_override {
-        return type_override::type_override_enum(&enum_attr, &name, attr_type_override);
-    }
-    if let Some(attr_type_as) = &enum_attr.type_as {
-        return type_as::type_as_enum(&enum_attr, &name, attr_type_as);
-    }
-
-    if s.variants.is_empty() {
-        return Ok(empty_enum(name, enum_attr));
-    }
-
-    if s.variants.is_empty() {
-        return Ok(DerivedTS {
-            crate_rename: crate_rename.clone(),
-            ts_name: name,
-            docs: enum_attr.docs,
-            inline: quote!("never".to_owned()),
-            inline_flattened: None,
-            dependencies: Dependencies::new(crate_rename),
-            export: enum_attr.export,
-            export_to: enum_attr.export_to,
-            concrete: enum_attr.concrete,
-            bound: enum_attr.bound,
-        });
-    }
-
-    let mut formatted_variants = Vec::new();
-    let mut dependencies = Dependencies::new(crate_rename.clone());
-    for variant in &s.variants {
-        format_variant(
-            &mut formatted_variants,
-            &mut dependencies,
-            &enum_attr,
-            variant,
-        )?;
-    }
-
-    Ok(DerivedTS {
-        crate_rename,
-        inline: quote!([#(#formatted_variants),*].join(" | ")),
-        inline_flattened: Some(quote!(
-            format!("({})", [#(#formatted_variants),*].join(" | "))
-        )),
-        dependencies,
-        docs: enum_attr.docs,
-        export: enum_attr.export,
-        export_to: enum_attr.export_to,
-        ts_name: name,
-        concrete: enum_attr.concrete,
-        bound: enum_attr.bound,
-    })
-}
-
-fn format_variant(
-    formatted_variants: &mut Vec<TokenStream>,
-    dependencies: &mut Dependencies,
-    enum_attr: &EnumAttr,
-    variant: &Variant,
-) -> syn::Result<()> {
-    let crate_rename = enum_attr.crate_rename();
-
-    // If `variant.fields` is not a `Fields::Named(_)` the `rename_all_fields`
-    // attribute must be ignored to prevent a `rename_all` from getting to
-    // the newtype, tuple or unit formatting, which would cause an error
-    let variant_attr = VariantAttr::from_attrs(&variant.attrs)?;
-
-    variant_attr.assert_validity(variant)?;
-
-    if variant_attr.skip {
-        return Ok(());
-    }
-
-    let untagged_variant = variant_attr.untagged;
-    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 struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields);
-    let variant_type = types::type_def(
-        &struct_attr,
-        // In internally tagged enums, we can tag the struct
-        &name,
-        &variant.fields,
-    )?;
-    let variant_dependencies = variant_type.dependencies;
-    let inline_type = variant_type.inline;
-
-    let formatted = match (untagged_variant, enum_attr.tagged()?) {
-        (true, _) | (_, Tagged::Untagged) => quote!(#inline_type),
-        (false, Tagged::Externally) => match &variant.fields {
-            Fields::Unit => quote!(format!("\"{}\"", #name)),
-            Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
-                let field = &unnamed.unnamed[0];
-                let field_attr = FieldAttr::from_attrs(&field.attrs)?;
-
-                field_attr.assert_validity(field)?;
-
-                if field_attr.skip {
-                    quote!(format!("\"{}\"", #name))
-                } else {
-                    quote!(format!("{{ \"{}\": {} }}", #name, #inline_type))
-                }
-            }
-            _ => quote!(format!("{{ \"{}\": {} }}", #name, #inline_type)),
-        },
-        (false, Tagged::Adjacently { tag, content }) => match &variant.fields {
-            Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
-                let field = &unnamed.unnamed[0];
-                let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
-
-                field_attr.assert_validity(field)?;
-
-                if field_attr.skip {
-                    quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
-                } else {
-                    let ty = match field_attr.type_override {
-                        Some(type_override) => quote!(#type_override),
-                        None => {
-                            let ty = field_attr.type_as(&field.ty);
-                            quote!(<#ty as #crate_rename::TS>::name())
-                        }
-                    };
-                    quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
-                }
-            }
-            Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
-            _ => quote!(
-                format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #inline_type)
-            ),
-        },
-        (false, Tagged::Internally { tag }) => match variant_type.inline_flattened {
-            Some(_) => {
-                quote! { #inline_type }
-            }
-            None => match &variant.fields {
-                Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
-                    let field = &unnamed.unnamed[0];
-                    let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
-
-                    field_attr.assert_validity(field)?;
-
-                    if field_attr.skip {
-                        quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
-                    } else {
-                        let ty = match field_attr.type_override {
-                            Some(type_override) => quote! { #type_override },
-                            None => {
-                                let ty = field_attr.type_as(&field.ty);
-                                quote!(<#ty as #crate_rename::TS>::name())
-                            }
-                        };
-
-                        quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
-                    }
-                }
-                Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
-                _ => {
-                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #inline_type))
-                }
-            },
-        },
-    };
-
-    dependencies.append(variant_dependencies);
-    formatted_variants.push(formatted);
-    Ok(())
-}
-
-// bindings for an empty enum (`never` in TS)
-fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
-    let name = name.into();
-    let crate_rename = enum_attr.crate_rename();
-    DerivedTS {
-        crate_rename: crate_rename.clone(),
-        inline: quote!("never".to_owned()),
-        docs: enum_attr.docs,
-        inline_flattened: None,
-        dependencies: Dependencies::new(crate_rename),
-        export: enum_attr.export,
-        export_to: enum_attr.export_to,
-        ts_name: name,
-        concrete: enum_attr.concrete,
-        bound: enum_attr.bound,
-    }
-}
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{Fields, ItemEnum, Variant};
+
+use crate::{
+    attr::{Attr, EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
+    deps::Dependencies,
+    types::{self, type_as, type_override},
+    DerivedTS,
+};
+
+pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
+    let enum_attr: EnumAttr = EnumAttr::from_attrs(&s.attrs)?;
+
+    enum_attr.assert_validity(s)?;
+
+    let crate_rename = enum_attr.crate_rename();
+
+    let name = match &enum_attr.rename {
+        Some(existing) => existing.clone(),
+        None => s.ident.to_string(),
+    };
+
+    if let Some(attr_type_override) = &enum_attr.type_override {
+        return type_override::type_override_enum(&enum_attr, &name, attr_type_override);
+    }
+ 
+    if let Some(attr_type_as) = &enum_attr.type_as {
+        return type_as::type_as_enum(&enum_attr, &name, attr_type_as);
+    }
+
+    if s.variants.is_empty() {
+        return Ok(empty_enum(name, enum_attr));
+    }
+
+    let mut formatted_variants = Vec::new();
+    let mut dependencies = Dependencies::new(crate_rename.clone());
+
+    for variant in &s.variants {
+        format_variant(
+            &mut formatted_variants,
+            &mut dependencies,
+            &enum_attr,
+            variant,
+        )?;
+    }
+
+    Ok(DerivedTS {
+        crate_rename,
+        inline: quote!([#(#formatted_variants),*].join(" | ")),
+        inline_flattened: Some(quote!(
+            format!("({})", [#(#formatted_variants),*].join(" | "))
+        )),
+        dependencies,
+        docs: enum_attr.docs,
+        export: enum_attr.export,
+        export_to: enum_attr.export_to,
+        ts_name: name,
+        concrete: enum_attr.concrete,
+        bound: enum_attr.bound,
+    })
+}
+
+fn format_variant(
+    formatted_variants: &mut Vec<TokenStream>,
+    dependencies: &mut Dependencies,
+    enum_attr: &EnumAttr,
+    variant: &Variant,
+) -> syn::Result<()> {
+    let crate_rename = enum_attr.crate_rename();
+
+    // If `variant.fields` is not a `Fields::Named(_)` the `rename_all_fields`
+    // attribute must be ignored to prevent a `rename_all` from getting to
+    // the newtype, tuple or unit formatting, which would cause an error
+    let variant_attr = VariantAttr::from_attrs(&variant.attrs)?;
+
+    variant_attr.assert_validity(variant)?;
+
+    if variant_attr.skip {
+        return Ok(());
+    }
+
+    let untagged_variant = variant_attr.untagged;
+    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 struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields);
+    let variant_type = types::type_def(
+        &struct_attr,
+        // In internally tagged enums, we can tag the struct
+        &name,
+        &variant.fields,
+    )?;
+
+    let variant_dependencies = variant_type.dependencies;
+    let inline_type = variant_type.inline;
+
+    let parsed_ty = match (&variant_attr.type_as, &variant_attr.type_override) {
+        (Some(_), Some(_)) => syn_err_spanned!(variant; "`type` is not compatible with `as`"),
+        (Some(ty), None) => {
+            dependencies.push(ty);
+            quote!(<#ty as #crate_rename::TS>::name())
+        }
+        (None, Some(ty)) => quote!(#ty.to_owned()),
+        (None, None) => {
+            dependencies.append(variant_dependencies);
+            inline_type
+        }
+    };
+
+    let formatted = match (untagged_variant, enum_attr.tagged()?) {
+        (true, _) | (_, Tagged::Untagged) => quote!(#parsed_ty),
+        (false, Tagged::Externally) => match &variant.fields {
+            Fields::Unit => quote!(format!("\"{}\"", #name)),
+            Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
+                let field = &unnamed.unnamed[0];
+                let field_attr = FieldAttr::from_attrs(&field.attrs)?;
+
+                field_attr.assert_validity(field)?;
+
+                if field_attr.skip {
+                    quote!(format!("\"{}\"", #name))
+                } else {
+                    quote!(format!("{{ \"{}\": {} }}", #name, #parsed_ty))
+                }
+            }
+            _ => quote!(format!("{{ \"{}\": {} }}", #name, #parsed_ty)),
+        },
+        (false, Tagged::Adjacently { tag, content }) => match &variant.fields {
+            Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
+                let field = &unnamed.unnamed[0];
+                let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                field_attr.assert_validity(field)?;
+
+                if field_attr.skip {
+                    quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
+                } else {
+                    let ty = match field_attr.type_override {
+                        Some(type_override) => quote!(#type_override),
+                        None => {
+                            let ty = field_attr.type_as(&field.ty);
+                            quote!(<#ty as #crate_rename::TS>::name())
+                        }
+                    };
+                    quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty))
+                }
+            }
+            Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
+            _ => quote!(
+                format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #parsed_ty)
+            ),
+        },
+        (false, Tagged::Internally { tag }) => match variant_type.inline_flattened {
+            Some(_) => {
+                quote! { #parsed_ty }
+            }
+            None => match &variant.fields {
+                Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
+                    let field = &unnamed.unnamed[0];
+                    let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
+
+                    field_attr.assert_validity(field)?;
+
+                    if field_attr.skip {
+                        quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name))
+                    } else {
+                        let ty = match field_attr.type_override {
+                            Some(type_override) => quote! { #type_override },
+                            None => {
+                                let ty = field_attr.type_as(&field.ty);
+                                quote!(<#ty as #crate_rename::TS>::name())
+                            }
+                        };
+
+                        quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty))
+                    }
+                }
+                Fields::Unit => quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)),
+                _ => {
+                    quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #parsed_ty))
+                }
+            },
+        },
+    };
+
+    formatted_variants.push(formatted);
+    Ok(())
+}
+
+// bindings for an empty enum (`never` in TS)
+fn empty_enum(name: impl Into<String>, enum_attr: EnumAttr) -> DerivedTS {
+    let name = name.into();
+    let crate_rename = enum_attr.crate_rename();
+    DerivedTS {
+        crate_rename: crate_rename.clone(),
+        inline: quote!("never".to_owned()),
+        docs: enum_attr.docs,
+        inline_flattened: None,
+        dependencies: Dependencies::new(crate_rename),
+        export: enum_attr.export,
+        export_to: enum_attr.export_to,
+        ts_name: name,
+        concrete: enum_attr.concrete,
+        bound: enum_attr.bound,
+    }
+}


Test patch:
diff --git a/ts-rs/tests/integration/type_as.rs b/ts-rs/tests/integration/type_as.rs
index 4b55bb1..dd84f7d 100644
--- a/ts-rs/tests/integration/type_as.rs
+++ b/ts-rs/tests/integration/type_as.rs
@@ -4,6 +4,7 @@ use std::{
     cell::UnsafeCell, mem::MaybeUninit, ptr::NonNull, sync::atomic::AtomicPtr, time::Instant,
 };
 
+use serde::Serialize;
 use ts_rs::TS;
 
 type Unsupported = UnsafeCell<MaybeUninit<NonNull<AtomicPtr<i32>>>>;
@@ -58,12 +59,51 @@ enum OverrideEnum {
     },
 }
 
+mod deser {
+    use serde::{Serialize, Serializer};
+
+    use super::Instant;
+    pub fn serialize<S: Serializer>(field: &Instant, serializer: S) -> Result<S::Ok, S::Error> {
+        #[derive(Serialize)]
+        struct Foo {
+            x: i32,
+        }
+        Foo { x: 0 }.serialize(serializer)
+    }
+}
+
+#[derive(TS)]
+struct OverrideVariantDef {
+    x: i32,
+}
+
+#[derive(TS, Serialize)]
+#[ts(export, export_to = "type_as/")]
+enum OverrideVariant {
+    #[ts(as = "OverrideVariantDef")]
+    #[serde(with = "deser")]
+    A {
+        x: Instant,
+    },
+    B {
+        y: i32,
+        z: i32,
+    },
+}
+
 #[test]
 fn enum_variants() {
+    let a = OverrideVariant::A { x: Instant::now() };
+    assert_eq!(serde_json::to_string(&a).unwrap(), r#"{"A":{"x":0}}"#);
     assert_eq!(
         OverrideEnum::inline(),
         r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"#
-    )
+    );
+
+    assert_eq!(
+        OverrideVariant::inline(),
+        r#"{ "A": OverrideVariantDef } | { "B": { y: number, z: number, } }"#
+    );
 }
 
 #[derive(TS)]