Skip to content

Commit

Permalink
Start work on an apply attribute #85
Browse files Browse the repository at this point in the history
This `apply` proc-macro takes a list of type patterns and a list of
attributes for each. On each field where the type matches, the list of
attributes will be added to the existing field attributes.
  • Loading branch information
jonasbb committed Nov 6, 2022
1 parent dc6fea6 commit 5b87563
Show file tree
Hide file tree
Showing 4 changed files with 579 additions and 12 deletions.
1 change: 1 addition & 0 deletions serde_with_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ features = [
version = "1.0.3"

[dev-dependencies]
expect-test = "1.4.0"
pretty_assertions = "1.0.0"
rustversion = "1.0.0"
serde = {version = "1.0.75", features = ["derive"]}
Expand Down
304 changes: 304 additions & 0 deletions serde_with_macros/src/apply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
use darling::{Error as DarlingError, FromMeta};
use proc_macro::TokenStream;
use quote::ToTokens as _;
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
Attribute, Error, Field, NestedMeta, Path, Token, Type, TypeArray, TypeGroup, TypeParen,
TypePath, TypePtr, TypeReference, TypeSlice, TypeTuple,
};

/// Parsed form of a single rule in the `#[apply(...)]` attribute.
///
/// This parses tokens in the shape of `Type => Attribute`.
/// For example, `Option<String> => #[serde(default)]`.
struct AddAttributesRule {
/// A type pattern determining the fields to which the attributes are applied.
ty: Type,
/// The attributes to apply.
///
/// All attributes are appended to the list of existing field attributes.
attrs: Vec<Attribute>,
}

impl Parse for AddAttributesRule {
fn parse(input: ParseStream<'_>) -> Result<Self, Error> {
let ty: Type = input.parse()?;
input.parse::<Token![=>]>()?;
let attr = Attribute::parse_outer(input)?;
Ok(AddAttributesRule { ty, attrs: attr })
}
}

/// Parsed form of the `#[apply(...)]` attribute.
///
/// The `apply` attribute takes a comma separated list of rules in the shape of `Type => Attribute`.
/// Each rule is stored as a [`AddAttributesRule`].
struct ApplyInput {
metas: Vec<NestedMeta>,
rules: Punctuated<AddAttributesRule, Token![,]>,
}

impl Parse for ApplyInput {
fn parse(input: ParseStream<'_>) -> Result<Self, Error> {
let mut metas: Vec<NestedMeta> = Vec::new();

while input.peek2(Token![=]) && !input.peek2(Token![=>]) {
let value = NestedMeta::parse(input)?;
metas.push(value);
if !input.peek(Token![,]) {
break;
}
input.parse::<Token![,]>()?;
}

let rules: Punctuated<AddAttributesRule, Token![,]> =
input.parse_terminated(AddAttributesRule::parse)?;
Ok(Self { metas, rules })
}
}

pub fn apply(args: TokenStream, input: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(args as ApplyInput);

#[derive(FromMeta)]
struct SerdeContainerOptions {
#[darling(rename = "crate")]
alt_crate_path: Option<Path>,
}

let container_options = match SerdeContainerOptions::from_list(&args.metas) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
}
};
let serde_with_crate_path = container_options
.alt_crate_path
.unwrap_or_else(|| syn::parse_quote!(::serde_with));

let res = match super::apply_function_to_struct_and_enum_fields_darling(
input,
&serde_with_crate_path,
&prepare_apply_attribute_to_field(args),
) {
Ok(res) => res,
Err(err) => err.write_errors(),
};
TokenStream::from(res)
}

/// Create a function compatible with [`super::apply_function_to_struct_and_enum_fields`] based on [`Input`].
///
/// A single [`Input`] can apply to multiple field types.
/// To account for this a new function must be created to stay compatible with the function signature or [`super::apply_function_to_struct_and_enum_fields`].
fn prepare_apply_attribute_to_field(
input: ApplyInput,
) -> impl Fn(&mut Field) -> Result<(), DarlingError> {
move |field: &mut Field| {
let has_skip_attr = super::field_has_attribute(field, "serde_with", "skip_apply");
if has_skip_attr {
return Ok(());
}

for matcher in input.rules.iter() {
if ty_pattern_matches_ty(&matcher.ty, &field.ty) {
field.attrs.extend(matcher.attrs.clone());
}
}
Ok(())
}
}

