Repository: Aleph-Alpha/ts-rs
Pull request #226 (2024-02-05 17:46:07)

Add support for `#[serde(untagged)]` on enum variants
As of https://github.com/serde-rs/serde/pull/2403 serde supports adding `#[serde(untagged)]` to individual enum variants, this PR adds support for it to `ts-rs`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a381c2..70cf05e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
 - Support `HashMap` with custom hashers ([#173](https://github.com/Aleph-Alpha/ts-rs/pull/173))
 - 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])
 
 ### 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/macros/src/attr/field.rs b/macros/src/attr/field.rs
index 5897dae..c66b299 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -1,9 +1,7 @@
-use syn::spanned::Spanned;
-use syn::{Attribute, Ident, Result};
-
-use crate::utils::parse_attrs;
+use syn::{spanned::Spanned, Attribute, Ident, Result};
 
 use super::parse_assign_str;
+use crate::utils::parse_attrs;
 
 #[derive(Default)]
 pub struct FieldAttr {
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index 74a3c28..a3f54b0 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -11,6 +11,7 @@ pub struct VariantAttr {
     pub rename_all: Option<Inflection>,
     pub inline: bool,
     pub skip: bool,
+    pub untagged: bool,
 }
 
 #[cfg(feature = "serde-compat")]
@@ -36,12 +37,14 @@ impl VariantAttr {
             rename_all,
             inline,
             skip,
+            untagged,
         }: 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;
+        self.untagged = self.untagged || untagged
     }
 }
 
@@ -51,6 +54,7 @@ impl_parse! {
         "rename_all" => out.rename_all = Some(parse_assign_inflection(input)?),
         "inline" => out.inline = true,
         "skip" => out.skip = true,
+        "untagged" => out.untagged = true,
     }
 }
 
