From b6209baecbc735ce61ee35314866303e20e74bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 5 Nov 2024 20:42:59 +0100 Subject: [PATCH] Implement transparent translation for newtypes and enums --- Cargo.toml | 3 +- aws_macros/src/tag.rs | 209 +++++++++++++++++++++++++++++++++++++++++- src/tags/mod.rs | 45 +++++++++ 3 files changed, 251 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 33d5192..e40ba4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ match_bool = "allow" new_without_default = "allow" missing_panics_doc = "warn" multiple_crate_versions = "allow" +map_unwrap_or = "allow" pedantic = { level = "deny", priority = -1 } nursery = { level = "deny", priority = -1 } @@ -142,7 +143,7 @@ float_cmp_const = "deny" fn_to_numeric_cast_any = "deny" format_push_string = "deny" get_unwrap = "deny" -if_then_some_else_none = "deny" +if_then_some_else_none = "allow" impl_trait_in_params = "allow" indexing_slicing = "deny" infinite_loop = "deny" diff --git a/aws_macros/src/tag.rs b/aws_macros/src/tag.rs index bbc8334..5b1e679 100644 --- a/aws_macros/src/tag.rs +++ b/aws_macros/src/tag.rs @@ -1,10 +1,107 @@ use proc_macro::TokenStream; use quote::quote; +#[derive(Debug)] +enum TransparentKind { + NewtypeStruct { + ty: syn::Type, + }, + SimpleEnum { + variants: Vec<(syn::Ident, Option)>, + }, +} + #[derive(Debug)] enum Translator { Serde, Manual, + Transparent(TransparentKind), +} + +fn parse_enum_attributes(attrs: &mut Vec) -> Option { + let index_of_tag_attribute = attrs + .iter() + .enumerate() + .filter(|&(_i, attr)| attr.style == syn::AttrStyle::Outer) + .find_map(|(i, 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())) + } else { + None + } + } else { + None + } + } + _ => None, + }); + + 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); + + let expr: syn::Expr = + syn::parse(meta_list.tokens.into()).expect("expected expr in macro attribute"); + + match expr { + syn::Expr::Assign(ref assign) => { + match *assign.left { + syn::Expr::Path(ref exprpath) => { + let segments = &exprpath.path.segments; + let (Some(segment), 1) = (segments.first(), segments.len()) else { + panic!("invalid attribute key") + }; + + assert!(segment.ident == "rename", "invalid attribute key"); + } + _ => panic!("invalid expression in enum variant attribute, left side"), + } + + match *assign.right { + syn::Expr::Lit(ref expr_lit) => match expr_lit.lit { + syn::Lit::Str(ref lit_str) => Some(lit_str.clone()), + _ => panic!("invalid literal in enum variant attribute"), + }, + _ => panic!("invalid expression in enum variant attribute, right side"), + } + } + _ => panic!("invalid expression in enum variant attribute"), + } + } + None => None, + } +} + +fn parse_transparent_enum(mut e: syn::ItemEnum) -> (Translator, syn::Item) { + let variants = e + .variants + .iter_mut() + .map(|variant| { + assert!( + variant.discriminant.is_none(), + "variant cannot have an explicit discriminant" + ); + match variant.fields { + syn::Fields::Unit => (), + _ => panic!("enum cannot have fields in variants"), + } + let rename = parse_enum_attributes(&mut variant.attrs); + + (variant.ident.clone(), rename) + }) + .collect::)>>(); + + ( + Translator::Transparent(TransparentKind::SimpleEnum { variants }), + e.into(), + ) } pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { @@ -12,6 +109,8 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute"); + let elem = syn::parse_macro_input!(item as syn::Item); + let syn::Expr::Assign(assign) = expr else { panic!("invalid expression in macro attribute") }; @@ -28,7 +127,7 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { _ => panic!("invalid expression in tag field attribute, left side"), } - let translator = match *assign.right { + let (translator, elem) = match *assign.right { syn::Expr::Path(ref exprpath) => { let segments = &exprpath.path.segments; let (Some(segment), 1) = (segments.first(), segments.len()) else { @@ -36,16 +135,40 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { }; match segment.ident.to_string().as_str() { - "serde" => Translator::Serde, - "manual" => Translator::Manual, + "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" + ) + }; + ( + Translator::Transparent(TransparentKind::NewtypeStruct { + ty: field.ty.clone(), + }), + elem, + ) + } + _ => 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 elem = syn::parse_macro_input!(item as syn::Item); - let name = match elem { syn::Item::Struct(ref s) => &s.ident, syn::Item::Enum(ref e) => &e.ident, @@ -69,6 +192,82 @@ pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream { type Translator = #root::tags::TranslateManual; } }, + Translator::Transparent(kind) => match kind { + TransparentKind::NewtypeStruct { ty } => quote! { + impl #root::tags::TranslatableManual for #name {} + + impl #root::tags::TagValue<#name> for #name { + type Error = #root::tags::ParseTagValueError; + type Translator = #root::tags::TranslateManual; + } + + impl TryFrom<#root::tags::RawTagValue> for #name { + type Error = #root::tags::ParseTagValueError; + + fn try_from(value: #root::tags::RawTagValue) -> Result { + Ok(Self(<#ty as #root::tags::TagValue<#ty>>::from_raw_tag(value)?)) + } + } + + impl From<#name> for RawTagValue { + fn from(value: #name) -> Self { + <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(value.0) + } + } + }, + + TransparentKind::SimpleEnum { variants } => { + let (into_raw_tag_mapping, from_raw_tag_mapping): (Vec<_>, Vec<_>) = variants + .into_iter() + .map(|(variant, rename)| { + let lit = rename + .map(|r| r.value()) + .unwrap_or_else(|| variant.to_string()); + ( + quote! { + #name::#variant => #root::tags::RawTagValue::new(#lit.to_owned()), + }, + quote! { + #lit => Self::#variant, + }, + ) + }) + .unzip(); + + quote! { + impl #root::tags::TranslatableManual for #name {} + + impl #root::tags::TagValue<#name> for #name { + type Error = #root::tags::ParseTagValueError; + type Translator = #root::tags::TranslateManual; + } + + impl From<#name> for #root::tags::RawTagValue { + fn from(value: #name) -> Self { + match value { + #(#into_raw_tag_mapping) + * + } + } + } + + impl TryFrom<#root::tags::RawTagValue> for #name { + type Error = #root::tags::ParseTagValueError; + + fn try_from(value: #root::tags::RawTagValue) -> Result { + Ok(match value.as_str() { + #(#from_raw_tag_mapping) + * + _ => return Err(#root::tags::ParseTagValueError::InvalidValue { + value, + message: "invalid enum value".to_owned(), + }), + }) + } + } + } + } + }, }; quote! { diff --git a/src/tags/mod.rs b/src/tags/mod.rs index eadede8..f6ce63f 100644 --- a/src/tags/mod.rs +++ b/src/tags/mod.rs @@ -403,4 +403,49 @@ mod tests { ]) ); } + + #[test] + fn test_transparent_tag() { + #[Tag(translate = transparent)] + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyTag(String); + + assert_eq!( + MyTag::into_raw_tag(MyTag("test".to_owned())), + RawTagValue::new("test".to_owned()) + ); + assert_eq!( + MyTag::from_raw_tag(RawTagValue::new("test".to_owned())).unwrap(), + MyTag("test".to_owned()) + ); + } + + #[test] + fn test_enums() { + #[Tag(translate = transparent)] + #[derive(PartialEq, Debug)] + enum MyCoolioTag { + A, + #[tag(rename = "C")] + B, + } + + assert_eq!( + MyCoolioTag::into_raw_tag(MyCoolioTag::A), + RawTagValue::new("A".to_owned()) + ); + assert_eq!( + MyCoolioTag::from_raw_tag(RawTagValue::new("A".to_owned())).unwrap(), + MyCoolioTag::A + ); + + assert_eq!( + MyCoolioTag::into_raw_tag(MyCoolioTag::B), + RawTagValue::new("C".to_owned()) + ); + assert_eq!( + MyCoolioTag::from_raw_tag(RawTagValue::new("C".to_owned())).unwrap(), + MyCoolioTag::B + ); + } }