fn ty_pattern_matches_ty(ty_pattern: &Type, ty: &Type) -> bool {
match (ty_pattern, ty) {
(
Type::Array(TypeArray {
elem: ty_pattern,
len: len_pattern,
..
}),
Type::Array(TypeArray { elem: ty, len, .. }),
) => {
let ty_match = ty_pattern_matches_ty(ty_pattern, ty);
dbg!(len_pattern);
let len_match = len_pattern == len || len_pattern.to_token_stream().to_string() == "_";
ty_match && len_match
}
(Type::BareFn(ty_pattern), Type::BareFn(ty)) => ty_pattern == ty,
(
Type::Group(TypeGroup {
elem: ty_pattern, ..
}),
Type::Group(TypeGroup { elem: ty, .. }),
) => ty_pattern_matches_ty(ty_pattern, ty),
(Type::ImplTrait(ty_pattern), Type::ImplTrait(ty)) => ty_pattern == ty,
(Type::Infer(_), _) => true,
(Type::Macro(ty_pattern), Type::Macro(ty)) => ty_pattern == ty,
(Type::Never(_), Type::Never(_)) => true,
(
Type::Paren(TypeParen {
elem: ty_pattern, ..
}),
Type::Paren(TypeParen { elem: ty, .. }),
) => ty_pattern_matches_ty(ty_pattern, ty),
(
Type::Path(TypePath {
qself: qself_pattern,
path: path_pattern,
}),
Type::Path(TypePath { qself, path }),
) => {
/// Compare two paths for relaxed equality.
///
/// Two paths match if they are equal except for the path arguments.
/// Path arguments are generics on types or functions.
/// If the pattern has no argument, it can match with everthing.
/// If the pattern does have an argument, the other side must be equal.
fn path_pattern_matches_path(path_pattern: &Path, path: &Path) -> bool {
if path_pattern.leading_colon != path.leading_colon
|| path_pattern.segments.len() != path.segments.len()
{
return false;
}
// Boths parts are equal length
std::iter::zip(&path_pattern.segments, &path.segments).all(
|(path_pattern_segment, path_segment)| {
let ident_equal = path_pattern_segment.ident == path_segment.ident;
let args_match =
match (&path_pattern_segment.arguments, &path_segment.arguments) {
(syn::PathArguments::None, _) => true,
(
syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments {
args: args_pattern,
..
},
),
syn::PathArguments::AngleBracketed(
syn::AngleBracketedGenericArguments { args, .. },
),
) => {
args_pattern.len() == args.len()
&& std::iter::zip(args_pattern, args).all(|(a, b)| {
match (a, b) {
(
syn::GenericArgument::Type(ty_pattern),
syn::GenericArgument::Type(ty),
) => ty_pattern_matches_ty(ty_pattern, ty),
(a, b) => a == b,
}
})
}
(args_pattern, args) => args_pattern == args,
};
ident_equal && args_match
},
)
}
qself_pattern == qself && path_pattern_matches_path(path_pattern, path)
}
(
Type::Ptr(TypePtr {
const_token: const_token_pattern,
mutability: mutability_pattern,
elem: ty_pattern,
..
}),
Type::Ptr(TypePtr {
const_token,
mutability,
elem: ty,
..
}),
) => {
const_token_pattern == const_token
&& mutability_pattern == mutability
&& ty_pattern_matches_ty(ty_pattern, ty)
}
(
Type::Reference(TypeReference {
lifetime: lifetime_pattern,
elem: ty_pattern,
..
}),
Type::Reference(TypeReference {
lifetime, elem: ty, ..
}),
) => {
(lifetime_pattern.is_none() || lifetime_pattern == lifetime)
&& ty_pattern_matches_ty(ty_pattern, ty)
}
(
Type::Slice(TypeSlice {
elem: ty_pattern, ..
}),
Type::Slice(TypeSlice { elem: ty, .. }),
) => ty_pattern_matches_ty(ty_pattern, ty),
(Type::TraitObject(ty_pattern), Type::TraitObject(ty)) => ty_pattern == ty,
(
Type::Tuple(TypeTuple {
elems: ty_pattern, ..
}),
Type::Tuple(TypeTuple { elems: ty, .. }),
) => {
ty_pattern.len() == ty.len()
&& std::iter::zip(ty_pattern, ty)
.all(|(ty_pattern, ty)| ty_pattern_matches_ty(ty_pattern, ty))
}
(Type::Verbatim(_), Type::Verbatim(_)) => false,
_ => false,
}
}

