From 24308e435dc66ac7b61ccd2092f3c74da70b14a2 Mon Sep 17 00:00:00 2001 From: Victorien Elvinger Date: Sun, 18 Feb 2024 15:25:17 +0100 Subject: [PATCH] feat(deserializable_macro): add `from` and `try_from` attributes (#1853) --- .../src/deserializable_derive.rs | 325 ++++++++++++------ .../deserializable_derive/container_attrs.rs | 84 +++++ .../enum_variant_attrs.rs | 2 +- .../src/deserializable_derive/struct_attrs.rs | 38 -- .../struct_field_attrs.rs | 8 +- crates/biome_deserialize_macros/src/lib.rs | 90 ++++- 6 files changed, 401 insertions(+), 146 deletions(-) create mode 100644 crates/biome_deserialize_macros/src/deserializable_derive/container_attrs.rs delete mode 100644 crates/biome_deserialize_macros/src/deserializable_derive/struct_attrs.rs diff --git a/crates/biome_deserialize_macros/src/deserializable_derive.rs b/crates/biome_deserialize_macros/src/deserializable_derive.rs index 2bfa776d9fa9..e5848000b523 100644 --- a/crates/biome_deserialize_macros/src/deserializable_derive.rs +++ b/crates/biome_deserialize_macros/src/deserializable_derive.rs @@ -1,13 +1,13 @@ +mod container_attrs; mod enum_variant_attrs; -mod struct_attrs; mod struct_field_attrs; -use self::struct_attrs::StructAttrs; +use self::container_attrs::ContainerAttrs; use self::struct_field_attrs::DeprecatedField; use crate::deserializable_derive::enum_variant_attrs::EnumVariantAttrs; use crate::deserializable_derive::struct_field_attrs::StructFieldAttrs; use convert_case::{Case, Casing}; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use proc_macro_error::*; use quote::quote; use syn::{Data, GenericParam, Generics, Path, Type}; @@ -20,22 +20,41 @@ pub(crate) struct DeriveInput { impl DeriveInput { pub fn parse(input: syn::DeriveInput) -> Self { - let data = match input.data { - Data::Enum(data) => { - let data = data + let attrs = + ContainerAttrs::try_from(&input.attrs).expect("Could not parse field attributes"); + let data = if let ContainerAttrs { + with_validator, + from: Some(from), + .. + } = attrs + { + DeserializableData::From(DeserializableFromData { + from, + with_validator, + }) + } else if let ContainerAttrs { + with_validator, + try_from: Some(try_from), + .. + } = attrs + { + DeserializableData::TryFrom(DeserializableTryFromData { + try_from, + with_validator, + }) + } else { + match input.data { + Data::Enum(data) => { + let variants = data .variants .into_iter() - .filter(|variant| { - if variant.fields.is_empty() { - true - } else { + .map(|variant| { + if !variant.fields.is_empty() { abort!( variant.fields, "Deserializable derive cannot handle enum variants with fields -- you may need a custom Deserializable implementation" ) } - }) - .map(|variant| { let attrs = EnumVariantAttrs::try_from(&variant.attrs).expect("Could not parse enum variant attributes"); let ident = variant.ident; let key = attrs @@ -45,56 +64,59 @@ impl DeriveInput { DeserializableVariantData { ident, key } }) .collect(); - DeserializableData::Enum(data) - } - Data::Struct(data) => { - let attrs = - StructAttrs::try_from(&input.attrs).expect("Could not parse field attributes"); - if data.fields.iter().all(|field| field.ident.is_some()) { - let fields = data - .fields - .clone() - .into_iter() - .filter_map(|field| field.ident.map(|ident| (ident, field.attrs, field.ty))) - .map(|(ident, attrs, ty)| { - let attrs = StructFieldAttrs::try_from(&attrs) - .expect("Could not parse field attributes"); - let key = attrs - .rename - .unwrap_or_else(|| ident.to_string().to_case(Case::Camel)); - - DeserializableFieldData { - bail_on_error: attrs.bail_on_error, - deprecated: attrs.deprecated, - ident, - key, - passthrough_name: attrs.passthrough_name, - required: attrs.required, - ty, - validate: attrs.validate, - } - }) - .collect(); - - DeserializableData::Struct(DeserializableStructData { - fields, + DeserializableData::Enum(DeserializableEnumData { + variants, with_validator: attrs.with_validator, }) - } else if data.fields.len() == 1 { - DeserializableData::Newtype(DeserializableNewtypeData { - with_validator: attrs.with_validator, - }) - } else { - abort!( + } + Data::Struct(data) => { + if data.fields.iter().all(|field| field.ident.is_some()) { + let fields = data + .fields + .into_iter() + .filter_map(|field| { + field.ident.map(|ident| (ident, field.attrs, field.ty)) + }) + .map(|(ident, attrs, ty)| { + let attrs = StructFieldAttrs::try_from(&attrs) + .expect("Could not parse field attributes"); + let key = attrs + .rename + .unwrap_or_else(|| ident.to_string().to_case(Case::Camel)); + + DeserializableFieldData { + bail_on_error: attrs.bail_on_error, + deprecated: attrs.deprecated, + ident, + key, + passthrough_name: attrs.passthrough_name, + required: attrs.required, + ty, + validate: attrs.validate, + } + }) + .collect(); + + DeserializableData::Struct(DeserializableStructData { + fields, + with_validator: attrs.with_validator, + }) + } else if data.fields.len() == 1 { + DeserializableData::Newtype(DeserializableNewtypeData { + with_validator: attrs.with_validator, + }) + } else { + abort!( data.fields, "Deserializable derive requires structs to have named fields or a single unnamed one -- you may need a custom Deserializable implementation" ) + } } + _ => abort!( + input, + "Deserializable can only be derived for enums and structs" + ), } - _ => abort!( - input, - "Deserializable can only be derived for enums and structs" - ), }; Self { @@ -107,9 +129,17 @@ impl DeriveInput { #[derive(Debug)] pub enum DeserializableData { - Enum(Vec), + Enum(DeserializableEnumData), Newtype(DeserializableNewtypeData), Struct(DeserializableStructData), + From(DeserializableFromData), + TryFrom(DeserializableTryFromData), +} + +#[derive(Debug)] +pub struct DeserializableEnumData { + variants: Vec, + with_validator: bool, } #[derive(Debug)] @@ -123,6 +153,18 @@ pub struct DeserializableStructData { with_validator: bool, } +#[derive(Debug)] +pub struct DeserializableFromData { + pub from: Path, + pub with_validator: bool, +} + +#[derive(Debug)] +pub struct DeserializableTryFromData { + pub try_from: Path, + pub with_validator: bool, +} + #[derive(Clone, Debug)] pub struct DeserializableFieldData { bail_on_error: bool, @@ -143,8 +185,8 @@ pub struct DeserializableVariantData { pub(crate) fn generate_deserializable(input: DeriveInput) -> TokenStream { match input.data { - DeserializableData::Enum(variants) => { - generate_deserializable_enum(input.ident, input.generics, variants) + DeserializableData::Enum(data) => { + generate_deserializable_enum(input.ident, input.generics, data) } DeserializableData::Newtype(data) => { generate_deserializable_newtype(input.ident, input.generics, data) @@ -152,31 +194,49 @@ pub(crate) fn generate_deserializable(input: DeriveInput) -> TokenStream { DeserializableData::Struct(data) => { generate_deserializable_struct(input.ident, input.generics, data) } + DeserializableData::From(data) => { + generate_deserializable_from(input.ident, input.generics, data) + } + DeserializableData::TryFrom(data) => { + generate_deserializable_try_from(input.ident, input.generics, data) + } } } fn generate_deserializable_enum( ident: Ident, generics: Generics, - variants: Vec, + data: DeserializableEnumData, ) -> TokenStream { - let allowed_variants: Vec<_> = variants + let allowed_variants: Vec<_> = data + .variants .iter() .map(|DeserializableVariantData { key, .. }| quote! { #key }) .collect(); - let deserialize_variants: Vec<_> = variants + let deserialize_variants: Vec<_> = data + .variants .iter() .map( |DeserializableVariantData { ident: variant_ident, key, }| { - quote! { #key => Some(Self::#variant_ident) } + quote! { #key => Self::#variant_ident } }, ) .collect(); + let validator = if data.with_validator { + quote! { + if !biome_deserialize::DeserializableValidator::validate(&result, name, range, diagnostics) { + return None; + } + } + } else { + quote! {} + }; + let trait_bounds = generate_trait_bounds(&generics); quote! { @@ -186,7 +246,7 @@ fn generate_deserializable_enum( name: &str, diagnostics: &mut Vec, ) -> Option { - match biome_deserialize::Text::deserialize(value, name, diagnostics)?.text() { + let result = match biome_deserialize::Text::deserialize(value, name, diagnostics)?.text() { #(#deserialize_variants),*, unknown_variant => { const ALLOWED_VARIANTS: &[&str] = &[#(#allowed_variants),*]; @@ -195,9 +255,11 @@ fn generate_deserializable_enum( value.range(), ALLOWED_VARIANTS, )); - None + return None; } - } + }; + #validator + Some(result) } } } @@ -381,8 +443,6 @@ fn generate_deserializable_struct( validator }; - let visitor_ident = Ident::new(&format!("{ident}Visitor"), Span::call_site()); - quote! { impl #generics biome_deserialize::Deserializable for #ident #generics #trait_bounds { fn deserialize( @@ -390,43 +450,116 @@ fn generate_deserializable_struct( name: &str, diagnostics: &mut Vec, ) -> Option { - value.deserialize(#visitor_ident, name, diagnostics) + struct Visitor #generics; + impl #generics biome_deserialize::DeserializationVisitor for Visitor #generics { + type Output = #ident; + + const EXPECTED_TYPE: biome_deserialize::VisitableType = biome_deserialize::VisitableType::MAP; + + fn visit_map( + self, + members: impl Iterator>, + range: biome_deserialize::TextRange, + name: &str, + diagnostics: &mut Vec, + ) -> Option { + use biome_deserialize::{Deserializable, DeserializationDiagnostic, Text}; + let mut result: Self::Output = Self::Output::default(); + for (key, value) in members.flatten() { + let Some(key_text) = Text::deserialize(&key, "", diagnostics) else { + continue; + }; + match key_text.text() { + #(#deserialize_fields)* + unknown_key => { + const ALLOWED_KEYS: &[&str] = &[#(#allowed_keys),*]; + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + unknown_key, + key.range(), + ALLOWED_KEYS, + )) + } + } + } + #validator + Some(result) + } + } + + value.deserialize(Visitor, name, diagnostics) } } + } +} - struct #visitor_ident #generics; - impl #generics biome_deserialize::DeserializationVisitor for #visitor_ident #generics { - type Output = #ident; - - const EXPECTED_TYPE: biome_deserialize::VisitableType = biome_deserialize::VisitableType::MAP; +fn generate_deserializable_from( + ident: Ident, + generics: Generics, + data: DeserializableFromData, +) -> TokenStream { + let trait_bounds = generate_trait_bounds(&generics); + let from = data.from; + let validator = if data.with_validator { + quote! { + if !biome_deserialize::DeserializableValidator::validate(&result, name, range, diagnostics) { + return None; + } + } + } else { + quote! {} + }; + quote! { + impl #generics biome_deserialize::Deserializable for #ident #generics #trait_bounds { + fn deserialize( + value: &impl biome_deserialize::DeserializableValue, + name: &str, + diagnostics: &mut Vec, + ) -> Option { + let result: #from = biome_deserialize::Deserializable::deserialize(value, name, diagnostics)?; + let result: Self = result.into(); + #validator + Some(result) + } + } + } +} - fn visit_map( - self, - members: impl Iterator>, - range: biome_deserialize::TextRange, +fn generate_deserializable_try_from( + ident: Ident, + generics: Generics, + data: DeserializableTryFromData, +) -> TokenStream { + let trait_bounds = generate_trait_bounds(&generics); + let try_from = data.try_from; + let validator = if data.with_validator { + quote! { + if !biome_deserialize::DeserializableValidator::validate(&result, name, range, diagnostics) { + return None; + } + } + } else { + quote! {} + }; + quote! { + impl #generics biome_deserialize::Deserializable for #ident #generics #trait_bounds { + fn deserialize( + value: &impl biome_deserialize::DeserializableValue, name: &str, diagnostics: &mut Vec, - ) -> Option { - use biome_deserialize::{Deserializable, DeserializationDiagnostic, Text}; - let mut result: Self::Output = Self::Output::default(); - for (key, value) in members.flatten() { - let Some(key_text) = Text::deserialize(&key, "", diagnostics) else { - continue; - }; - match key_text.text() { - #(#deserialize_fields)* - unknown_key => { - const ALLOWED_KEYS: &[&str] = &[#(#allowed_keys),*]; - diagnostics.push(DeserializationDiagnostic::new_unknown_key( - unknown_key, - key.range(), - ALLOWED_KEYS, - )) - } + ) -> Option { + let result: #try_from = biome_deserialize::Deserializable::deserialize(value, name, diagnostics)?; + match result.try_into() { + Ok(result) => { + #validator + Some(result) + } + Err(err) => { + diagnostics.push(biome_deserialize::DeserializationDiagnostic::new( + format_args!("{}", err) + ).with_range(value.range())); + None } } - #validator - Some(result) } } } diff --git a/crates/biome_deserialize_macros/src/deserializable_derive/container_attrs.rs b/crates/biome_deserialize_macros/src/deserializable_derive/container_attrs.rs new file mode 100644 index 000000000000..d163db711f23 --- /dev/null +++ b/crates/biome_deserialize_macros/src/deserializable_derive/container_attrs.rs @@ -0,0 +1,84 @@ +use quote::ToTokens; +use syn::spanned::Spanned; +use syn::{Attribute, Error, Lit, Meta, MetaNameValue, Path}; + +use crate::util::parse_meta_list; + +/// Attributes for struct and enum. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ContainerAttrs { + pub with_validator: bool, + /// Deserialize the given `from` type, then convert to the annotated type + /// + /// See also: + pub from: Option, + /// Deserialize the given `try_from` type, then try converting to the annotated type + /// + /// See also: + pub try_from: Option, +} + +impl TryFrom<&Vec> for ContainerAttrs { + type Error = Error; + + fn try_from(attrs: &Vec) -> std::prelude::v1::Result { + let mut opts = Self::default(); + for attr in attrs { + if attr.path.is_ident("deserializable") { + parse_meta_list(&attr.parse_meta()?, |meta| { + match meta { + Meta::Path(path) if path.is_ident("with_validator") => { + opts.with_validator = true + } + Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(s), + .. + }) if path.is_ident("from") => opts.from = Some(s.parse()?), + Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(s), + .. + }) if path.is_ident("try_from") => opts.try_from = Some(s.parse()?), + _ => { + let meta_str = meta.to_token_stream().to_string(); + return Err(Error::new( + meta.span(), + format_args!("Unexpected attribute: {meta_str}"), + )); + } + } + if opts.from.is_some() && opts.try_from.is_some() { + return Err(Error::new( + meta.span(), + "You cannot specify both `from` and `try_from`", + )); + } + Ok(()) + })?; + } else if attr.path.is_ident("serde") { + parse_meta_list(&attr.parse_meta()?, |meta| { + if let Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(s), + .. + }) = meta + { + if opts.from.is_none() && path.is_ident("from") { + opts.from = Some(s.parse()?); + } else if opts.try_from.is_none() && path.is_ident("try_from") { + opts.try_from = Some(s.parse()?); + } else { + // Don't fail on unrecognized Serde attrs + } + } else { + // Don't fail on unrecognized Serde attrs + } + Ok(()) + }) + .ok(); + } + } + Ok(opts) + } +} diff --git a/crates/biome_deserialize_macros/src/deserializable_derive/enum_variant_attrs.rs b/crates/biome_deserialize_macros/src/deserializable_derive/enum_variant_attrs.rs index 29683b7df208..66e92f4471ef 100644 --- a/crates/biome_deserialize_macros/src/deserializable_derive/enum_variant_attrs.rs +++ b/crates/biome_deserialize_macros/src/deserializable_derive/enum_variant_attrs.rs @@ -30,7 +30,7 @@ impl TryFrom<&Vec> for EnumVariantAttrs { let val_str = val.to_token_stream().to_string(); return Err(Error::new( val.span(), - format!("Unexpected attribute: {val_str}"), + format_args!("Unexpected attribute: {val_str}"), )); } } diff --git a/crates/biome_deserialize_macros/src/deserializable_derive/struct_attrs.rs b/crates/biome_deserialize_macros/src/deserializable_derive/struct_attrs.rs deleted file mode 100644 index 429295a3cd9a..000000000000 --- a/crates/biome_deserialize_macros/src/deserializable_derive/struct_attrs.rs +++ /dev/null @@ -1,38 +0,0 @@ -use quote::ToTokens; -use syn::spanned::Spanned; -use syn::{Attribute, Error, Meta}; - -use crate::util::parse_meta_list; - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct StructAttrs { - pub with_validator: bool, -} - -impl TryFrom<&Vec> for StructAttrs { - type Error = Error; - - fn try_from(attrs: &Vec) -> std::prelude::v1::Result { - let mut opts = Self::default(); - for attr in attrs { - if attr.path.is_ident("deserializable") { - parse_meta_list(&attr.parse_meta()?, |meta| { - match meta { - Meta::Path(path) if path.is_ident("with_validator") => { - opts.with_validator = true - } - val => { - let val_str = val.to_token_stream().to_string(); - return Err(Error::new( - val.span(), - format!("Unexpected attribute: {val_str}"), - )); - } - } - Ok(()) - })?; - } - } - Ok(opts) - } -} diff --git a/crates/biome_deserialize_macros/src/deserializable_derive/struct_field_attrs.rs b/crates/biome_deserialize_macros/src/deserializable_derive/struct_field_attrs.rs index 05766ba86e64..38048c6793f2 100644 --- a/crates/biome_deserialize_macros/src/deserializable_derive/struct_field_attrs.rs +++ b/crates/biome_deserialize_macros/src/deserializable_derive/struct_field_attrs.rs @@ -66,7 +66,7 @@ impl TryFrom<&Vec> for StructFieldAttrs { let path_str = path.to_token_stream().to_string(); return Err(Error::new( path.span(), - format!("Unexpected attribute: {path_str}"), + format_args!("Unexpected attribute: {path_str}"), )); } } @@ -93,7 +93,7 @@ impl TryFrom<&Vec> for StructFieldAttrs { let meta_text = meta.to_token_stream().to_string(); return Err(Error::new( meta.span(), - format!("Unexpected attribute: {meta_text}"), + format_args!("Unexpected attribute: {meta_text}"), )); }; deprecated = if deprecated.is_some() { @@ -109,7 +109,7 @@ impl TryFrom<&Vec> for StructFieldAttrs { let path_text = path.to_token_stream().to_string(); return Err(Error::new( path.span(), - format!( + format_args!( "Unexpected attribute inside deprecated(): {path_text}" ), )); @@ -122,7 +122,7 @@ impl TryFrom<&Vec> for StructFieldAttrs { let meta_text = meta.to_token_stream().to_string(); return Err(Error::new( meta.span(), - format!("Unexpected attribute: {meta_text}"), + format_args!("Unexpected attribute: {meta_text}"), )); } } diff --git a/crates/biome_deserialize_macros/src/lib.rs b/crates/biome_deserialize_macros/src/lib.rs index 56bc22e99372..038a53e3e4b2 100644 --- a/crates/biome_deserialize_macros/src/lib.rs +++ b/crates/biome_deserialize_macros/src/lib.rs @@ -37,10 +37,10 @@ use syn::{parse_macro_input, DeriveInput}; /// } /// ``` /// -/// ## Struct attributes +/// ## Container attributes /// -/// When [Deserializable] is derived on a struct, its behavior may be adjusted -/// through attributes. +/// When [Deserializable] is derived on a struct or an enum, +/// its behavior may be adjusted through attributes. /// /// ### `with_validator` /// @@ -79,6 +79,79 @@ use syn::{parse_macro_input, DeriveInput}; /// } /// ``` /// +/// ### `from` +/// +/// This attribute allows deserializing a type, +/// and then converting it to the current annotated type. +/// The annotated type must implement the standard [From] trait. +/// +/// For structs and enums that also implement Serde's [serde::Deserialize], +/// it automatically picks up on Serde's +/// [`from` attribute](https://serde.rs/container-attrs.html#from). +/// `deserializable(from = _)` takes precdence over `serde(from = _)`. +/// +/// ```no_test +/// #[derive(Default, Deserializable)] +/// #[deserializable(from = Person)] +/// struct Contact { +/// fullname: String, +/// } +/// +/// #[derive(Default, Deserializable)] +/// struct Person { +/// firstnames: String, +/// lastname: String, +/// } +/// +/// impl From for Contact { +/// fn from(value: Person) -> Contact { +/// Contact { +/// fullname: format!("{} {}", value.firstnames, value.lastname), +/// } +/// } +/// } +/// ``` +/// +/// ### `try_from` +/// +/// This attribute allows deserializing a type, +/// and then attempting to convert it to the current annotated type. +/// The annotated type must implement the standard [TryFrom] trait. +/// +/// For structs and enums that also implement Serde's [serde::Deserialize], +/// it automatically picks up on Serde's +/// [`try_from` attribute](https://serde.rs/container-attrs.html#try_from). +/// `deserializable(try_from = _)` takes precdence over `serde(try_from = _)`. +/// +/// ```no_test +/// #[derive(Default, Deserializable)] +/// #[deserializable(try_from = Contact)] +/// struct Person { +/// firstnames: String, +/// lastname: String, +/// } +/// +/// #[derive(Default, Deserializable)] +/// struct Contact { +/// fullname: String, +/// } +/// +/// impl TryFrom for Person { +/// Error = &'static str; +/// +/// fn from(value: Contact) -> Person { +/// let names: Vec<&str> = value.fullname.splitn(' ', 2).collect(); +/// if names.len() < 2 { +/// return Err("At least two names separated by a whitespace are required.") +/// } +/// Person { +/// firstnames: names[..names.len()-1].join(' ').to_string(), +/// lastname: names[names.len()-1].to_string(), +/// } +/// } +/// } +/// ``` +/// /// ## Struct field attributes /// /// A struct's fields may also be adjusted through attributes. @@ -86,7 +159,7 @@ use syn::{parse_macro_input, DeriveInput}; /// ### `bail_on_error` /// /// If present, bails on deserializing the entire struct if validation for this -/// this field fails. +/// field fails. /// /// Note the struct may still be deserialized if the field is not present in the /// serialized representation at all. In that case `Default::default()` will be @@ -157,8 +230,10 @@ use syn::{parse_macro_input, DeriveInput}; /// } /// ``` /// -/// For structs that also implement Serde's `Serialize` or `Deserialize`, it -/// automatically picks up on Serde's `rename` attribute: +/// For structs that also implement Serde's `Serialize` or `Deserialize`, +/// it automatically picks up on Serde's +/// [`rename` attribute](https://serde.rs/field-attrs.html#rename). +/// `deserializable(rename = _)` takes precdence over `serde(rename = _)`. /// /// ```no_test /// #[derive(Default, Deserialize, Deserializable, Serialize)] @@ -226,7 +301,8 @@ use syn::{parse_macro_input, DeriveInput}; /// } /// ``` /// -/// Using Serde's attributes is supported on enums too. +/// Using Serde's [rename attribute](https://serde.rs/variant-attrs.html#rename) +/// is supported on enums too. #[proc_macro_derive(Deserializable, attributes(deserializable))] #[proc_macro_error] pub fn derive_deserializable(input: TokenStream) -> TokenStream {