#![allow(clippy::option_if_let_else)]
use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{
DataEnum, DeriveInput, Error, Fields, FieldsNamed, Ident, Lit, parse_macro_input,
spanned::Spanned,
};
#[allow(clippy::missing_panics_doc)]
#[proc_macro_derive(Describe, attributes(describe))]
pub fn describe(input: TokenStream) -> TokenStream {
let DeriveInput {
ident,
data,
generics,
..
} = parse_macro_input!(input);
if !generics.params.is_empty() {
return Error::new(
generics.params.span(),
"Generics not supported in the current version",
)
.to_compile_error()
.into();
}
let description = match data {
syn::Data::Struct(s) => Ok(match s.fields {
syn::Fields::Named(FieldsNamed { named, .. }) => {
let field_descriptions = named.iter().filter_map(|f| {
let attrs = parse_field_attrs(&f.attrs);
if attrs.skip {
return None;
}
let field_name = f.ident.as_ref().unwrap();
let field_name_str = attrs.rename.unwrap_or_else(|| field_name.to_string());
let type_description = if let Some(with) = attrs.with {
quote! { #with.to_string() }
} else {
let field_type = &f.ty;
quote! { <#field_type>::describe() }
};
Some(quote! {
format!("{}: {}", #field_name_str, #type_description)
})
});
quote! {
format!("{} {{ {} }}",
stringify!(#ident),
vec![#(#field_descriptions),*].join(", ")
)
}
}
syn::Fields::Unnamed(fields) => {
let field_descriptions = fields.unnamed.iter().filter_map(|f| {
let attrs = parse_field_attrs(&f.attrs);
if attrs.skip {
return None;
}
let type_description = if let Some(with) = attrs.with {
quote! { #with.to_string() }
} else {
let field_type = &f.ty;
quote! { <#field_type>::describe() }
};
Some(quote! {
#type_description
})
});
quote! {
format!("{}({})",
stringify!(#ident),
vec![#(#field_descriptions),*].join(", ")
)
}
}
syn::Fields::Unit => quote! {
stringify!(#ident).to_string()
},
}),
syn::Data::Enum(DataEnum { variants, .. }) => Ok(enum_variants(&ident, &variants)),
syn::Data::Union(_) => Err(Error::new(
ident.span(),
"The syn::Data::Union variant is not supported",
)),
};
let description = match description {
Ok(ok) => ok,
Err(err) => return err.to_compile_error().into(),
};
let expanded = quote! {
impl Describer for #ident {
fn describe() -> String {
#description
}
}
};
expanded.into()
}
fn enum_variants(
name: &Ident,
variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let variant_descriptions = variants.iter().map(|v| {
let variant_name = &v.ident;
let variant_name_str = variant_name.to_string();
match &v.fields {
Fields::Named(fields) => {
let field_descriptions = fields.named.iter().filter_map(|f| {
let attrs = parse_field_attrs(&f.attrs);
if attrs.skip {
return None;
}
let field_name = f.ident.as_ref().unwrap();
let field_name_str = attrs.rename.unwrap_or_else(|| field_name.to_string());
let type_description = if let Some(with) = attrs.with {
quote! { #with.to_string() }
} else {
let field_type = &f.ty;
quote! { <#field_type>::describe() }
};
Some(quote! {
format!("{}: {}", #field_name_str, #type_description)
})
});
quote! {
format!("{} {{ {} }}", #variant_name_str, vec![#(#field_descriptions),*].join(", "))
}
}
Fields::Unnamed(fields) => {
let field_descriptions = fields.unnamed.iter().filter_map(|f| {
let attrs = parse_field_attrs(&f.attrs);
if attrs.skip {
return None;
}
let type_description = if let Some(with) = attrs.with {
quote! { #with.to_string() }
} else {
let field_type = &f.ty;
quote! { <#field_type>::describe() }
};
Some(quote! {
#type_description
})
});
quote! {
format!("{}({})", #variant_name_str, vec![#(#field_descriptions),*].join(", "))
}
}
Fields::Unit => {
quote! {
#variant_name_str.to_string()
}
}
}
});
quote! {
format!("{} #{{ {} }}",
stringify!(#name),
vec![#(#variant_descriptions),*].join(" | ")
)
}
}
struct FieldAttrs {
skip: bool,
rename: Option<String>,
with: Option<String>,
}
fn parse_field_attrs(attrs: &[syn::Attribute]) -> FieldAttrs {
let mut field_attrs = FieldAttrs {
skip: false,
rename: None,
with: None,
};
for attr in attrs {
if !attr.path().is_ident("describe") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
field_attrs.skip = true;
Ok(())
} else if meta.path.is_ident("rename") {
let value = meta.value()?;
let s: Lit = value.parse()?;
if let Lit::Str(lit_str) = s {
field_attrs.rename = Some(lit_str.value());
}
Ok(())
} else if meta.path.is_ident("with") {
let value = meta.value()?;
let s: Lit = value.parse()?;
if let Lit::Str(lit_str) = s {
field_attrs.with = Some(lit_str.value());
}
Ok(())
} else {
Err(meta.error("unsupported describe attribute"))
}
});
}
field_attrs
}