diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index d45e30ffc..3625224e6 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -7,6 +7,7 @@ use super::parse_assign_str; #[derive(Default)] pub struct FieldAttr { + pub type_as: Option, pub type_override: Option, pub rename: Option, pub inline: bool, @@ -43,6 +44,7 @@ impl FieldAttr { fn merge( &mut self, FieldAttr { + type_as, type_override, rename, inline, @@ -52,6 +54,7 @@ impl FieldAttr { }: FieldAttr, ) { self.rename = self.rename.take().or(rename); + self.type_as = self.type_as.take().or(type_as); self.type_override = self.type_override.take().or(type_override); self.inline = self.inline || inline; self.skip = self.skip || skip; @@ -65,6 +68,7 @@ impl FieldAttr { impl_parse! { FieldAttr(input, out) { + "as" => out.type_as = Some(parse_assign_str(input)?), "type" => out.type_override = Some(parse_assign_str(input)?), "rename" => out.rename = Some(parse_assign_str(input)?), "inline" => out.inline = true, diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 5c0e05669..8978fa52d 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Fields, Generics, ItemEnum, Variant}; +use syn::{Fields, Generics, ItemEnum, Type, Variant}; use crate::{ attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, @@ -106,18 +106,22 @@ fn format_variant( Tagged::Adjacently { tag, content } => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { let FieldAttr { + type_as, type_override, skip, .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + let ty = match (type_override, type_as) { + (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), + (Some(type_override), None) => quote! { #type_override }, + (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), + (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), + }; + if skip { quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)) } else { - let ty = if let Some(type_override) = type_override { - quote! { #type_override } - } else { - format_type(&unnamed.unnamed[0].ty, dependencies, generics) - }; quote!(format!("{{ \"{}\": \"{}\", \"{}\": {} }}", #tag, #name, #content, #ty)) } } @@ -146,18 +150,22 @@ fn format_variant( None => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { let FieldAttr { - type_override, + type_as, skip, + type_override, .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + let ty = match (type_override, type_as) { + (Some(_), Some(_)) => syn_err!("`type` is not compatible with `as`"), + (Some(type_override), None) => quote! { #type_override }, + (None, Some(type_as)) => format_type(&syn::parse_str::(&type_as)?, dependencies, generics), + (None, None) => format_type(&unnamed.unnamed[0].ty, dependencies, generics), + }; + if skip { quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)) } else { - let ty = if let Some(type_override) = type_override { - quote! { #type_override } - } else { - format_type(&unnamed.unnamed[0].ty, dependencies, generics) - }; quote!(format!("{{ \"{}\": \"{}\" }} & {}", #tag, #name, #ty)) } } diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index e3141d08f..d61267b89 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -2,6 +2,7 @@ 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}, deps::Dependencies, @@ -9,7 +10,6 @@ use crate::{ utils::{raw_name_to_ts_field, to_ts_ident}, DerivedTS, }; -use crate::attr::Optional; pub(crate) fn named( attr: &StructAttr, @@ -80,6 +80,7 @@ fn format_field( generics: &Generics, ) -> Result<()> { let FieldAttr { + type_as, type_override, rename, inline, @@ -92,22 +93,38 @@ fn format_field( return Ok(()); } + if type_as.is_some() && type_override.is_some() { + syn_err!("`type` is not compatible with `as`") + } + + let parsed_ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? + } else { + field.ty.clone() + }; + let (ty, optional_annotation) = match optional { - Optional { optional: true, nullable } => { - let inner_type = extract_option_argument(&field.ty)?; // inner type of the optional + Optional { + optional: true, + nullable, + } => { + let inner_type = extract_option_argument(&parsed_ty)?; // inner type of the optional match nullable { - true => (&field.ty, "?"), // if it's nullable, we keep the original type + true => (&parsed_ty, "?"), // if it's nullable, we keep the original type false => (inner_type, "?"), // if not, we use the Option's inner type } - }, - Optional { optional: false, .. } => (&field.ty, "") + } + Optional { + optional: false, .. + } => (&parsed_ty, ""), }; if flatten { - match (&type_override, &rename, inline) { - (Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"), - (_, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"), - (_, _, true) => syn_err!("`inline` is not compatible with `flatten`"), + match (&type_as, &type_override, &rename, inline) { + (Some(_), _, _, _) => syn_err!("`as` is not compatible with `flatten`"), + (_, Some(_), _, _) => syn_err!("`type` is not compatible with `flatten`"), + (_, _, Some(_), _) => syn_err!("`rename` is not compatible with `flatten`"), + (_, _, _, true) => syn_err!("`inline` is not compatible with `flatten`"), _ => {} } diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index 24730717a..f1038b6f4 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -1,5 +1,5 @@ use quote::quote; -use syn::{FieldsUnnamed, Generics, Result}; +use syn::{FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, @@ -22,6 +22,7 @@ pub(crate) fn newtype( } let inner = fields.unnamed.first().unwrap(); let FieldAttr { + type_as, type_override, rename: rename_inner, inline, @@ -38,17 +39,28 @@ pub(crate) fn newtype( _ => {} }; - let inner_ty = &inner.ty; + if type_as.is_some() && type_override.is_some() { + syn_err!("`type` is not compatible with `as`") + } + + let inner_ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? + } else { + inner.ty.clone() + }; + let mut dependencies = Dependencies::default(); - match (inline, &type_override) { - (_, Some(_)) => (), - (true, _) => dependencies.append_from(inner_ty), - (false, _) => dependencies.push_or_append_from(inner_ty), + + match (type_override.is_none(), inline) { + (false, _) => (), + (true, true) => dependencies.append_from(&inner_ty), + (true, false) => dependencies.push_or_append_from(&inner_ty), }; - let inline_def = match &type_override { - Some(o) => quote!(#o.to_owned()), + + let inline_def = match type_override { + Some(ref o) => quote!(#o.to_owned()), None if inline => quote!(<#inner_ty as ts_rs::TS>::inline()), - None => format_type(inner_ty, &mut dependencies, generics), + None => format_type(&inner_ty, &mut dependencies, generics), }; let generic_args = format_generics(&mut dependencies, generics); diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index 71ae7f024..bc98005b7 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Field, FieldsUnnamed, Generics, Result}; +use syn::{Field, FieldsUnnamed, Generics, Result, Type}; use crate::{ attr::{FieldAttr, StructAttr}, @@ -58,8 +58,8 @@ fn format_field( field: &Field, generics: &Generics, ) -> Result<()> { - let ty = &field.ty; let FieldAttr { + type_as, type_override, rename, inline, @@ -71,29 +71,42 @@ fn format_field( if skip { return Ok(()); } + + let ty = if let Some(ref type_as) = type_as { + syn::parse_str::(type_as)? + } else { + field.ty.clone() + }; + + if type_as.is_some() && type_override.is_some() { + syn_err!("`type` is not compatible with `as`") + } + if rename.is_some() { syn_err!("`rename` is not applicable to tuple structs") } + if optional.optional { syn_err!("`optional` is not applicable to tuple fields") } + if flatten { syn_err!("`flatten` is not applicable to tuple fields") } - formatted_fields.push(match &type_override { - Some(o) => quote!(#o.to_owned()), + formatted_fields.push(match type_override { + Some(ref o) => quote!(#o.to_owned()), None if inline => quote!(<#ty as ts_rs::TS>::inline()), - None => format_type(ty, dependencies, generics), + None => format_type(&ty, dependencies, generics), }); - match (inline, &type_override) { + match (inline, type_override) { (_, Some(_)) => (), (false, _) => { - dependencies.push_or_append_from(ty); + dependencies.push_or_append_from(&ty); } (true, _) => { - dependencies.append_from(ty); + dependencies.append_from(&ty); } }; diff --git a/ts-rs/tests/type_as.rs b/ts-rs/tests/type_as.rs new file mode 100644 index 000000000..f727bc75d --- /dev/null +++ b/ts-rs/tests/type_as.rs @@ -0,0 +1,78 @@ +#![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 ts_rs::TS; + +type Unsupported = UnsafeCell>>>; + +#[derive(TS)] +struct ExternalTypeDef { + a: i32, + b: i32, + c: i32, +} + +#[test] +fn struct_properties() { + #[derive(TS)] + struct Override { + a: i32, + #[ts(as = "ExternalTypeDef")] + #[ts(inline)] + x: Instant, + // here, 'as' just behaves like 'type' (though it adds a dependency!) + #[ts(as = "ExternalTypeDef")] + y: Unsupported, + } + + assert_eq!( + Override::inline(), + "{ a: number, x: { a: number, b: number, c: number, }, y: ExternalTypeDef, }" + ); + assert!(Override::dependencies() + .iter() + .any(|d| d.ts_name == "ExternalTypeDef")); +} + +#[test] +fn enum_variants() { + #[derive(TS)] + enum OverrideEnum { + A(#[ts(as = "ExternalTypeDef")] Instant), + B { + #[ts(as = "ExternalTypeDef")] + x: Unsupported, + y: i32, + z: i32, + }, + } + + assert_eq!( + OverrideEnum::inline(), + r#"{ "A": ExternalTypeDef } | { "B": { x: ExternalTypeDef, y: number, z: number, } }"# + ) +} + +#[test] +fn complex() { + #[derive(TS)] + struct Outer { + #[ts(as = "Option")] + #[ts(optional = nullable, inline)] + x: Unsupported, + #[ts(as = "Option")] + #[ts(optional = nullable)] + y: Unsupported, + } + + let external = ExternalTypeDef::inline(); + assert_eq!( + Outer::inline(), + format!(r#"{{ x?: {external} | null, y?: ExternalTypeDef | null, }}"#) + ) +}