From 40360d42361ddf24864f21b654dd82ce440e5c60 Mon Sep 17 00:00:00 2001 From: escritorio-gustavo <131818645+escritorio-gustavo@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:08:45 -0300 Subject: [PATCH] Rework attribute parsing (#290) --- macros/src/attr/enum.rs | 196 ++++++++++++++++++------- macros/src/attr/field.rs | 147 ++++++++++++++----- macros/src/attr/mod.rs | 29 +++- macros/src/attr/struct.rs | 137 ++++++++++------- macros/src/attr/variant.rs | 61 ++++---- macros/src/types/enum.rs | 39 +++-- macros/src/types/mod.rs | 8 +- macros/src/types/named.rs | 25 +--- macros/src/types/newtype.rs | 43 ++---- macros/src/types/tuple.rs | 36 +---- macros/src/types/type_as.rs | 28 +--- macros/src/types/type_override.rs | 28 +--- macros/src/types/unit.rs | 6 +- macros/src/utils.rs | 31 ++-- ts-rs/tests/serde-skip-with-default.rs | 2 +- 15 files changed, 484 insertions(+), 332 deletions(-) diff --git a/macros/src/attr/enum.rs b/macros/src/attr/enum.rs index 1f94bd23d..5532a69be 100644 --- a/macros/src/attr/enum.rs +++ b/macros/src/attr/enum.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; -use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate}; +use syn::{parse_quote, Attribute, Ident, ItemEnum, Path, Result, Type, WherePredicate}; -use super::{parse_assign_from_str, parse_bound}; +use super::{parse_assign_from_str, parse_bound, Attr, ContainerAttr, Serde}; use crate::{ attr::{parse_assign_inflection, parse_assign_str, parse_concrete, Inflection}, utils::{parse_attrs, parse_docs}, @@ -26,10 +26,6 @@ pub struct EnumAttr { pub content: Option, } -#[cfg(feature = "serde-compat")] -#[derive(Default)] -pub struct SerdeEnumAttr(EnumAttr); - #[derive(Copy, Clone)] pub enum Tagged<'a> { Externally, @@ -52,14 +48,17 @@ impl EnumAttr { } pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = Self::default(); - parse_attrs(attrs)?.for_each(|a| result.merge(a)); + let mut result = parse_attrs::(attrs)?; + + #[cfg(feature = "serde-compat")] + { + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); + } let docs = parse_docs(attrs)?; result.docs = docs; - #[cfg(feature = "serde-compat")] - crate::utils::parse_serde_attrs::(attrs).for_each(|a| result.merge(a.0)); Ok(result) } @@ -68,48 +67,141 @@ impl EnumAttr { .clone() .unwrap_or_else(|| parse_quote!(::ts_rs)) } +} + +impl Attr for EnumAttr { + type Item = ItemEnum; + + fn merge(self, other: Self) -> Self { + Self { + crate_rename: self.crate_rename.or(other.crate_rename), + type_as: self.type_as.or(other.type_as), + type_override: self.type_override.or(other.type_override), + rename: self.rename.or(other.rename), + rename_all: self.rename_all.or(other.rename_all), + rename_all_fields: self.rename_all_fields.or(other.rename_all_fields), + tag: self.tag.or(other.tag), + untagged: self.untagged || other.untagged, + content: self.content.or(other.content), + export: self.export || other.export, + export_to: self.export_to.or(other.export_to), + docs: other.docs, + concrete: self.concrete.into_iter().chain(other.concrete).collect(), + bound: match (self.bound, other.bound) { + (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()), + (Some(bound), None) | (None, Some(bound)) => Some(bound), + (None, None) => None, + }, + } + } + + fn assert_validity(&self, item: &Self::Item) -> Result<()> { + if self.type_override.is_some() { + if self.type_as.is_some() { + syn_err_spanned!( + item; + "`as` is not compatible with `type`" + ); + } + + if self.rename_all.is_some() { + syn_err_spanned!( + item; + "`rename_all` is not compatible with `type`" + ); + } + + if self.rename_all_fields.is_some() { + syn_err_spanned!( + item; + "`rename_all_fields` is not compatible with `type`" + ); + } + + if self.tag.is_some() { + syn_err_spanned!( + item; + "`tag` is not compatible with `type`" + ); + } + + if self.content.is_some() { + syn_err_spanned!( + item; + "`content` is not compatible with `type`" + ); + } - fn merge( - &mut self, - EnumAttr { - crate_rename, - type_as, - type_override, - rename_all, - rename_all_fields, - rename, - tag, - content, - untagged, - export_to, - export, - docs, - concrete, - bound, - }: 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); - self.rename_all_fields = self.rename_all_fields.take().or(rename_all_fields); - self.tag = self.tag.take().or(tag); - self.untagged = self.untagged || untagged; - self.content = self.content.take().or(content); - self.export = self.export || export; - self.export_to = self.export_to.take().or(export_to); - self.docs = docs; - self.concrete.extend(concrete); - self.bound = self - .bound - .take() - .map(|b| { - b.into_iter() - .chain(bound.clone().unwrap_or_default()) - .collect() - }) - .or(bound); + if self.untagged { + syn_err_spanned!( + item; + "`untagged` is not compatible with `type`" + ); + } + } + + if self.type_as.is_some() { + if self.rename_all.is_some() { + syn_err_spanned!( + item; + "`rename_all` is not compatible with `as`" + ); + } + + if self.rename_all_fields.is_some() { + syn_err_spanned!( + item; + "`rename_all_fields` is not compatible with `as`" + ); + } + + if self.tag.is_some() { + syn_err_spanned!( + item; + "`tag` is not compatible with `as`" + ); + } + + if self.content.is_some() { + syn_err_spanned!( + item; + "`content` is not compatible with `as`" + ); + } + + if self.untagged { + syn_err_spanned!( + item; + "`untagged` is not compatible with `as`" + ); + } + } + + match (self.untagged, &self.tag, &self.content) { + (true, Some(_), None) => syn_err_spanned!( + item; + "untagged cannot be used with tag" + ), + (true, _, Some(_)) => syn_err_spanned!( + item; + "untagged cannot be used with content" + ), + (false, None, Some(_)) => syn_err_spanned!( + item; + "content cannot be used without tag" + ), + _ => (), + }; + + Ok(()) + } +} + +impl ContainerAttr for EnumAttr { + fn crate_rename(&self) -> Path { + self.crate_rename + .clone() + .unwrap_or_else(|| parse_quote!(::ts_rs)) } } @@ -133,7 +225,7 @@ impl_parse! { #[cfg(feature = "serde-compat")] impl_parse! { - SerdeEnumAttr(input, out) { + Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), "rename_all_fields" => out.0.rename_all_fields = Some(parse_assign_inflection(input)?), diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs index 7345eb4a1..a1815f064 100644 --- a/macros/src/attr/field.rs +++ b/macros/src/attr/field.rs @@ -1,6 +1,6 @@ -use syn::{Attribute, Ident, Result, Type}; +use syn::{Attribute, Field, Ident, Result, Type}; -use super::{parse_assign_from_str, parse_assign_str}; +use super::{parse_assign_from_str, parse_assign_str, Attr, Serde}; use crate::utils::{parse_attrs, parse_docs}; #[derive(Default)] @@ -24,47 +24,121 @@ pub struct Optional { pub nullable: bool, } -#[cfg(feature = "serde-compat")] -#[derive(Default)] -pub struct SerdeFieldAttr(FieldAttr); - impl FieldAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = Self::default(); - parse_attrs(attrs)?.for_each(|a| result.merge(a)); - result.docs = parse_docs(attrs)?; + let mut result = parse_attrs::(attrs)?; + #[cfg(feature = "serde-compat")] if !result.skip { - crate::utils::parse_serde_attrs::(attrs) - .for_each(|a| result.merge(a.0)); + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); } + + result.docs = parse_docs(attrs)?; + Ok(result) } +} + +impl Attr for FieldAttr { + type Item = Field; - fn merge( - &mut self, - FieldAttr { - type_as, - type_override, - rename, - inline, - skip, - optional: Optional { optional, nullable }, - flatten, - docs, - }: 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; - self.optional = Optional { - optional: self.optional.optional || optional, - nullable: self.optional.nullable || nullable, - }; - self.flatten |= flatten; - self.docs.push_str(&docs); + fn merge(self, other: Self) -> Self { + Self { + type_as: self.type_as.or(other.type_as), + type_override: self.type_override.or(other.type_override), + rename: self.rename.or(other.rename), + inline: self.inline || other.inline, + skip: self.skip || other.skip, + optional: Optional { + optional: self.optional.optional || other.optional.optional, + nullable: self.optional.nullable || other.optional.nullable, + }, + flatten: self.flatten || other.flatten, + + // We can't emit TSDoc for a flattened field + // and we cant make this invalid in assert_validity because + // this documentation is totally valid in Rust + docs: if self.flatten || other.flatten { + String::new() + } else { + self.docs + &other.docs + }, + } + } + + fn assert_validity(&self, field: &Self::Item) -> Result<()> { + if self.type_override.is_some() { + if self.type_as.is_some() { + syn_err_spanned!(field; "`type` is not compatible with `as`") + } + + if self.inline { + syn_err_spanned!(field; "`type` is not compatible with `inline`") + } + + if self.flatten { + syn_err_spanned!( + field; + "`type` is not compatible with `flatten`" + ); + } + } + + if self.flatten { + if self.type_as.is_some() { + syn_err_spanned!( + field; + "`as` is not compatible with `flatten`" + ); + } + + if self.rename.is_some() { + syn_err_spanned!( + field; + "`rename` is not compatible with `flatten`" + ); + } + + if self.inline { + syn_err_spanned!( + field; + "`inline` is not compatible with `flatten`" + ); + } + + if self.optional.optional { + syn_err_spanned!( + field; + "`optional` is not compatible with `flatten`" + ); + } + } + + if field.ident.is_none() { + if self.flatten { + syn_err_spanned!( + field; + "`flatten` cannot with tuple struct fields" + ); + } + + if self.rename.is_some() { + syn_err_spanned!( + field; + "`flatten` cannot with tuple struct fields" + ); + } + + if self.optional.optional { + syn_err_spanned!( + field; + "`optional` cannot with tuple struct fields" + ); + } + } + + Ok(()) } } @@ -98,7 +172,7 @@ impl_parse! { #[cfg(feature = "serde-compat")] impl_parse! { - SerdeFieldAttr(input, out) { + Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), "skip" => out.0.skip = true, "flatten" => out.0.flatten = true, @@ -106,7 +180,6 @@ impl_parse! { "default" => { use syn::Token; if input.peek(Token![=]) { - input.parse::()?; parse_assign_str(input)?; } }, diff --git a/macros/src/attr/mod.rs b/macros/src/attr/mod.rs index b80ea7bba..2e0931feb 100644 --- a/macros/src/attr/mod.rs +++ b/macros/src/attr/mod.rs @@ -6,7 +6,7 @@ pub use r#struct::*; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, - Error, Lit, Result, Token, WherePredicate, + Error, Lit, Path, Result, Token, WherePredicate, }; pub use variant::*; @@ -26,6 +26,33 @@ pub enum Inflection { Kebab, } +pub(super) trait Attr: Default { + type Item; + + fn merge(self, other: Self) -> Self; + fn assert_validity(&self, item: &Self::Item) -> Result<()>; +} + +pub(super) trait ContainerAttr: Attr { + fn crate_rename(&self) -> Path; +} + +#[cfg(feature = "serde-compat")] +#[derive(Default)] +pub(super) struct Serde(pub T) +where + T: Attr; + +#[cfg(feature = "serde-compat")] +impl Serde +where + T: Attr, +{ + pub fn merge(self, other: Self) -> Self { + Self(self.0.merge(other.0)) + } +} + impl Inflection { pub fn apply(self, string: &str) -> String { use inflector::Inflector; diff --git a/macros/src/attr/struct.rs b/macros/src/attr/struct.rs index e0948341e..bb8d186bf 100644 --- a/macros/src/attr/struct.rs +++ b/macros/src/attr/struct.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; -use syn::{parse_quote, Attribute, Ident, Path, Result, Type, WherePredicate}; +use syn::{parse_quote, Attribute, Fields, Ident, Path, Result, Type, WherePredicate}; -use super::{parse_assign_from_str, parse_bound, parse_concrete}; +use super::{parse_assign_from_str, parse_bound, parse_concrete, Attr, ContainerAttr, Serde}; use crate::{ attr::{parse_assign_str, EnumAttr, Inflection, VariantAttr}, utils::{parse_attrs, parse_docs}, @@ -23,75 +23,108 @@ pub struct StructAttr { pub bound: Option>, } -#[cfg(feature = "serde-compat")] -#[derive(Default)] -pub struct SerdeStructAttr(StructAttr); - impl StructAttr { pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = Self::default(); - parse_attrs(attrs)?.for_each(|a| result.merge(a)); + let mut result = parse_attrs::(attrs)?; + + #[cfg(feature = "serde-compat")] + { + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); + } let docs = parse_docs(attrs)?; result.docs = docs; - - #[cfg(feature = "serde-compat")] - crate::utils::parse_serde_attrs::(attrs).for_each(|a| result.merge(a.0)); + Ok(result) } - pub fn from_variant(enum_attr: &EnumAttr, variant_attr: &VariantAttr) -> Self { + pub fn from_variant( + enum_attr: &EnumAttr, + variant_attr: &VariantAttr, + variant_fields: &Fields, + ) -> Self { Self { crate_rename: Some(enum_attr.crate_rename()), rename: variant_attr.rename.clone(), - rename_all: variant_attr.rename_all, + rename_all: variant_attr.rename_all.or(match variant_fields { + Fields::Named(_) => enum_attr.rename_all_fields, + Fields::Unnamed(_) | Fields::Unit => None, + }), // inline and skip are not supported on StructAttr ..Self::default() } } +} + +impl Attr for StructAttr { + type Item = Fields; + + fn merge(self, other: Self) -> Self { + Self { + crate_rename: self.crate_rename.or(other.crate_rename), + type_as: self.type_as.or(other.type_as), + type_override: self.type_override.or(other.type_override), + rename: self.rename.or(other.rename), + rename_all: self.rename_all.or(other.rename_all), + export_to: self.export_to.or(other.export_to), + export: self.export || other.export, + tag: self.tag.or(other.tag), + docs: other.docs, + concrete: self.concrete.into_iter().chain(other.concrete).collect(), + bound: match (self.bound, other.bound) { + (Some(a), Some(b)) => Some(a.into_iter().chain(b).collect()), + (Some(bound), None) | (None, Some(bound)) => Some(bound), + (None, None) => None, + }, + } + } - pub fn crate_rename(&self) -> Path { + fn assert_validity(&self, item: &Self::Item) -> Result<()> { + if self.type_override.is_some() { + if self.type_as.is_some() { + syn_err!("`as` is not compatible with `type`"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `type`"); + } + + if self.tag.is_some() { + syn_err!("`tag` is not compatible with `type`"); + } + } + + if self.type_as.is_some() { + if self.tag.is_some() { + syn_err!("`tag` is not compatible with `as`"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` is not compatible with `as`"); + } + } + + if !matches!(item, Fields::Named(_)) { + if self.tag.is_some() { + syn_err!("`tag` cannot be used with unit or tuple structs"); + } + + if self.rename_all.is_some() { + syn_err!("`rename_all` cannot be used with unit or tuple structs"); + } + } + + Ok(()) + } +} + +impl ContainerAttr for StructAttr { + fn crate_rename(&self) -> Path { self.crate_rename .clone() .unwrap_or_else(|| parse_quote!(::ts_rs)) } - - fn merge( - &mut self, - StructAttr { - crate_rename, - type_as, - type_override, - rename_all, - rename, - export, - export_to, - tag, - docs, - concrete, - bound, - }: 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); - self.export_to = self.export_to.take().or(export_to); - self.export = self.export || export; - self.tag = self.tag.take().or(tag); - self.docs = docs; - self.concrete.extend(concrete); - self.bound = self - .bound - .take() - .map(|b| { - b.into_iter() - .chain(bound.clone().unwrap_or_default()) - .collect() - }) - .or(bound); - } } impl_parse! { @@ -111,7 +144,7 @@ impl_parse! { #[cfg(feature = "serde-compat")] impl_parse! { - SerdeStructAttr(input, out) { + Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), "rename_all" => out.0.rename_all = Some(parse_assign_str(input).and_then(Inflection::try_from)?), "tag" => out.0.tag = Some(parse_assign_str(input)?), diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs index cca953268..52daedad6 100644 --- a/macros/src/attr/variant.rs +++ b/macros/src/attr/variant.rs @@ -1,6 +1,6 @@ -use syn::{Attribute, Ident, Result}; +use syn::{Attribute, Fields, Ident, Result, Variant}; -use super::EnumAttr; +use super::{Attr, Serde}; use crate::{ attr::{parse_assign_inflection, parse_assign_str, Inflection}, utils::parse_attrs, @@ -15,43 +15,40 @@ pub struct VariantAttr { pub untagged: bool, } -#[cfg(feature = "serde-compat")] -#[derive(Default)] -pub struct SerdeVariantAttr(VariantAttr); - impl VariantAttr { - pub fn new(attrs: &[Attribute], enum_attr: &EnumAttr) -> Result { - let mut result = Self::from_attrs(attrs)?; - result.rename_all = result.rename_all.or(enum_attr.rename_all_fields); - Ok(result) - } - pub fn from_attrs(attrs: &[Attribute]) -> Result { - let mut result = Self::default(); - parse_attrs(attrs)?.for_each(|a| result.merge(a)); + let mut result = parse_attrs::(attrs)?; #[cfg(feature = "serde-compat")] if !result.skip { - crate::utils::parse_serde_attrs::(attrs) - .for_each(|a| result.merge(a.0)); + let serde_attr = crate::utils::parse_serde_attrs::(attrs); + result = result.merge(serde_attr.0); } Ok(result) } +} + +impl Attr for VariantAttr { + type Item = Variant; + + fn merge(self, other: Self) -> Self { + Self { + rename: self.rename.or(other.rename), + rename_all: self.rename_all.or(other.rename_all), + inline: self.inline || other.inline, + skip: self.skip || other.skip, + untagged: self.untagged || other.untagged, + } + } + + fn assert_validity(&self, item: &Self::Item) -> Result<()> { + if !matches!(item.fields, Fields::Named(_)) && self.rename_all.is_some() { + syn_err_spanned!( + item; + "`rename_all` is not applicable to unit or tuple variants" + ) + } - fn merge( - &mut self, - VariantAttr { - rename, - rename_all, - inline, - skip, - untagged, - }: VariantAttr, - ) { - self.rename = self.rename.take().or(rename); - self.rename_all = self.rename_all.take().or(rename_all); - self.inline = self.inline || inline; - self.skip = self.skip || skip; - self.untagged = self.untagged || untagged; + Ok(()) } } @@ -67,7 +64,7 @@ impl_parse! { #[cfg(feature = "serde-compat")] impl_parse! { - SerdeVariantAttr(input, out) { + Serde(input, out) { "rename" => out.0.rename = Some(parse_assign_str(input)?), "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?), "skip" => out.0.skip = true, diff --git a/macros/src/types/enum.rs b/macros/src/types/enum.rs index 655500dc6..6dcc0f4e7 100644 --- a/macros/src/types/enum.rs +++ b/macros/src/types/enum.rs @@ -3,7 +3,7 @@ use quote::{format_ident, quote}; use syn::{Fields, ItemEnum, Variant}; use crate::{ - attr::{EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, + attr::{Attr, EnumAttr, FieldAttr, StructAttr, Tagged, VariantAttr}, deps::Dependencies, types::{self, type_as, type_override}, DerivedTS, @@ -12,6 +12,8 @@ use crate::{ pub(crate) fn r#enum_def(s: &ItemEnum) -> syn::Result { let enum_attr: EnumAttr = EnumAttr::from_attrs(&s.attrs)?; + enum_attr.assert_validity(s)?; + let crate_rename = enum_attr.crate_rename(); let name = match &enum_attr.rename { @@ -83,10 +85,9 @@ fn format_variant( // If `variant.fields` is not a `Fields::Named(_)` the `rename_all_fields` // attribute must be ignored to prevent a `rename_all` from getting to // the newtype, tuple or unit formatting, which would cause an error - let variant_attr = match variant.fields { - Fields::Unit | Fields::Unnamed(_) => VariantAttr::from_attrs(&variant.attrs)?, - Fields::Named(_) => VariantAttr::new(&variant.attrs, enum_attr)?, - }; + let variant_attr = VariantAttr::from_attrs(&variant.attrs)?; + + variant_attr.assert_validity(variant)?; if variant_attr.skip { return Ok(()); @@ -99,7 +100,7 @@ fn format_variant( (None, Some(rn)) => rn.apply(&variant.ident.to_string()), }; - let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr); + let struct_attr = StructAttr::from_variant(enum_attr, &variant_attr, &variant.fields); let variant_type = types::type_def( &struct_attr, // since we are generating the variant as a struct, it doesn't have a name @@ -114,8 +115,12 @@ fn format_variant( (false, Tagged::Externally) => match &variant.fields { Fields::Unit => quote!(format!("\"{}\"", #name)), Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { - let FieldAttr { skip, .. } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; - if skip { + let field = &unnamed.unnamed[0]; + let field_attr = FieldAttr::from_attrs(&field.attrs)?; + + field_attr.assert_validity(field)?; + + if field_attr.skip { quote!(format!("\"{}\"", #name)) } else { quote!(format!("{{ \"{}\": {} }}", #name, #inline_type)) @@ -125,19 +130,24 @@ fn format_variant( }, (false, Tagged::Adjacently { tag, content }) => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { + let field = &unnamed.unnamed[0]; + let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + field_attr.assert_validity(field)?; + let FieldAttr { type_as, type_override, skip, .. - } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + } = field_attr; if skip { quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)) } else { let ty = match (type_override, type_as) { (Some(_), Some(_)) => { - syn_err_spanned!(variant; "`type` is not compatible with `as`") + unreachable!("This has been handled by assert_validity") } (Some(type_override), None) => quote! { #type_override }, (None, Some(type_as)) => { @@ -176,19 +186,24 @@ fn format_variant( }, None => match &variant.fields { Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => { + let field = &unnamed.unnamed[0]; + let field_attr = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + + field_attr.assert_validity(field)?; + let FieldAttr { type_as, skip, type_override, .. - } = FieldAttr::from_attrs(&unnamed.unnamed[0].attrs)?; + } = field_attr; if skip { quote!(format!("{{ \"{}\": \"{}\" }}", #tag, #name)) } else { let ty = match (type_override, type_as) { (Some(_), Some(_)) => { - syn_err_spanned!(variant; "`type` is not compatible with `as`") + unreachable!("This has been handled by assert_validity") } (Some(type_override), None) => quote! { #type_override }, (None, Some(type_as)) => { diff --git a/macros/src/types/mod.rs b/macros/src/types/mod.rs index 4cb551db8..caebefc00 100644 --- a/macros/src/types/mod.rs +++ b/macros/src/types/mod.rs @@ -1,6 +1,10 @@ use syn::{Fields, Ident, ItemStruct, Result}; -use crate::{attr::StructAttr, utils::to_ts_ident, DerivedTS}; +use crate::{ + attr::{Attr, StructAttr}, + utils::to_ts_ident, + DerivedTS, +}; mod r#enum; mod named; @@ -19,6 +23,8 @@ pub(crate) fn struct_def(s: &ItemStruct) -> Result { } fn type_def(attr: &StructAttr, ident: &Ident, fields: &Fields) -> Result { + attr.assert_validity(fields)?; + 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); diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs index c0ddb43f4..a5db8992c 100644 --- a/macros/src/types/named.rs +++ b/macros/src/types/named.rs @@ -5,7 +5,7 @@ use syn::{ }; use crate::{ - attr::{FieldAttr, Inflection, Optional, StructAttr}, + attr::{Attr, ContainerAttr, FieldAttr, Inflection, Optional, StructAttr}, deps::Dependencies, utils::{raw_name_to_ts_field, to_ts_ident}, DerivedTS, @@ -88,6 +88,10 @@ fn format_field( field: &Field, rename_all: &Option, ) -> Result<()> { + let field_attr = FieldAttr::from_attrs(&field.attrs)?; + + field_attr.assert_validity(field)?; + let FieldAttr { type_as, type_override, @@ -97,16 +101,12 @@ fn format_field( optional, flatten, docs, - } = FieldAttr::from_attrs(&field.attrs)?; + } = field_attr; if skip { return Ok(()); } - if type_as.is_some() && type_override.is_some() { - syn_err_spanned!(field; "`type` is not compatible with `as`") - } - let parsed_ty = type_as.as_ref().unwrap_or(&field.ty).clone(); let (ty, optional_annotation) = match optional { @@ -126,18 +126,6 @@ fn format_field( }; if flatten { - match (&type_as, &type_override, &rename, inline) { - (Some(_), _, _, _) => syn_err_spanned!(field; "`as` is not compatible with `flatten`"), - (_, Some(_), _, _) => { - syn_err_spanned!(field; "`type` is not compatible with `flatten`") - } - (_, _, Some(_), _) => { - syn_err_spanned!(field; "`rename` is not compatible with `flatten`") - } - (_, _, _, true) => syn_err_spanned!(field; "`inline` is not compatible with `flatten`"), - _ => {} - } - flattened_fields.push(quote!(<#ty as #crate_rename::TS>::inline_flattened())); dependencies.append_from(ty); return Ok(()); @@ -152,6 +140,7 @@ fn format_field( quote!(<#ty as #crate_rename::TS>::name()) } }); + let field_name = to_ts_ident(field.ident.as_ref().unwrap()); let name = match (rename, rename_all) { (Some(rn), _) => rn, diff --git a/macros/src/types/newtype.rs b/macros/src/types/newtype.rs index df198da5c..9589d05bc 100644 --- a/macros/src/types/newtype.rs +++ b/macros/src/types/newtype.rs @@ -2,56 +2,39 @@ use quote::quote; use syn::{FieldsUnnamed, Result}; use crate::{ - attr::{FieldAttr, StructAttr}, + attr::{Attr, ContainerAttr, FieldAttr, StructAttr}, deps::Dependencies, DerivedTS, }; pub(crate) fn newtype(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result { - if attr.rename_all.is_some() { - syn_err!("`rename_all` is not applicable to newtype structs"); - } - if attr.tag.is_some() { - syn_err!("`tag` is not applicable to newtype structs"); - } let inner = fields.unnamed.first().unwrap(); + + let field_attr = FieldAttr::from_attrs(&inner.attrs)?; + field_attr.assert_validity(inner)?; + let FieldAttr { type_as, type_override, - rename: rename_inner, inline, skip, - optional, - flatten, - docs: _, - } = FieldAttr::from_attrs(&inner.attrs)?; + .. + } = field_attr; let crate_rename = attr.crate_rename(); - match (&rename_inner, skip, optional.optional, flatten) { - (Some(_), ..) => syn_err_spanned!(fields; "`rename` is not applicable to newtype fields"), - (_, true, ..) => return super::unit::null(attr, name), - (_, _, true, ..) => { - syn_err_spanned!(fields; "`optional` is not applicable to newtype fields") - } - (_, _, _, true) => { - syn_err_spanned!(fields; "`flatten` is not applicable to newtype fields") - } - _ => {} - }; - - if type_as.is_some() && type_override.is_some() { - syn_err_spanned!(fields; "`type` is not compatible with `as`") + if skip { + return super::unit::null(attr, name); } let inner_ty = type_as.as_ref().unwrap_or(&inner.ty).clone(); let mut dependencies = Dependencies::new(crate_rename.clone()); - match (type_override.is_none(), inline) { - (false, _) => (), - (true, true) => dependencies.append_from(&inner_ty), - (true, false) => dependencies.push(&inner_ty), + match (&type_override, inline) { + (Some(_), _) => (), + (None, true) => dependencies.append_from(&inner_ty), + (None, false) => dependencies.push(&inner_ty), }; let inline_def = match type_override { diff --git a/macros/src/types/tuple.rs b/macros/src/types/tuple.rs index e85ca1a39..2b3f44a15 100644 --- a/macros/src/types/tuple.rs +++ b/macros/src/types/tuple.rs @@ -3,19 +3,12 @@ use quote::quote; use syn::{Field, FieldsUnnamed, Path, Result}; use crate::{ - attr::{FieldAttr, StructAttr}, + attr::{Attr, ContainerAttr, FieldAttr, StructAttr}, deps::Dependencies, DerivedTS, }; pub(crate) fn tuple(attr: &StructAttr, name: &str, fields: &FieldsUnnamed) -> Result { - if attr.rename_all.is_some() { - syn_err!("`rename_all` is not applicable to tuple structs"); - } - if attr.tag.is_some() { - syn_err!("`tag` is not applicable to tuple structs"); - } - let crate_rename = attr.crate_rename(); let mut formatted_fields = Vec::new(); let mut dependencies = Dependencies::new(crate_rename.clone()); @@ -53,16 +46,19 @@ fn format_field( dependencies: &mut Dependencies, field: &Field, ) -> Result<()> { + let field_attr = FieldAttr::from_attrs(&field.attrs)?; + field_attr.assert_validity(field)?; + let FieldAttr { type_as, type_override, - rename, + rename: _, inline, skip, - optional, - flatten, + optional: _, + flatten: _, docs: _, - } = FieldAttr::from_attrs(&field.attrs)?; + } = field_attr; if skip { return Ok(()); @@ -70,22 +66,6 @@ fn format_field( let ty = type_as.as_ref().unwrap_or(&field.ty).clone(); - if type_as.is_some() && type_override.is_some() { - syn_err_spanned!(field; "`type` is not compatible with `as`") - } - - if rename.is_some() { - syn_err_spanned!(field; "`rename` is not applicable to tuple structs") - } - - if optional.optional { - syn_err_spanned!(field; "`optional` is not applicable to tuple fields") - } - - if flatten { - syn_err_spanned!(field; "`flatten` is not applicable to tuple fields") - } - formatted_fields.push(match type_override { Some(ref o) => quote!(#o.to_owned()), None if inline => quote!(<#ty as #crate_rename::TS>::inline()), diff --git a/macros/src/types/type_as.rs b/macros/src/types/type_as.rs index e964af11f..9a76eac24 100644 --- a/macros/src/types/type_as.rs +++ b/macros/src/types/type_as.rs @@ -2,19 +2,12 @@ use quote::quote; use syn::{Result, Type}; use crate::{ - attr::{EnumAttr, StructAttr}, + attr::{ContainerAttr, 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 { @@ -32,25 +25,6 @@ pub(crate) fn type_as_struct(attr: &StructAttr, name: &str, type_as: &Type) -> R } 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 { diff --git a/macros/src/types/type_override.rs b/macros/src/types/type_override.rs index 8ebc91105..1f9c06f91 100644 --- a/macros/src/types/type_override.rs +++ b/macros/src/types/type_override.rs @@ -2,7 +2,7 @@ use quote::quote; use syn::Result; use crate::{ - attr::{EnumAttr, StructAttr}, + attr::{ContainerAttr, EnumAttr, StructAttr}, deps::Dependencies, DerivedTS, }; @@ -12,13 +12,6 @@ pub(crate) fn type_override_struct( name: &str, type_override: &str, ) -> Result { - 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 { @@ -40,25 +33,6 @@ pub(crate) fn type_override_enum( name: &str, type_override: &str, ) -> Result { - 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`"); - } - if attr.type_as.is_some() { - syn_err!("`type` is not compatible with `as`"); - } - let crate_rename = attr.crate_rename(); Ok(DerivedTS { diff --git a/macros/src/types/unit.rs b/macros/src/types/unit.rs index 6a3c0c36f..1a379545a 100644 --- a/macros/src/types/unit.rs +++ b/macros/src/types/unit.rs @@ -1,7 +1,11 @@ use quote::quote; use syn::Result; -use crate::{attr::StructAttr, deps::Dependencies, DerivedTS}; +use crate::{ + attr::{ContainerAttr, StructAttr}, + deps::Dependencies, + DerivedTS, +}; pub(crate) fn empty_object(attr: &StructAttr, name: &str) -> Result { check_attributes(attr)?; diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 01a99538d..51511d3dd 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -7,6 +7,7 @@ use syn::{ Result, Type, }; +use super::attr::{Attr, Serde}; use crate::deps::Dependencies; macro_rules! syn_err { @@ -25,16 +26,16 @@ macro_rules! syn_err_spanned { } macro_rules! impl_parse { - ($i:ident ($input:ident, $out:ident) { $($k:pat => $e:expr),* $(,)? }) => { - impl std::convert::TryFrom<&syn::Attribute> for $i { + ($i:ident $(<$inner: ident>)? ($input:ident, $out:ident) { $($k:pat => $e:expr),* $(,)? }) => { + impl std::convert::TryFrom<&syn::Attribute> for $i $(<$inner>)? { type Error = syn::Error; fn try_from(attr: &syn::Attribute) -> syn::Result { attr.parse_args() } } - impl syn::parse::Parse for $i { + impl syn::parse::Parse for $i $(<$inner>)? { fn parse($input: syn::parse::ParseStream) -> syn::Result { - let mut $out = $i::default(); + let mut $out = Self::default(); loop { let span = $input.span(); let key: Ident = $input.call(syn::ext::IdentExt::parse_any)?; @@ -96,28 +97,33 @@ pub fn raw_name_to_ts_field(value: String) -> String { } /// Parse all `#[ts(..)]` attributes from the given slice. -pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result> +pub fn parse_attrs<'a, A>(attrs: &'a [Attribute]) -> Result where - A: TryFrom<&'a Attribute, Error = Error>, + A: TryFrom<&'a Attribute, Error = Error> + Attr, { Ok(attrs .iter() .filter(|a| a.path().is_ident("ts")) .map(A::try_from) .collect::>>()? - .into_iter()) + .into_iter() + .fold(A::default(), |acc, cur| acc.merge(cur))) } /// Parse all `#[serde(..)]` attributes from the given slice. #[cfg(feature = "serde-compat")] #[allow(unused)] -pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>( - attrs: &'a [Attribute], -) -> impl Iterator { +pub fn parse_serde_attrs<'a, A>(attrs: &'a [Attribute]) -> Serde +where + A: Attr, + Serde: TryFrom<&'a Attribute, Error = Error>, +{ + use crate::attr::Serde; + attrs .iter() .filter(|a| a.path().is_ident("serde")) - .flat_map(|attr| match A::try_from(attr) { + .flat_map(|attr| match Serde::::try_from(attr) { Ok(attr) => Some(attr), Err(_) => { #[cfg(not(feature = "no-serde-warnings"))] @@ -133,8 +139,7 @@ pub fn parse_serde_attrs<'a, A: TryFrom<&'a Attribute, Error = Error>>( None } }) - .collect::>() - .into_iter() + .fold(Serde::::default(), |acc, cur| acc.merge(cur)) } /// Return doc comments parsed and formatted as JSDoc. diff --git a/ts-rs/tests/serde-skip-with-default.rs b/ts-rs/tests/serde-skip-with-default.rs index fc11b9b63..b67e2e581 100644 --- a/ts-rs/tests/serde-skip-with-default.rs +++ b/ts-rs/tests/serde-skip-with-default.rs @@ -12,7 +12,7 @@ fn default_http_version() -> String { #[derive(Debug, Clone, Deserialize, Serialize, TS)] #[ts(export, export_to = "serde_skip_with_default/")] pub struct Foobar { - #[ts(skip)] + // #[ts(skip)] #[serde(skip, default = "default_http_version")] pub http_version: String, pub something_else: i32,