@@ -60,5 +64,6 @@ impl_parse! {
         "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,
+        "untagged" => out.0.untagged = true,
     }
 }
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index a473822..fd5ab0f 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -73,6 +73,7 @@ fn format_variant(
         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(),
@@ -89,9 +90,9 @@ fn format_variant(
     let variant_dependencies = variant_type.dependencies;
     let inline_type = variant_type.inline;
 
-    let formatted = match enum_attr.tagged()? {
-        Tagged::Untagged => quote!(#inline_type),
-        Tagged::Externally => match &variant.fields {
+    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 FieldAttr { skip, .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?;
@@ -103,7 +104,7 @@ fn format_variant(
             }
             _ => quote!(format!("{{ \"{}\": {} }}", #name, #inline_type)),
         },
-        Tagged::Adjacently { tag, content } => match &variant.fields {
+        (false, Tagged::Adjacently { tag, content }) => match &variant.fields {
             Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => {
                 let FieldAttr {
                     type_as,
@@ -132,7 +133,7 @@ fn format_variant(
                 format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #inline_type)
             ),
         },
-        Tagged::Internally { tag } => match variant_type.inline_flattened {
+        (false, Tagged::Internally { tag }) => match variant_type.inline_flattened {
             Some(inline_flattened) => quote! {
                 format!(
                     "{{ \"{}\": \"{}\", {} }}",
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index d61267b..8f0375d 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -2,9 +2,8 @@ use proc_macro2::TokenStream;
 use quote::quote;
 use syn::{Field, FieldsNamed, GenericArgument, Generics, PathArguments, Result, Type};
 
-use crate::attr::Optional;
 use crate::{
-    attr::{FieldAttr, Inflection, StructAttr},
+    attr::{FieldAttr, Inflection, Optional, StructAttr},
     deps::Dependencies,
     types::generics::{format_generics, format_type},
     utils::{raw_name_to_ts_field, to_ts_ident},
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index 626fed9..ce1c511 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -3,11 +3,10 @@ use std::{
     collections::BTreeMap,
     fmt::Write,
     path::{Component, Path, PathBuf},
+    sync::Mutex,
 };
-use std::sync::Mutex;
 
 use thiserror::Error;
-
 use ExportError::*;
 
 use crate::TS;
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 14784c3..dbfd7f8 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -165,11 +165,10 @@ use std::{
     path::{Path, PathBuf},
 };
 
-use crate::typelist::TypeList;
-
 pub use ts_rs_macros::TS;
 
 pub use crate::export::ExportError;
+use crate::typelist::TypeList;
 
 #[cfg(feature = "chrono-impl")]
 mod chrono;
diff --git a/ts-rs/src/typelist.rs b/ts-rs/src/typelist.rs
index edb46f6..8a94994 100644
--- a/ts-rs/src/typelist.rs
+++ b/ts-rs/src/typelist.rs
@@ -1,5 +1,4 @@
-use std::any::TypeId;
-use std::marker::PhantomData;
+use std::{any::TypeId, marker::PhantomData};
 
 use crate::TS;
 


Test patch:
diff --git a/ts-rs/tests/enum_flattening.rs b/ts-rs/tests/enum_flattening.rs
index 07a7af6..b9d9de9 100644
--- a/ts-rs/tests/enum_flattening.rs
+++ b/ts-rs/tests/enum_flattening.rs
@@ -50,14 +50,25 @@ fn adjacently_tagged() {
     #[cfg_attr(feature = "serde-compat", serde(tag = "type", content = "stuff"))]
     #[cfg_attr(not(feature = "serde-compat"), ts(tag = "type", content = "stuff"))]
     enum Bar {
-        Baz { a: i32, a2: String },
-        Biz { b: bool },
-        Buz { c: String, d: Option<i32> },
+        Baz {
+            a: i32,
+            a2: String,
+        },
+        Biz {
+            b: bool,
+        },
+
+        #[cfg_attr(feature = "serde-compat", serde(untagged))]
+        #[cfg_attr(not(feature = "serde-compat"), ts(untagged))]
+        Buz {
+            c: String,
+            d: Option<i32>,
+        },
     }
 
     assert_eq!(
         Foo::inline(),
-        r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { "type": "Buz", "stuff": { c: string, d: number | null, } })"#
+        r#"{ one: number, qux: string | null, } & ({ "type": "Baz", "stuff": { a: number, a2: string, } } | { "type": "Biz", "stuff": { b: boolean, } } | { c: string, d: number | null, })"#
     )
 }
 
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
index a588e9c..1fc5354 100644
--- a/ts-rs/tests/hashmap.rs
+++ b/ts-rs/tests/hashmap.rs
@@ -1,4 +1,5 @@
 use std::collections::{HashMap, HashSet};
+
 use ts_rs::TS;
 
 #[test]
diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs
index 10fd8ed..47c4bd9 100644
--- a/ts-rs/tests/ranges.rs
+++ b/ts-rs/tests/ranges.rs
@@ -1,5 +1,7 @@
-use std::collections::BTreeSet;
-use std::ops::{Range, RangeInclusive};
+use std::{
+    collections::BTreeSet,
+    ops::{Range, RangeInclusive},
+};
 
 use ts_rs::{Dependency, TS};
 
diff --git a/ts-rs/tests/self_referential.rs b/ts-rs/tests/self_referential.rs
index 0019686..c7fbad4 100644
--- a/ts-rs/tests/self_referential.rs
+++ b/ts-rs/tests/self_referential.rs
@@ -1,7 +1,7 @@
 #![allow(dead_code)]
 
-use std::collections::HashMap;
-use std::sync::Arc;
+use std::{collections::HashMap, sync::Arc};
+
 use ts_rs::TS;
 
 #[test]
diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs
index f727bc7..ff5bbb5 100644
--- a/ts-rs/tests/type_as.rs
+++ b/ts-rs/tests/type_as.rs
@@ -1,10 +1,8 @@
 #![allow(dead_code)]
 
-use std::cell::UnsafeCell;
-use std::mem::MaybeUninit;
-use std::ptr::NonNull;
-use std::sync::atomic::AtomicPtr;
-use std::time::Instant;
+use std::{
+    cell::UnsafeCell, mem::MaybeUninit, ptr::NonNull, sync::atomic::AtomicPtr, time::Instant,
+};
 
 use ts_rs::TS;