From 784605c4185dd0e43c68258e028d63053aec6587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Fri, 8 Nov 2024 22:05:36 +0100 Subject: [PATCH] Change "Tag" to a derive macro --- aws_macros/Cargo.toml | 1 + aws_macros/src/lib.rs | 7 ++- aws_macros/src/tag.rs | 123 +++++++++++++++++++++++------------------- src/lib.rs | 8 +-- src/tags/README.md | 8 +-- src/tags/mod.rs | 16 +++--- 6 files changed, 87 insertions(+), 76 deletions(-) diff --git a/aws_macros/Cargo.toml b/aws_macros/Cargo.toml index 830bc98..fff0f81 100644 --- a/aws_macros/Cargo.toml +++ b/aws_macros/Cargo.toml @@ -29,6 +29,7 @@ syn = { version = "2.*", default-features = false, features = [ "parsing", "printing", "clone-impls", + "derive", ] } [lints] diff --git a/aws_macros/src/lib.rs b/aws_macros/src/lib.rs index 2ffeed5..60b4387 100644 --- a/aws_macros/src/lib.rs +++ b/aws_macros/src/lib.rs @@ -15,8 +15,7 @@ pub fn Tags(attr: TokenStream, item: TokenStream) -> TokenStream { tags::transform(attr, item) } -#[proc_macro_attribute] -#[expect(non_snake_case, reason = "attribute proc macros should be capitalized")] -pub fn Tag(attr: TokenStream, item: TokenStream) -> TokenStream { - tag::transform(attr, item) +#[proc_macro_derive(Tag, attributes(tag))] +pub fn tag(input: TokenStream) -> TokenStream { + tag::transform(input) } diff --git a/aws_macros/src/tag.rs b/aws_macros/src/tag.rs index 0452d81..2b05dc6 100644 --- a/aws_macros/src/tag.rs +++ b/aws_macros/src/tag.rs @@ -18,19 +18,18 @@ enum Translator { Transparent(TransparentKind), } -fn parse_enum_attributes(attrs: &mut Vec) -> Option { +fn parse_enum_attributes(attrs: &[syn::Attribute]) -> Option { let index_of_tag_attribute = attrs .iter() - .enumerate() - .filter(|&(_i, attr)| attr.style == syn::AttrStyle::Outer) - .find_map(|(i, attr)| match attr.meta { + .filter(|attr| attr.style == syn::AttrStyle::Outer) + .find_map(|attr| match attr.meta { syn::Meta::List(ref meta_list) => { if let (Some(name), 1) = ( meta_list.path.segments.first(), meta_list.path.segments.len(), ) { if name.ident == "tag" { - Some((i, meta_list.clone())) + Some(meta_list.clone()) } else { None } @@ -42,11 +41,7 @@ fn parse_enum_attributes(attrs: &mut Vec) -> Option }); match index_of_tag_attribute { - Some((i, meta_list)) => { - // `i` came from `enumerate()` and is guaranteed to be in bounds - let removed_attribute = attrs.remove(i); - drop(removed_attribute); - + Some(meta_list) => { let expr: syn::Expr = syn::parse(meta_list.tokens.into()).expect("expected expr in macro attribute"); @@ -79,10 +74,10 @@ fn parse_enum_attributes(attrs: &mut Vec) -> Option } } -fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) { +fn parse_transparent_enum(e: &syn::DataEnum) -> Translator { let variants = e .variants - .iter_mut() + .iter() .map(|variant| { assert!( variant.discriminant.is_none(), @@ -92,25 +87,16 @@ fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) { syn::Fields::Unit => (), _ => panic!("enum cannot have fields in variants"), } - let rename = parse_enum_attributes(&mut variant.attrs); + let rename = parse_enum_attributes(&variant.attrs); (variant.ident.clone(), rename) }) .collect::)>>(); - ( - Translator::Transparent(TransparentKind::SimpleEnum { variants }), - e.into(), - ) + Translator::Transparent(TransparentKind::SimpleEnum { variants }) } -pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { - let root = quote! {::aws_lib}; - - let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute"); - - let elem = syn::parse_macro_input!(item as syn::Item); - +fn parse_tag_attribute(expr: syn::Expr, elem: &syn::Data) -> Translator { let syn::Expr::Assign(assign) = expr else { panic!("invalid expression in macro attribute") }; @@ -127,7 +113,7 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { _ => panic!("invalid expression in tag field attribute, left side"), } - let (translator, elem) = match *assign.right { + match *assign.right { syn::Expr::Path(ref exprpath) => { let segments = &exprpath.path.segments; let (Some(segment), 1) = (segments.first(), segments.len()) else { @@ -135,45 +121,71 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { }; match segment.ident.to_string().as_str() { - "serde" => (Translator::Serde, elem), - "manual" => (Translator::Manual, elem), - "transparent" => match elem { - syn::Item::Struct(ref s) => match s.fields { - syn::Fields::Unnamed(ref fields) => { - let (Some(field), 1) = (fields.unnamed.first(), fields.unnamed.len()) - else { - panic!( - "transparent translation is only available for newtype-style macros" - ) - }; - ( + "serde" => Translator::Serde, + "manual" => Translator::Manual, + "transparent" => + { + #[expect( + clippy::match_wildcard_for_single_variants, + reason = "just by chance is there only one additional variant" + )] + match *elem { + syn::Data::Struct(ref s) => match s.fields { + syn::Fields::Unnamed(ref fields) => { + let (Some(field), 1) = + (fields.unnamed.first(), fields.unnamed.len()) + else { + panic!( + "transparent translation is only available for newtype-style macros" + ) + }; Translator::Transparent(TransparentKind::NewtypeStruct { ty: field.ty.clone(), - }), - elem, - ) + }) + } + _ => panic!( + "transparent translation is only available for newtype-style macros" + ), + }, + syn::Data::Enum(ref e) => parse_transparent_enum(e), + _ => { + panic!("transparent translation is only available for newtype-style macros") } - _ => panic!( - "transparent translation is only available for newtype-style macros" - ), - }, - - syn::Item::Enum(e) => parse_transparent_enum(e), - _ => { - panic!("transparent translation is only available for newtype-style macros") } - }, + } t => panic!("invalid translator {t}"), } } _ => panic!("invalid expression in tag field attribute, left side"), - }; + } +} - let name = match elem { - syn::Item::Struct(ref s) => &s.ident, - syn::Item::Enum(ref e) => &e.ident, - _ => panic!("only applicable to structs/enums"), - }; +pub(crate) fn transform(input: TokenStream) -> TokenStream { + let root = quote! {::aws_lib}; + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + + let expr = input + .attrs + .into_iter() + .find_map(|attr| match attr.meta { + syn::Meta::List(meta_list) => { + if meta_list.path.is_ident("tag") { + Some( + syn::parse2::(meta_list.tokens) + .expect("invalid expression in tag attribute"), + ) + } else { + None + } + } + _ => None, + }) + .expect("Tag derive macro requires a tag attribute"); + + let translator = parse_tag_attribute(expr, &input.data); + + let name = input.ident; let translator = match translator { Translator::Serde => quote! { @@ -271,7 +283,6 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { }; quote! { - #elem #translator } .into() diff --git a/src/lib.rs b/src/lib.rs index 3426ace..082e599 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -337,9 +337,9 @@ impl fmt::Display for SubnetId { macro_rules! string_newtype { ($name:ident) => { - #[Tag(translate = transparent)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[derive(Debug, Clone, Eq, PartialEq)] + #[derive(Debug, Clone, Eq, PartialEq, Tag)] + #[tag(translate = transparent)] pub struct $name(String); impl std::fmt::Display for $name { @@ -422,9 +422,9 @@ impl TryFrom for Ami { } } -#[Tag(translate = manual)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] +#[derive(Tag, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] +#[tag(translate = manual)] pub struct Timestamp(DateTime); impl Timestamp { diff --git a/src/tags/README.md b/src/tags/README.md index 188f896..b9f018c 100644 --- a/src/tags/README.md +++ b/src/tags/README.md @@ -51,14 +51,14 @@ strategies: - `impl From for RawTagValue`: How to turn `T` back into the value of a tag. This cannot fail. -There is a [`macro@Tag`] macro that selects the strategy, which can then be used -in a struct that is using `#[Tags]`: +There is a [`macro@Tag`] derive macro that selects the strategy, which can then +be used in a struct that is using `#[Tags]`: ```rust use aws_lib::tags::{Tag, Tags}; -#[Tag(translate = transparent)] -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Tag, Debug, Clone, PartialEq, Eq)] +#[tag(translate = transparent)] struct MyTag(String); #[Tags] diff --git a/src/tags/mod.rs b/src/tags/mod.rs index a6f558d..0169d81 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -309,16 +309,16 @@ mod tests { use super::*; - #[Tag(translate = manual)] - #[derive(Debug, Clone, PartialEq, Eq)] + #[derive(Tag, Debug, Clone, PartialEq, Eq)] + #[tag(translate = manual)] enum MyTag { A, B, } #[cfg(feature = "serde-tags")] - #[Tag(translate = serde)] - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[derive(Tag, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[tag(translate = serde)] struct MyStructTag { foo: String, bar: bool, @@ -426,8 +426,8 @@ mod tests { #[test] fn test_transparent_tag() { - #[Tag(translate = transparent)] - #[derive(PartialEq, Debug)] + #[derive(Tag, PartialEq, Debug)] + #[tag(translate = transparent)] struct MyTag(String); assert_eq!( @@ -442,8 +442,8 @@ mod tests { #[test] fn test_enums() { - #[Tag(translate = transparent)] - #[derive(PartialEq, Debug)] + #[derive(PartialEq, Debug, Tag)] + #[tag(translate = transparent)] enum MyCoolioTag { A, #[tag(rename = "C")]