#[cfg(test)]
mod test {
use super::*;

#[track_caller]
fn matches(ty_pattern: &str, ty: &str) -> bool {
let ty_pattern = syn::parse_str(ty_pattern).unwrap();
let ty = syn::parse_str(ty).unwrap();
ty_pattern_matches_ty(&ty_pattern, &ty)
}

#[test]
fn test_ty_generic() {
assert!(matches("Option<u8>", "Option<u8>"));
assert!(matches("Option", "Option<u8>"));
assert!(!matches("Option<u8>", "Option<String>"));

assert!(matches("BTreeMap<u8, u8>", "BTreeMap<u8, u8>"));
assert!(matches("BTreeMap", "BTreeMap<u8, u8>"));
assert!(!matches("BTreeMap<String, String>", "BTreeMap<u8, u8>"));
assert!(matches("BTreeMap<_, _>", "BTreeMap<u8, u8>"));
assert!(matches("BTreeMap<_, u8>", "BTreeMap<u8, u8>"));
assert!(!matches("BTreeMap<String, _>", "BTreeMap<u8, u8>"));
}

#[test]
fn test_array() {
assert!(matches("[u8; 1]", "[u8; 1]"));
assert!(matches("[_; 1]", "[u8; 1]"));
assert!(matches("[u8; _]", "[u8; 1]"));

assert!(!matches("[u8; 1]", "[u8; 2]"));
assert!(!matches("[u8; 1]", "[u8; _]"));
assert!(!matches("[u8; 1]", "[String; 1]"));
}

#[test]
fn test_reference() {
assert!(matches("&str", "&str"));
assert!(matches("&mut str", "&str"));
assert!(matches("&str", "&mut str"));
assert!(matches("&str", "&'a str"));
assert!(matches("&str", "&'static str"));
assert!(matches("&str", "&'static mut str"));

assert!(matches("&'a str", "&'a str"));
assert!(matches("&'a mut str", "&'a str"));

assert!(!matches("&'b str", "&'a str"));
}
}
39 changes: 27 additions & 12 deletions serde_with_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#[allow(unused_extern_crates)]
extern crate proc_macro;

mod apply;
mod utils;

use crate::utils::{split_with_de_lifetime, DeriveOptions, IteratorExt as _};
Expand Down Expand Up @@ -422,12 +423,22 @@ fn field_has_attribute(field: &Field, namespace: &str, name: &str) -> bool {
// Ignore non parsable attributes, as these are not important for us
if let Ok(Meta::List(expr)) = attr.parse_meta() {
for expr in expr.nested {
if let NestedMeta::Meta(Meta::NameValue(expr)) = expr {
if let Some(ident) = expr.path.get_ident() {
if *ident == name {
return true;
match expr {
NestedMeta::Meta(Meta::NameValue(expr)) => {
if let Some(ident) = expr.path.get_ident() {
if *ident == name {
return true;
}
}
}
NestedMeta::Meta(Meta::Path(expr)) => {
if let Some(ident) = expr.get_ident() {
if *ident == name {
return true;
}
}
}
_ => (),
}
}
}
Expand Down Expand Up @@ -558,7 +569,7 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {
#[derive(FromMeta)]
struct SerdeContainerOptions {
#[darling(rename = "crate")]
alt_crate_path: Option<String>,
alt_crate_path: Option<Path>,
}

let args: AttributeArgs = parse_macro_input!(args);
Expand All @@ -571,12 +582,7 @@ pub fn serde_as(args: TokenStream, input: TokenStream) -> TokenStream {

let serde_with_crate_path = container_options
.alt_crate_path
.as_deref()
.unwrap_or("::serde_with");
let serde_with_crate_path = match syn::parse_str(serde_with_crate_path) {
Ok(path) => path,
Err(err) => return TokenStream::from(DarlingError::from(err).write_errors()),
};
.unwrap_or_else(|| syn::parse_quote!(::serde_with));

// Convert any error message into a nice compiler error
let res = match apply_function_to_struct_and_enum_fields_darling(
Expand Down Expand Up @@ -1099,7 +1105,16 @@ fn serialize_display(mut input: DeriveInput, serde_with_crate_path: Path) -> Tok
/// Otherwise, downstream proc-macros would need to be placed *in front of* the main `#[serde_as]` attribute, since otherwise the field attributes would already be stripped off.
///
/// More details about the use-cases in the GitHub discussion: <https://github.com/jonasbb/serde_with/discussions/260>.
#[proc_macro_derive(__private_consume_serde_as_attributes, attributes(serde_as))]
#[proc_macro_derive(
__private_consume_serde_as_attributes,
attributes(serde_as, serde_with)
)]
pub fn __private_consume_serde_as_attributes(_: TokenStream) -> TokenStream {
TokenStream::new()
}

/// TODO
#[proc_macro_attribute]
pub fn apply(args: TokenStream, input: TokenStream) -> TokenStream {
apply::apply(args, input)
}

0 comments on commit 5b87563

Please sign in to comment.