From c862aa6208b0294ced07d25358a2c74ecaf26b03 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:10:34 -0600 Subject: [PATCH] add support for top level `as` (#288) --- macros/src/attr/enum.rs | 4 ++ macros/src/attr/struct.rs | 4 ++ macros/src/types/enum.rs | 5 ++- macros/src/types/mod.rs | 4 ++ macros/src/types/type_as.rs | 68 +++++++++++++++++++++++++++++++ macros/src/types/type_override.rs | 3 ++ ts-rs/src/lib.rs | 5 +++ ts-rs/tests/top_level_type_as.rs | 22 ++++++++++ 8 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 macros/src/types/type_as.rs create mode 100644 ts-rs/tests/top_level_type_as.rs diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index d89f13d7..1f94bd23 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, + pub type_as: Option, pub type_override: Option, pub rename_all: Option, pub rename_all_fields: Option, @@ -72,6 +73,7 @@ impl EnumAttr { &mut self, EnumAttr { crate_rename, + type_as, type_override, rename_all, rename_all_fields, @@ -87,6 +89,7 @@ impl EnumAttr { }: EnumAttr, ) { self.crate_rename = self.crate_rename.take().or(crate_rename); + self.type_as = self.type_as.take().or(type_as); 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); @@ -113,6 +116,7 @@ impl EnumAttr { impl_parse! { EnumAttr(input, out) { "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), + "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)?), diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index 4464f4c2..e0948341 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, + pub type_as: Option, pub type_override: Option, pub rename_all: Option, pub rename: Option, @@ -59,6 +60,7 @@ impl StructAttr { &mut self, StructAttr { crate_rename, + type_as, type_override, rename_all, rename, @@ -71,6 +73,7 @@ impl StructAttr { }: StructAttr, ) { self.crate_rename = self.crate_rename.take().or(crate_rename); + self.type_as = self.type_as.take().or(type_as); 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); @@ -94,6 +97,7 @@ impl StructAttr { impl_parse! { StructAttr(input, out) { "crate" => out.crate_rename = Some(parse_assign_from_str(input)?), + "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_str(input).and_then(Inflection::try_from)?), diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index afaa8a17..655500dc 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -5,7 +5,7 @@ use syn::{Fields, ItemEnum, Variant}; use crate::{ attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, deps::Dependencies, - types::{self, type_override}, + types::{self, type_as, type_override}, DerivedTS, }; @@ -22,6 +22,9 @@ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { 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)); diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs index c0ac8c31..4cb551db 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_as; mod type_override; mod unit; @@ -22,6 +23,9 @@ fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result match named.named.len() { diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs new file mode 100644 index 00000000..e964af11 --- /dev/null +++ b/macros/src/types/type_as.rs @@ -0,0 +1,68 @@ +use quote::quote; +use syn::{Result, Type}; + +use crate::{ + attr::{EnumAttr, StructAttr}, + deps::Dependencies, + DerivedTS, +}; + +pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> Result { + if attr.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `as`"); + } + if attr.tag.is_some() { + syn_err!("`tag` is not compatible with `as`"); + } + + let crate_rename = attr.crate_rename(); + + Ok(DerivedTS { + crate_rename: crate_rename.clone(), + inline: quote!(#type_as::inline()), + 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_as_enum(attr: &EnumAttr, name: &str, type_as: &Type) -> Result { + if attr.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `as`"); + } + if attr.rename_all_fields.is_some() { + syn_err!("`rename_all_fields` is not compatible with `as`"); + } + if attr.tag.is_some() { + syn_err!("`tag` is not compatible with `as`"); + } + if attr.content.is_some() { + syn_err!("`content` is not compatible with `as`"); + } + if attr.untagged { + syn_err!("`untagged` is not compatible with `as`"); + } + if attr.type_override.is_some() { + syn_err!("`type` is not compatible with `as`"); + } + + let crate_rename = attr.crate_rename(); + + Ok(DerivedTS { + crate_rename: crate_rename.clone(), + inline: quote!(#type_as::inline()), + 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/macros/src/types/type_override.rs b/macros/src/types/type_override.rs index 823619b8..8ebc9110 100644 --- a/macros/src/types/type_override.rs +++ b/macros/src/types/type_override.rs @@ -55,6 +55,9 @@ pub(crate) fn type_override_enum( if attr.untagged { syn_err!("`untagged` is not compatible with `type`"); } + if attr.type_as.is_some() { + syn_err!("`type` is not compatible with `as`"); + } let crate_rename = attr.crate_rename(); diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs index fad8eb53..1560ac31 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. ///

/// +/// - **`#[ts(as = "..")]`** +/// Overrides the type used in Typescript, using the provided Rust type instead. +/// This is useful when you have a custom serializer and deserializer and don't want to implement `TS` manually +///

+/// /// - **`#[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 diff --git a/ts-rs/tests/top_level_type_as.rs b/ts-rs/tests/top_level_type_as.rs new file mode 100644 index 00000000..940c855f --- /dev/null +++ b/ts-rs/tests/top_level_type_as.rs @@ -0,0 +1,22 @@ +use ts_rs::TS; + +#[derive(TS)] +#[ts(as = "T")] +pub enum UntaggedEnum { + Left(T), + Right(T), +} + +#[test] +pub fn top_level_type_as_enum() { + assert_eq!(UntaggedEnum::::inline(), r#"string"#) +} + +#[derive(TS)] +#[ts(as = "T")] +pub struct Wrapper(T); + +#[test] +pub fn top_level_type_as_struct() { + assert_eq!(Wrapper::::inline(), r#"string"#) +}