#![doc(
html_root_url = "https://docs.rs/structdoc-derive/0.1.4/structdoc-derive/",
test(attr(deny(warnings)))
)]
#![forbid(unsafe_code)]
extern crate proc_macro;
use std::iter;
use either::Either;
use itertools::Itertools;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{
Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, Ident, Lit, LitStr, Meta,
MetaList, MetaNameValue, NestedMeta, Path, Variant,
};
macro_rules! pat_eq {
($pat: pat, $val: expr) => {
if let $pat = $val {
true
} else {
false
}
};
}
#[derive(Clone, Eq, PartialEq)]
enum RenameMode {
Lower,
Upper,
Pascal,
Camel,
Snake,
ScreamingSnake,
Kebab,
ScreamingKebab,
}
impl RenameMode {
fn apply(&self, s: &str) -> String {
use self::RenameMode::*;
use heck::*;
match self {
Lower => s.to_ascii_lowercase(),
Upper => s.to_ascii_uppercase(),
Pascal => s.to_camel_case(),
Camel => s.to_mixed_case(),
Snake => s.to_snake_case(),
ScreamingSnake => s.to_snake_case().to_ascii_uppercase(),
Kebab => s.to_kebab_case(),
ScreamingKebab => s.to_kebab_case().to_ascii_uppercase(),
}
}
}
impl From<&str> for RenameMode {
fn from(s: &str) -> RenameMode {
use self::RenameMode::*;
match s {
"lowercase" => Lower,
"UPPERCASE" => Upper,
"PascalCase" => Pascal,
"camelCase" => Camel,
"snake_case" => Snake,
"SCREAMING_SNAKE_CASE" => ScreamingSnake,
"kebab-case" => Kebab,
"SCREAMING-KEBAB-CASE" => ScreamingKebab,
s => panic!("Unknown rename-all value {}", s),
}
}
}
#[derive(Clone, Eq, PartialEq)]
enum Tag {
Untagged,
Internal { tag: String },
}
#[derive(Clone)]
enum Attr {
Hidden,
Flatten,
Leaf(String),
Default,
Doc(String),
RenameAll(RenameMode),
Rename(String),
Tag(Tag),
TagContent(String),
With(LitStr),
}
fn parse_paren(outer: &Ident, inner: &Ident) -> Option<Attr> {
match (outer.to_string().as_ref(), inner.to_string().as_ref()) {
("doc", "hidden")
| ("serde", "skip")
| ("serde", "skip_deserializing")
| ("structdoc", "skip") => Some(Attr::Hidden),
("serde", "flatten") | ("structdoc", "flatten") => Some(Attr::Flatten),
("serde", "default") | ("structdoc", "default") => Some(Attr::Default),
("structdoc", "leaf") => Some(Attr::Leaf(String::new())),
("serde", "untagged") | ("structdoc", "untagged") => Some(Attr::Tag(Tag::Untagged)),
("structdoc", attr) => panic!("Unknown structdoc attribute {}", attr),
_ => None,
}
}
fn parse_name_value(outer: &Ident, inner: &Ident, value: &Lit) -> Option<Attr> {
match (
outer.to_string().as_ref(),
inner.to_string().as_ref(),
value,
) {
("serde", "rename_all", Lit::Str(s)) | ("structdoc", "rename_all", Lit::Str(s)) => {
Some(Attr::RenameAll(RenameMode::from(&s.value() as &str)))
}
("serde", "rename_all", _) | ("structdoc", "rename_all", _) => {
panic!("rename-all expects string")
}
("serde", "rename", Lit::Str(s)) | ("structdoc", "rename", Lit::Str(s)) => {
Some(Attr::Rename(s.value()))
}
("serde", "rename", _) | ("structdoc", "rename", _) => panic!("rename expects string"),
("structdoc", "leaf", Lit::Str(s)) => Some(Attr::Leaf(s.value())),
("structdoc", "leaf", _) => panic!("leaf expects string"),
("serde", "tag", Lit::Str(s)) | ("structdoc", "tag", Lit::Str(s)) => {
Some(Attr::Tag(Tag::Internal { tag: s.value() }))
}
("serde", "tag", _) | ("structdoc", "tag", _) => panic!("tag expects string"),
("serde", "content", Lit::Str(s)) | ("structdoc", "content", Lit::Str(s)) => {
Some(Attr::TagContent(s.value()))
}
("serde", "content", _) | ("structdoc", "content", _) => panic!("content expects string"),
("structdoc", "with", Lit::Str(s)) => Some(Attr::With(s.clone())),
("structdoc", "with", _) => panic!("with expects string"),
("structdoc", name, _) => panic!("Unknown strucdoc attribute {}", name),
_ => None,
}
}
fn parse_nested_meta(
ident: Ident,
nested: impl IntoIterator<Item = NestedMeta>,
) -> impl Iterator<Item = Attr> {
nested.into_iter().filter_map(move |nm| match nm {
NestedMeta::Meta(Meta::Path(path)) => {
parse_paren(&ident, &path.get_ident().expect("Multi-word attribute"))
}
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path: name,
lit: value,
..
})) => parse_name_value(&ident, name.get_ident().expect("Bad name"), &value),
_ => panic!("Confused by attribute syntax"),
})
}
fn parse_attrs(attrs: &[Attribute]) -> Vec<Attr> {
attrs
.iter()
.filter(|attr| {
attr.path.is_ident("structdoc")
|| attr.path.is_ident("doc")
|| attr.path.is_ident("serde")
})
.map(|attr| attr.parse_meta().expect("Unparsable attribute"))
.flat_map(|meta| match meta {
Meta::List(MetaList { path, nested, .. }) => {
let ident = path.get_ident().expect("Multi-word attribute").to_owned();
Either::Left(parse_nested_meta(ident, nested))
}
Meta::NameValue(MetaNameValue { path, lit, .. }) => {
assert_eq!(
path.get_ident().expect("Non-ident attribute"),
"doc",
"Broken attribute"
);
if let Lit::Str(string) = lit {
Either::Right(iter::once(Attr::Doc(string.value())))
} else {
panic!("Invalid doc text (must be string)");
}
}
_ => panic!("Wrong attribute"),
})
.collect()
}
fn mangle_name(name: &Ident, container_attrs: &[Attr], field_attrs: &[Attr]) -> String {
for attr in field_attrs {
if let Attr::Rename(name) = attr {
return name.clone();
}
}
for attr in container_attrs {
if let Attr::RenameAll(mode) = attr {
return mode.apply(&name.to_string());
}
}
name.to_string()
}
fn get_doc(attrs: &[Attr]) -> String {
let lines = iter::once(&Attr::Doc(String::new()))
.chain(attrs)
.filter_map(|a| if let Attr::Doc(d) = a { Some(d) } else { None })
.join("\n");
unindent::unindent(&lines)
}
fn get_mods(what: &Ident, attrs: &[Attr]) -> TokenStream {
let mut mods = TokenStream::new();
if attrs.iter().any(|a| pat_eq!(Attr::Default, a)) {
mods.extend(quote!(#what.set_flag(::structdoc::Flags::OPTIONAL);));
}
if attrs.iter().any(|a| pat_eq!(Attr::Flatten, a)) {
mods.extend(quote!(#what.set_flag(::structdoc::Flags::FLATTEN);));
}
if attrs.iter().any(|a| pat_eq!(Attr::Hidden, a)) {
mods.extend(quote!(#what.set_flag(::structdoc::Flags::HIDE);));
}
mods
}
fn leaf(ty: &str) -> TokenStream {
quote!(::structdoc::Documentation::leaf(#ty))
}
fn find_leaf(attrs: &[Attr]) -> Option<&str> {
for attr in attrs {
if let Attr::Leaf(s) = attr {
return Some(s);
}
}
None
}
fn find_with(attrs: &[Attr]) -> Option<&LitStr> {
for attr in attrs {
if let Attr::With(s) = attr {
return Some(s);
}
}
None
}
fn call_with(s: &LitStr) -> TokenStream {
let with: Path = s.parse().unwrap();
quote!(#with())
}
fn named_field(field: &Field, container_attrs: &[Attr]) -> TokenStream {
let ident = field
.ident
.as_ref()
.expect("A struct with anonymous field?!");
let field_attrs = parse_attrs(&field.attrs);
let name = mangle_name(ident, &container_attrs, &field_attrs);
let ty = &field.ty;
let doc = get_doc(&field_attrs);
let mods = get_mods(&Ident::new("field", Span::call_site()), &field_attrs);
let found_leaf = find_leaf(&field_attrs);
let is_leaf = found_leaf.is_some() || field_attrs.iter().any(|a| pat_eq!(Attr::Hidden, a));
let field_document = if let Some(with) = find_with(&field_attrs) {
call_with(with)
} else if is_leaf {
leaf(found_leaf.unwrap_or_default())
} else {
quote!(<#ty as ::structdoc::StructDoc>::document())
};
quote! {
let mut field = #field_document;
#mods
let field = ::structdoc::Field::new(field, #doc);
fields.push((#name.into(), field));
}
}
fn derive_struct(fields: &Punctuated<Field, Comma>, attrs: &[Attribute]) -> TokenStream {
let struct_attrs = parse_attrs(attrs);
let insert_fields = fields.iter().map(|field| named_field(field, &struct_attrs));
quote! {
let mut fields = ::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
#(#insert_fields)*
::structdoc::Documentation::struct_(fields)
}
}
fn find_tag(attrs: &[Attr]) -> Option<&Tag> {
for attr in attrs {
if let Attr::Tag(tag) = attr {
return Some(tag);
}
}
None
}
fn find_tag_content(attrs: &[Attr]) -> Option<&str> {
for attr in attrs {
if let Attr::TagContent(s) = attr {
return Some(s);
}
}
None
}
fn derive_enum(variants: &Punctuated<Variant, Comma>, attrs: &[Attribute]) -> TokenStream {
let enum_attrs = parse_attrs(attrs);
let insert_varianst = variants.iter().map(|variant| {
let variant_attrs = parse_attrs(&variant.attrs);
let name = mangle_name(&variant.ident, &enum_attrs, &variant_attrs);
let doc = get_doc(&variant_attrs);
let mods = get_mods(&Ident::new("variant", Span::call_site()), &variant_attrs);
let found_leaf = find_leaf(&variant_attrs);
let is_leaf =
found_leaf.is_some() || variant_attrs.iter().any(|a| pat_eq!(Attr::Hidden, a));
let constructor = if let Some(with) = find_with(&variant_attrs) {
call_with(with)
} else if is_leaf {
leaf(found_leaf.unwrap_or_default())
} else {
match &variant.fields {
Fields::Unit => leaf(""),
Fields::Named(fields) => {
let mut attrs = Vec::new();
attrs.extend(variant_attrs);
attrs.extend(enum_attrs.clone());
let insert_fields = fields.named.iter().map(|field| named_field(field, &attrs));
quote! {
{
let mut fields =
::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
#(#insert_fields)*
::structdoc::Documentation::struct_(fields)
}
}
}
Fields::Unnamed(fields) if fields.unnamed.is_empty() => leaf(""),
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
let ty = &fields.unnamed[0].ty;
quote!(<#ty as ::structdoc::StructDoc>::document())
}
Fields::Unnamed(fields) => {
panic!(
"Don't know what to do with tuple variant with {} fields",
fields.unnamed.len(),
);
}
}
};
quote! {
let mut variant = #constructor;
#mods
let variant = ::structdoc::Field::new(variant, #doc);
variants.push((#name.into(), variant));
}
});
#[rustfmt::skip]
let tag = match (find_tag(&enum_attrs), find_tag_content(&enum_attrs)) {
(None, _) => quote!(External),
(Some(Tag::Internal { tag }), Some(content)) => {
quote!(Adjacent { tag: #tag.to_owned(), content: #content.to_owned() })
},
(Some(Tag::Internal { tag }), _) => quote!(Internal { tag: #tag.to_owned() }),
(Some(Tag::Untagged), _) => quote!(Untagged),
};
quote! {
let mut variants = ::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
#(#insert_varianst)*
::structdoc::Documentation::enum_(variants, ::structdoc::Tagging::#tag)
}
}
fn derive_transparent(field: &Field) -> TokenStream {
let ty = &field.ty;
quote!(<#ty as ::structdoc::StructDoc>::document())
}
#[proc_macro_derive(StructDoc, attributes(structdoc))]
pub fn structdoc_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mut input: DeriveInput = syn::parse(input).unwrap();
let types = input.generics.type_params().cloned().collect::<Vec<_>>();
let clause = input.generics.make_where_clause();
for t in types {
let t = t.ident;
clause
.predicates
.push(syn::parse(quote!(#t: ::structdoc::StructDoc).into()).unwrap());
}
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let inner = match input.data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => derive_struct(&fields.named, &input.attrs),
Data::Struct(DataStruct {
fields: Fields::Unnamed(ref fields),
..
}) if fields.unnamed.len() == 1 => derive_transparent(&fields.unnamed[0]),
Data::Enum(DataEnum { variants, .. }) => derive_enum(&variants, &input.attrs),
_ => unimplemented!("Only named structs and enums for now :-("),
};
(quote! {
impl #impl_generics ::structdoc::StructDoc for #name #ty_generics
#where_clause
{
fn document() -> ::structdoc::Documentation {
#inner
}
}
})
.into()
}