Repository: Aleph-Alpha/ts-rs
Pull request #286 (2024-03-27 17:47:49)

add support for top level type overrides
## Goal

This adds support for easily defining a struct or enum's type definition manually at the top level using the `type = ".."` attribute. This is useful for when you have a custom serialize/deserialize and don't want to manually implement the `TS` trait.

## Changes

Added `type_override` to the container attributes, and defined an early-returned implementation for if this attribute is specified

## Checklist

- [x] I have followed the steps listed in the [Contributing guide](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md).
- [x] If necessary, I have added documentation related to the changes made.
- [x] I have added or updated the tests related to the changes made.


NyxCode: I like this! It's a simple change and it's cleanly implemented!  
What do you think @escritorio-gustavo?
escritorio-gustavo: @NyxCode is there anything else you'd like to change?

Patch:
diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs
index e6e4fdc..d89f13d 100644
--- a/macros/src/attr/enum.rs
+++ b/macros/src/attr/enum.rs
@@ -11,6 +11,7 @@ use crate::{
 #[derive(Default)]
 pub struct EnumAttr {
     crate_rename: Option<Path>,
+    pub type_override: Option<String>,
     pub rename_all: Option<Inflection>,
     pub rename_all_fields: Option<Inflection>,
     pub rename: Option<String>,
@@ -19,9 +20,9 @@ pub struct EnumAttr {
     pub docs: String,
     pub concrete: HashMap<Ident, Type>,
     pub bound: Option<Vec<WherePredicate>>,
-    tag: Option<String>,
-    untagged: bool,
-    content: Option<String>,
+    pub tag: Option<String>,
+    pub untagged: bool,
+    pub content: Option<String>,
 }
 
 #[cfg(feature = "serde-compat")]
@@ -71,6 +72,7 @@ impl EnumAttr {
         &mut self,
         EnumAttr {
             crate_rename,
+            type_override,
             rename_all,
             rename_all_fields,
             rename,
@@ -85,6 +87,7 @@ impl EnumAttr {
         }: EnumAttr,
     ) {
         self.crate_rename = self.crate_rename.take().or(crate_rename);
+        self.type_override = self.type_override.take().or(type_override);
         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);
@@ -110,6 +113,7 @@ impl EnumAttr {
 impl_parse! {
     EnumAttr(input, out) {
         "crate" => out.crate_rename = 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)?),
         "rename_all_fields" => out.rename_all_fields = Some(parse_assign_inflection(input)?),
diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs
index bd177f6..4464f4c 100644
--- a/macros/src/attr/struct.rs
+++ b/macros/src/attr/struct.rs
@@ -11,6 +11,7 @@ use crate::{
 #[derive(Default, Clone)]
 pub struct StructAttr {
     crate_rename: Option<Path>,
+    pub type_override: Option<String>,
     pub rename_all: Option<Inflection>,
     pub rename: Option<String>,
     pub export_to: Option<String>,
@@ -58,6 +59,7 @@ impl StructAttr {
         &mut self,
         StructAttr {
             crate_rename,
+            type_override,
             rename_all,
             rename,
             export,
@@ -69,6 +71,7 @@ impl StructAttr {
         }: StructAttr,
     ) {
         self.crate_rename = self.crate_rename.take().or(crate_rename);
+        self.type_override = self.type_override.take().or(type_override);
         self.rename = self.rename.take().or(rename);
         self.rename_all = self.rename_all.take().or(rename_all);
         self.export_to = self.export_to.take().or(export_to);
@@ -91,6 +94,7 @@ impl StructAttr {
 impl_parse! {
     StructAttr(input, out) {
         "crate" => out.crate_rename = 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_str(input).and_then(Inflection::try_from)?),
         "tag" => out.tag = Some(parse_assign_str(input)?),
diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs
index 130b4aa..a205ce2 100644
--- a/macros/src/types/enum.rs
+++ b/macros/src/types/enum.rs
@@ -5,7 +5,8 @@ use syn::{Fields, ItemEnum, Variant};
 use crate::{
     attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr},
     deps::Dependencies,
-    types, DerivedTS,
+    types::{self, type_override},
+    DerivedTS,
 };
 
 pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
@@ -18,6 +19,10 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result<DerivedTS> {
         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 s.variants.is_empty() {
         return Ok(empty_enum(name, enum_attr));
     }
diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs
index d135a77..c0ac8c3 100644
--- a/macros/src/types/mod.rs
+++ b/macros/src/types/mod.rs
@@ -6,6 +6,7 @@ mod r#enum;
 mod named;
 mod newtype;
 mod tuple;
+mod type_override;
 mod unit;
 
 pub(crate) use r#enum::r#enum_def;
@@ -18,6 +19,10 @@ pub(crate) fn struct_def(s: &ItemStruct) -> Result<DerivedTS> {
 
 fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result<DerivedTS> {
     let name = attr.rename.clone().unwrap_or_else(|| to_ts_ident(ident));
+    if let Some(attr_type_override) = &attr.type_override {
+        return type_override::type_override_struct(attr, &name, attr_type_override);
+    }
+
     match fields {
         Fields::Named(named) => match named.named.len() {
             0 => unit::empty_object(attr, &name),
diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs
new file mode 100644
index 0000000..823619b
--- /dev/null
+++ b/macros/src/types/type_override.rs
@@ -0,0 +1,73 @@
+use quote::quote;
+use syn::Result;
+
+use crate::{
+    attr::{EnumAttr, StructAttr},
+    deps::Dependencies,
+    DerivedTS,
+};
+
+pub(crate) fn type_override_struct(
+    attr: &StructAttr,
+    name: &str,
+    type_override: &str,
+) -> Result<DerivedTS> {
+    if attr.rename_all.is_some() {
+        syn_err!("`rename_all` is not compatible with `type`");
+    }
+    if attr.tag.is_some() {
+        syn_err!("`tag` is not compatible with `type`");
+    }
+
+    let crate_rename = attr.crate_rename();
+
+    Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
+        inline: quote!(#type_override.to_owned()),
+        inline_flattened: None,
+        docs: attr.docs.clone(),
+        dependencies: Dependencies::new(crate_rename),
+        export: attr.export,
+        export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
+    })
+}
+
+pub(crate) fn type_override_enum(
+    attr: &EnumAttr,
+    name: &str,
+    type_override: &str,
+) -> Result<DerivedTS> {
+    if attr.rename_all.is_some() {
+        syn_err!("`rename_all` is not compatible with `type`");
+    }
+    if attr.rename_all_fields.is_some() {
+        syn_err!("`rename_all_fields` is not compatible with `type`");
+    }
+    if attr.tag.is_some() {
+        syn_err!("`tag` is not compatible with `type`");
+    }
+    if attr.content.is_some() {
+        syn_err!("`content` is not compatible with `type`");
+    }
+    if attr.untagged {
+        syn_err!("`untagged` is not compatible with `type`");
+    }
+
+    let crate_rename = attr.crate_rename();
+
+    Ok(DerivedTS {
+        crate_rename: crate_rename.clone(),
+        inline: quote!(#type_override.to_owned()),
+        inline_flattened: None,
+        docs: attr.docs.clone(),
+        dependencies: Dependencies::new(crate_rename),
+        export: attr.export,
+        export_to: attr.export_to.clone(),
+        ts_name: name.to_owned(),
+        concrete: attr.concrete.clone(),
+        bound: attr.bound.clone(),
+    })
+}
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index 5f6827a..fad8eb5 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -204,6 +204,11 @@ pub mod typelist;
 ///   Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
 ///   <br/><br/>
 ///
+/// - **`#[ts(type = "..")]`**  
+///   Overrides the type used in TypeScript.  
+///   This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually
+///   <br/><br/>
+///
 /// - **`#[ts(rename = "..")]`**  
 ///   Sets the typescript name of the generated type
 ///   <br/><br/>


Test patch:
diff --git a/ts-rs/tests/top_level_type_override.rs b/ts-rs/tests/top_level_type_override.rs
new file mode 100644
index 0000000..e78f0b7
--- /dev/null
+++ b/ts-rs/tests/top_level_type_override.rs
@@ -0,0 +1,30 @@
+use ts_rs::TS;
+
+#[derive(TS)]
+#[ts(export, export_to = "top_level_type_override/")]
+#[ts(type = "string")]
+#[non_exhaustive]
+pub enum IncompleteEnum {
+    Foo,
+    Bar,
+    Baz,
+    // more
+}
+
+#[test]
+pub fn top_level_type_override_enum() {
+    assert_eq!(IncompleteEnum::inline(), r#"string"#)
+}
+
+#[derive(TS)]
+#[ts(export, export_to = "top_level_type_override/")]
+#[ts(type = "string")]
+pub struct DataUrl {
+    pub mime: String,
+    pub contents: Vec<u8>,
+}
+
+#[test]
+pub fn top_level_type_override_struct() {
+    assert_eq!(DataUrl::inline(), r#"string"#)
+}