Skip to content

Commit

Permalink
Merge pull request #174 from josema03/main
Browse files Browse the repository at this point in the history
add "as" derive helper attribute
  • Loading branch information
escritorio-gustavo authored Jan 31, 2024
2 parents 50f74b5 + 5773e28 commit 3b3fbf8
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 39 deletions.
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
}
},
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, }}"#)
)
}

0 comments on commit 3b3fbf8

Please sign in to comment.