Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add "as" derive helper attribute #174

Merged
merged 12 commits into from
Jan 31, 2024
4 changes: 4 additions & 0 deletions macros/src/attr/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use super::parse_assign_str;

#[derive(Default)]
pub struct FieldAttr {
pub type_as: Option<String>,
pub type_override: Option<String>,
pub rename: Option<String>,
pub inline: bool,
Expand Down Expand Up @@ -43,6 +44,7 @@ impl FieldAttr {
fn merge(
&mut self,
FieldAttr {
type_as,
type_override,
rename,
inline,
Expand All @@ -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;
Expand All @@ -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,
Expand Down
32 changes: 20 additions & 12 deletions macros/src/types/enum.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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>(&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))
}
}
Expand Down Expand Up @@ -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>(&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))
}
}
Expand Down
37 changes: 27 additions & 10 deletions macros/src/types/named.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ 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,
types::generics::{format_generics, format_type},
utils::{raw_name_to_ts_field, to_ts_ident},
DerivedTS,
};
use crate::attr::Optional;

pub(crate) fn named(
attr: &StructAttr,
Expand Down Expand Up @@ -80,6 +80,7 @@ fn format_field(
generics: &Generics,
) -> Result<()> {
let FieldAttr {
type_as,
type_override,
rename,
inline,
Expand All @@ -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>(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
NyxCode marked this conversation as resolved.
Show resolved Hide resolved
}
},
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`"),
_ => {}
}

Expand Down
30 changes: 21 additions & 9 deletions macros/src/types/newtype.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use quote::quote;
use syn::{FieldsUnnamed, Generics, Result};
use syn::{FieldsUnnamed, Generics, Result, Type};

use crate::{
attr::{FieldAttr, StructAttr},
Expand All @@ -22,6 +22,7 @@ pub(crate) fn newtype(
}
let inner = fields.unnamed.first().unwrap();
let FieldAttr {
type_as,
type_override,
rename: rename_inner,
inline,
Expand All @@ -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>(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);
Expand Down
29 changes: 21 additions & 8 deletions macros/src/types/tuple.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -58,8 +58,8 @@ fn format_field(
field: &Field,
generics: &Generics,
) -> Result<()> {
let ty = &field.ty;
let FieldAttr {
type_as,
type_override,
rename,
inline,
Expand All @@ -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>(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);
}
};

Expand Down
78 changes: 78 additions & 0 deletions ts-rs/tests/type_as.rs
Original file line number Diff line number Diff line change
@@ -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<MaybeUninit<NonNull<AtomicPtr<i32>>>>;

#[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<ExternalTypeDef>")]
#[ts(optional = nullable, inline)]
x: Unsupported,
#[ts(as = "Option<ExternalTypeDef>")]
#[ts(optional = nullable)]
y: Unsupported,
}

let external = ExternalTypeDef::inline();
assert_eq!(
Outer::inline(),
format!(r#"{{ x?: {external} | null, y?: ExternalTypeDef | null, }}"#)
)
}
Loading