extern crate proc_macro;
use heck::AsSnekCase;
use itertools::Itertools;
use manyhow::manyhow;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Attribute, Error, Expr, Field, Ident, ItemEnum, parse2, spanned::Spanned};
#[manyhow(proc_macro_attribute)]
pub fn status_code(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
let mut item: ItemEnum = parse2(item)?;
let enum_name = &item.ident;
let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
let enum_span = item.span();
let default = (!attr.is_empty()).then(|| parse2(attr));
item.variants
.iter_mut()
.map(|variant| {
let attribute = &variant
.attrs
.pop_if(|attr| attr.path().is_ident("code"));
let attribute = attribute.as_ref();
if let Some(value) = attribute
.map(Attribute::parse_args::<Expr>)
.or(default.clone())
{
let variant_name = &variant.ident;
let mut value = value?.into_token_stream();
let turbofish = ty_generics.as_turbofish();
let pattern: Option<TokenStream> = match &variant.fields {
syn::Fields::Named(fields) => {
fields.named.iter()
.map(|Field { ident, .. }| ident.into_token_stream())
.reduce(|a, b| quote! {
#a, #b
})
.map(|p| quote! {
{ #p }
})
},
syn::Fields::Unnamed(fields) => {
let variant_name = Ident::new(&AsSnekCase(variant_name.to_string()).to_string(), enum_span);
let tuple_members = (0..fields.unnamed.len()).map(|id| {
let id_ident = Ident::new(&format!("__tuple_n{}", id), enum_span);
id_ident.into_token_stream()
}).reduce(|a, b| {
quote! {
#a, #b
}
});
value = quote! {
{
let #variant_name = (#tuple_members);
#value
}
};
Some(quote! {
(#tuple_members)
})
},
syn::Fields::Unit => None,
};
Ok(quote! {
#enum_name #turbofish :: #variant_name #pattern => #value,
})
} else {
Err(Error::new(
variant.span(),
"variant is missing status code and no default set",
))
}
})
.process_results(|iter| {
iter.tree_reduce(|a, b| {
quote! {
#a
#b
}
})
})?
.map(|contents| {
quote! {
#item
impl #impl_generics enum_status_code::AsStatusCode for #enum_name #ty_generics #where_clause {
fn as_status_code(&self) -> enum_status_code::http::StatusCode {
match self {
#contents
}
}
}
}
})
.ok_or(Error::new(enum_span, "expcted enum body"))
}