use proc_macro::TokenStream;
use syn::{spanned::Spanned, Fields, Generics, Ident, ItemEnum, Meta, NestedMeta, Type, Variant};
fn convert_enum_variant(variant: &Variant, f: impl Fn(&Ident, &Type) -> TokenStream) -> Result<TokenStream, &'static str> {
let mut optout = false;
for attr in &variant.attrs {
if attr.path.is_ident("convert_enum") {
match attr.parse_meta() {
Ok(Meta::List(meta)) => {
for entry in meta.nested {
match entry {
NestedMeta::Meta(Meta::Path(path)) => {
if path.is_ident("optout") {
optout = true;
} else {
return Err("Invalid #[convert_enum] attribute");
}
}
_ => return Err("Invalid #[convert_enum] attribute"),
}
}
}
_ => return Err("Invalid #[convert_enum] attribute"),
}
}
}
if optout {
Ok(TokenStream::new())
} else {
match &variant.fields {
Fields::Unnamed(fields) => {
let fields = fields.unnamed.iter().collect::<Vec<_>>();
if fields.len() == 1 {
let field = fields.into_iter().next().unwrap();
Ok(f(&variant.ident, &field.ty))
} else {
Err("ConvertEnum items must have exactly one field")
}
}
_ => Err("ConvertEnum items must be tuple-like"),
}
}
}
fn convert_enum(item: TokenStream, f: impl Fn(&Ident, &Generics, &Ident, &Type) -> TokenStream) -> TokenStream {
let item = syn::parse_macro_input!(item as ItemEnum);
let name = &item.ident;
let generics = &item.generics;
item.variants
.into_iter()
.map(|variant| match convert_enum_variant(&variant, |var, ty| f(name, generics, var, ty)) {
Ok(tokens) => tokens,
Err(msg) => quote::quote_spanned! {
variant.span() => compile_error!(#msg);
}
.into(),
})
.collect()
}
#[proc_macro_derive(From, attributes(convert_enum))]
pub fn convert_enum_from(item: TokenStream) -> TokenStream {
convert_enum(item, |name, generics, var, ty| {
quote::quote! {
impl #generics ::core::convert::From<#ty> for #name #generics {
fn from(val: #ty) -> Self {
Self::#var(val)
}
}
}
.into()
})
}
#[proc_macro_derive(TryInto, attributes(convert_enum))]
pub fn convert_enum_try_into(item: TokenStream) -> TokenStream {
convert_enum(item, |name, generics, var, ty| {
quote::quote! {
impl #generics ::core::convert::TryFrom<#name #generics> for #ty {
type Error = #name #generics;
fn try_from(val: #name #generics) -> Result<#ty, #name #generics> {
match val {
#name::#var(val) => Ok(val),
_ => Err(val),
}
}
}
}
.into()
})
}