extern crate proc_macro;
use std::mem::take;
use itertools::Itertools;
use manyhow::manyhow;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Attribute, Error, Expr, ItemEnum, TypeGenerics, Variant, 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(parse_variant(default, enum_name, &ty_generics))
.process_results(|iter| {
iter.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"))
}
fn parse_variant(
default: Option<Result<Expr, Error>>,
enum_name: impl ToTokens,
ty_generics: &TypeGenerics,
) -> impl Fn(&mut Variant) -> Result<TokenStream, Error> {
move |variant| {
let (pattern_attribute, code_attribute) = get_attributes(variant)?;
code_attribute
.as_ref()
.map(Attribute::parse_args::<Expr>)
.or(default.clone())
.ok_or(Error::new(
variant.span(),
"variant is missing status code and no default set",
))
.map(parse_attribute(
variant,
ty_generics,
&pattern_attribute,
&enum_name,
))?
}
}
fn get_attributes(variant: &mut Variant) -> Result<(Option<Attribute>, Option<Attribute>), Error> {
let mut attributes = (None, None);
for attribute in take(&mut variant.attrs) {
let path = attribute.path();
if path.is_ident("pattern") {
if let Some(replaced) = attributes.0.replace(attribute) {
return Err(Error::new(replaced.span(), "duplicate pattern attribute"));
}
} else if path.is_ident("code") {
if let Some(replaced) = attributes.1.replace(attribute) {
return Err(Error::new(replaced.span(), "duplicate code attribute"));
}
}
if attributes.0.is_some() && attributes.1.is_some() {
break;
};
}
Ok(attributes)
}
fn parse_attribute(
variant: &Variant,
ty_generics: &TypeGenerics,
pattern_attribute: &Option<Attribute>,
enum_name: impl ToTokens,
) -> impl Fn(Result<Expr, Error>) -> Result<TokenStream, Error> {
move |value| {
let variant_name = &variant.ident;
let expression = value?.into_token_stream();
let turbofish = ty_generics.as_turbofish();
let pattern = if !matches!(variant.fields, syn::Fields::Unit)
&& let Some(pattern) = pattern_attribute
{
Some(pattern.parse_args::<TokenStream>()?)
} else {
None
};
Ok(quote! {
#enum_name #turbofish :: #variant_name #pattern => #expression,
})
}
}