Initial commit

This commit is contained in:
2024-11-02 13:37:44 +01:00
commit 8880918f1a
19 changed files with 4973 additions and 0 deletions

35
aws_macros/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
[package]
name = "aws-macros"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
description.workspace = true
license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
readme.workspace = true
[lib]
name = "aws_macros"
path = "src/lib.rs"
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.*", default-features = false, features = [
"proc-macro",
] }
quote = { version = "1.*", default-features = false }
syn = { version = "2.*", default-features = false, features = [
"full",
"extra-traits",
"proc-macro",
"parsing",
"printing",
"clone-impls",
] }
[lints]
workspace = true

22
aws_macros/src/lib.rs Normal file
View File

@@ -0,0 +1,22 @@
#![expect(
clippy::expect_used,
clippy::panic,
reason = "panics and expects are fine for proc macros"
)]
use proc_macro::TokenStream;
mod tag;
mod tags;
#[proc_macro_attribute]
#[expect(non_snake_case, reason = "attribute proc macros should be capitalized")]
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)
}

79
aws_macros/src/tag.rs Normal file
View File

@@ -0,0 +1,79 @@
use proc_macro::TokenStream;
use quote::quote;
#[derive(Debug)]
enum Translator {
Serde,
Manual,
}
pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
let root = quote! {::aws};
let expr: syn::Expr = syn::parse(attr).expect("expected expr in macro attribute");
let syn::Expr::Assign(assign) = expr else {
panic!("invalid expression in macro attribute")
};
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 == "translate", "invalid attribute key");
}
_ => panic!("invalid expression in tag field attribute, left side"),
}
let translator = match *assign.right {
syn::Expr::Path(ref exprpath) => {
let segments = &exprpath.path.segments;
let (Some(segment), 1) = (segments.first(), segments.len()) else {
panic!("invalid attribute key")
};
match segment.ident.to_string().as_str() {
"serde" => Translator::Serde,
"manual" => Translator::Manual,
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,
_ => panic!("only applicable to structs/enums"),
};
let translator = match translator {
Translator::Serde => quote! {
impl #root::tags::TranslatableSerde for #name {}
impl #root::tags::TagValue<#name> for #name {
type Error = #root::tags::ParseTagValueError;
type Translator = #root::tags::TranslateSerde;
}
},
Translator::Manual => quote! {
impl #root::tags::TranslatableManual for #name {}
impl #root::tags::TagValue<#name> for #name {
type Error = #root::tags::ParseTagValueError;
type Translator = #root::tags::TranslateManual;
}
},
};
quote! {
#elem
#translator
}
.into()
}

369
aws_macros/src/tags.rs Normal file
View File

@@ -0,0 +1,369 @@
use proc_macro::TokenStream;
use quote::quote;
#[derive(Debug)]
struct Input {
ident: syn::Ident,
vis: syn::Visibility,
elements: Vec<Element>,
}
#[derive(Debug)]
enum ElementKind {
Required,
Optional,
}
#[derive(Debug)]
struct Element {
ident: syn::Ident,
vis: syn::Visibility,
ty: syn::Path,
kind: ElementKind,
name: String,
}
fn parse_type(input: syn::Type) -> (syn::Path, ElementKind) {
match input {
syn::Type::Path(ty) => {
let segments = ty.path.segments.clone();
let first = segments.first().expect("segments is empty");
let (ident, optional) = match first.ident.to_string().as_str() {
"Option" => match first.arguments {
// It may not be required to parse this, we could just
// extract the inner type and let the compiler beat the
// caller up if there is some bullshit happening.
syn::PathArguments::AngleBracketed(ref genargs) => {
let mut args = genargs.args.clone();
match genargs.args.len() {
1 => {
let ty = args.pop().expect("genargs are empty");
let ty = match ty {
syn::punctuated::Pair::Punctuated(node, _punct) => node,
syn::punctuated::Pair::End(node) => node,
};
match ty {
syn::GenericArgument::Type(ty) => match ty {
syn::Type::Path(ty) => (ty, ElementKind::Optional),
_ => panic!("invalid generic type for Option"),
},
_ => panic!("need simple owned Option generic"),
}
}
_ => panic!("wrong number of Option generic arguments"),
}
}
_ => panic!("invalid Option usage"),
},
_ => (ty, ElementKind::Required),
};
(ident.path, optional)
}
_ => panic!("invalid field type"),
}
}
fn parse_field_attrs(attrs: &[syn::Attribute]) -> Option<String> {
match (attrs.first(), attrs.len()) {
(Some(attr), 1) => {
assert!(
attr.style == syn::AttrStyle::Outer,
"field attribute style needs to be an outer attribute"
);
match attr.meta {
syn::Meta::List(ref meta_list) => {
let tag = &meta_list.path;
let tag_name = match (tag.segments.first(), tag.segments.len()) {
(Some(segment), 1) => segment.ident.to_string(),
(_, 0) => return None,
_ => panic!("invalid field attribute path"),
};
assert!(tag_name == "tag", "invalid field attribute path {tag_name}");
let expr: syn::Expr = match meta_list.parse_args() {
Ok(expr) => expr,
Err(e) => panic!("failed parsing tag field attribute: {e}"),
};
let syn::Expr::Assign(assign) = expr else {
panic!("invalid expression in tag field attribute")
};
match *assign.left {
syn::Expr::Path(ref exprpath) => {
let segments = &exprpath.path.segments;
let (Some(segment), 1) = (segments.first(), segments.len()) else {
panic!("invalid tag field attribute key")
};
assert!(segment.ident == "key", "invalid tag field attribute key");
}
_ => panic!("invalid expression in tag field 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.value()),
_ => panic!("right side of tag field not a string literal"),
},
_ => panic!("right side of tag field attribute not a literal"),
}
}
_ => panic!("invalid field attribute"),
}
}
(_, 0) => None,
_ => panic!("invalid field attributes"),
}
}
fn parse_fields(input: impl IntoIterator<Item = syn::Field>) -> Vec<Element> {
let mut elements = Vec::new();
for field in input {
let ident = field.ident.expect("tuple structs not supported");
let vis = field.vis;
let (ty, kind) = parse_type(field.ty);
let name = parse_field_attrs(&field.attrs);
elements.push(Element {
ident: ident.clone(),
vis,
ty,
kind,
name: name.unwrap_or_else(|| ident.to_string()),
});
}
elements
}
fn parse_struct(input: syn::ItemStruct) -> Input {
Input {
ident: input.ident,
vis: input.vis,
elements: match input.fields {
syn::Fields::Named(fields) => parse_fields(fields.named),
_ => panic!("invalid fields"),
},
}
}
fn build_output(input: Input) -> TokenStream {
let root = quote! { ::aws };
let ident = input.ident;
let vis = input.vis;
let type_definition = {
let elements: Vec<proc_macro2::TokenStream> = input
.elements
.iter()
.map(|element| {
let ident = &element.ident;
let vis = &element.vis;
let ty = &element.ty;
match element.kind {
ElementKind::Required => {
quote!(
#vis #ident: #ty
)
}
ElementKind::Optional => {
quote!(
#vis #ident: ::std::option::Option<#ty>
)
}
}
})
.collect();
quote! {
#vis struct #ident {
#(#elements),*
}
}
};
let impls = {
let params = input.elements.iter().map(|element| {
let ident = &element.ident;
let ty = &element.ty;
match element.kind {
ElementKind::Required => quote!(#ident: #ty),
ElementKind::Optional => quote!(#ident: ::std::option::Option<#ty>),
}
});
let from_fields: Vec<proc_macro2::TokenStream> = input
.elements
.iter()
.map(|element| {
let ident = &element.ident;
quote! {#ident: #ident}
})
.collect();
let from_tags_fields: Vec<proc_macro2::TokenStream>= input.elements.iter().map(|element| {
let ident = &element.ident;
let ty = &element.ty;
let tag_name = &element.name;
let try_convert = quote!{
let value: ::std::result::Result<#ty, #root::tags::ParseTagsError> = <#ty as #root::tags::TagValue<#ty>>::from_raw_tag(value)
.map_err(
|e| #root::tags::ParseTagsError::ParseTag(#root::tags::ParseTagError::InvalidTagValue {
key,
inner: <<#ty as #root::tags::TagValue<#ty>>::Error as Into<#root::tags::ParseTagValueError>>::into(e),
}
)
);
let value = match value {
::std::result::Result::Ok(v) => v,
::std::result::Result::Err(e) => {
return Err(e);
}
};
value
};
let transformer = match element.kind {
ElementKind::Required => {
quote! {
let value: #root::tags::RawTagValue = value.ok_or_else(|| #root::tags::ParseTagsError::TagNotFound {
key: key.clone()
})?
.clone();
let value = {
#try_convert
};
value
}
}
ElementKind::Optional => {
quote! {
let value: ::std::option::Option<#ty> = value.map(|value: #root::tags::RawTagValue| {
let value = {
#try_convert
};
Ok(value)
}).transpose()?;
value
}
}
};
quote! {
#ident: {
let key: #root::tags::TagKey = #root::tags::TagKey::new(#tag_name.to_owned());
let value: ::std::option::Option<#root::tags::RawTagValue> = tags
.as_slice()
.iter()
.find(|tag| tag.key() == #tag_name)
.map(|tag| tag.value()).cloned();
let value = {
#transformer
};
value
}
}
}).collect();
let fields_to_tags: Vec<proc_macro2::TokenStream> = input
.elements
.iter()
.map(|element| {
let ident = &element.ident;
let ty= &element.ty;
let tag_name = &element.name;
match element.kind {
ElementKind::Required => {
quote! {
{
let key = #root::tags::TagKey::new(#tag_name.to_owned());
let value: #root::tags::RawTagValue = <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(self.#ident);
v.push(#root::tags::RawTag::new(key, value));
}
}
}
ElementKind::Optional => {
quote! {
{
match self.#ident {
::std::option::Option::Some(value) => {
let key = #root::tags::TagKey::new(#tag_name.to_owned());
let value: #root::tags::RawTagValue = <#ty as #root::tags::TagValue<#ty>>::into_raw_tag(value);
v.push(#root::tags::RawTag::new(key, value));
},
::std::option::Option::None => {
// do not serialize none values
},
}
}
}
}
}
})
.collect();
quote! {
impl #ident {
#vis fn from_values(#(#params),*) -> Self {
Self {
#(#from_fields),*
}
}
#vis fn from_tags(tags: #root::tags::TagList) -> Result<Self, #root::tags::ParseTagsError> {
Ok(Self {
#(#from_tags_fields),*
})
}
#vis fn into_tags(self) -> #root::tags::TagList {
let mut v = ::std::vec::Vec::new();
{
#(#fields_to_tags);*;
}
#root::tags::TagList::from_vec(v)
}
}
}
};
quote! {
#type_definition
#impls
}
.into()
}
#[expect(
clippy::needless_pass_by_value,
reason = "this is the usual signature for proc macros, and the inner function should have the same signature"
)]
pub(crate) fn transform(attr: TokenStream, item: TokenStream) -> TokenStream {
assert!(
attr.is_empty(),
"cannot take any attribute macro attributes"
);
let input = syn::parse_macro_input!(item as syn::Item);
let input = match input {
syn::Item::Struct(s) => parse_struct(s),
_ => panic!("only applicable to structs"),
};
build_output(input)
}