structdoc_derive/
lib.rs

1#![doc(
2    html_root_url = "https://docs.rs/structdoc-derive/0.1.4/structdoc-derive/",
3    test(attr(deny(warnings)))
4)]
5#![forbid(unsafe_code)]
6
7//! Procedural derive support for the [`StructDoc`] trait.
8//!
9//! Users don't need to interact with this crate directly. It is brought in as a dependency of the
10//! [`structdoc`][structdoc-crate] crate and reexported from there.
11//!
12//! [`StructDoc`]: https://docs.rs/structdoc/*/structdoc/trait.StructDoc.html
13//! [structdoc-crate]: https://crates.io/crates/structdoc
14
15extern crate proc_macro;
16
17use std::iter;
18
19use either::Either;
20use itertools::Itertools;
21use proc_macro2::{Span, TokenStream};
22use quote::quote;
23use syn::punctuated::Punctuated;
24use syn::token::Comma;
25use syn::{
26    Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, Ident, Lit, LitStr, Meta,
27    MetaList, MetaNameValue, NestedMeta, Path, Variant,
28};
29
30macro_rules! pat_eq {
31    ($pat: pat, $val: expr) => {
32        if let $pat = $val {
33            true
34        } else {
35            false
36        }
37    };
38}
39
40#[derive(Clone, Eq, PartialEq)]
41enum RenameMode {
42    Lower,
43    Upper,
44    Pascal,
45    Camel,
46    Snake,
47    ScreamingSnake,
48    Kebab,
49    ScreamingKebab,
50}
51
52impl RenameMode {
53    fn apply(&self, s: &str) -> String {
54        use self::RenameMode::*;
55        use heck::*;
56        match self {
57            Lower => s.to_ascii_lowercase(),
58            Upper => s.to_ascii_uppercase(),
59            // Note that serde's and heck's definitions differ a bit (we have serde's in our API)
60            Pascal => s.to_camel_case(),
61            Camel => s.to_mixed_case(),
62            Snake => s.to_snake_case(),
63            ScreamingSnake => s.to_snake_case().to_ascii_uppercase(),
64            Kebab => s.to_kebab_case(),
65            ScreamingKebab => s.to_kebab_case().to_ascii_uppercase(),
66        }
67    }
68}
69
70impl From<&str> for RenameMode {
71    fn from(s: &str) -> RenameMode {
72        use self::RenameMode::*;
73        match s {
74            "lowercase" => Lower,
75            "UPPERCASE" => Upper,
76            "PascalCase" => Pascal,
77            "camelCase" => Camel,
78            "snake_case" => Snake,
79            "SCREAMING_SNAKE_CASE" => ScreamingSnake,
80            "kebab-case" => Kebab,
81            "SCREAMING-KEBAB-CASE" => ScreamingKebab,
82            s => panic!("Unknown rename-all value {}", s),
83        }
84    }
85}
86
87#[derive(Clone, Eq, PartialEq)]
88enum Tag {
89    Untagged,
90    Internal { tag: String },
91}
92
93#[derive(Clone)]
94enum Attr {
95    Hidden,
96    Flatten,
97    Leaf(String),
98    Default,
99    Doc(String),
100    RenameAll(RenameMode),
101    Rename(String),
102    Tag(Tag),
103    TagContent(String),
104    With(LitStr),
105}
106
107fn parse_paren(outer: &Ident, inner: &Ident) -> Option<Attr> {
108    match (outer.to_string().as_ref(), inner.to_string().as_ref()) {
109        ("doc", "hidden")
110        | ("serde", "skip")
111        | ("serde", "skip_deserializing")
112        | ("structdoc", "skip") => Some(Attr::Hidden),
113        ("serde", "flatten") | ("structdoc", "flatten") => Some(Attr::Flatten),
114        ("serde", "default") | ("structdoc", "default") => Some(Attr::Default),
115        ("structdoc", "leaf") => Some(Attr::Leaf(String::new())),
116        ("serde", "untagged") | ("structdoc", "untagged") => Some(Attr::Tag(Tag::Untagged)),
117        ("structdoc", attr) => panic!("Unknown structdoc attribute {}", attr),
118        // TODO: Does serde-transparent mean anything to us?
119        // Serde or rustc will validate doc and serde attributes, we don't hope to know them all.
120        _ => None,
121    }
122}
123
124fn parse_name_value(outer: &Ident, inner: &Ident, value: &Lit) -> Option<Attr> {
125    match (
126        outer.to_string().as_ref(),
127        inner.to_string().as_ref(),
128        value,
129    ) {
130        ("serde", "rename_all", Lit::Str(s)) | ("structdoc", "rename_all", Lit::Str(s)) => {
131            Some(Attr::RenameAll(RenameMode::from(&s.value() as &str)))
132        }
133        ("serde", "rename_all", _) | ("structdoc", "rename_all", _) => {
134            panic!("rename-all expects string")
135        }
136        ("serde", "rename", Lit::Str(s)) | ("structdoc", "rename", Lit::Str(s)) => {
137            Some(Attr::Rename(s.value()))
138        }
139        ("serde", "rename", _) | ("structdoc", "rename", _) => panic!("rename expects string"),
140        ("structdoc", "leaf", Lit::Str(s)) => Some(Attr::Leaf(s.value())),
141        ("structdoc", "leaf", _) => panic!("leaf expects string"),
142        ("serde", "tag", Lit::Str(s)) | ("structdoc", "tag", Lit::Str(s)) => {
143            Some(Attr::Tag(Tag::Internal { tag: s.value() }))
144        }
145        ("serde", "tag", _) | ("structdoc", "tag", _) => panic!("tag expects string"),
146        ("serde", "content", Lit::Str(s)) | ("structdoc", "content", Lit::Str(s)) => {
147            Some(Attr::TagContent(s.value()))
148        }
149        ("serde", "content", _) | ("structdoc", "content", _) => panic!("content expects string"),
150        ("structdoc", "with", Lit::Str(s)) => Some(Attr::With(s.clone())),
151        ("structdoc", "with", _) => panic!("with expects string"),
152        ("structdoc", name, _) => panic!("Unknown strucdoc attribute {}", name),
153        _ => None,
154    }
155}
156
157fn parse_nested_meta(
158    ident: Ident,
159    nested: impl IntoIterator<Item = NestedMeta>,
160) -> impl Iterator<Item = Attr> {
161    nested.into_iter().filter_map(move |nm| match nm {
162        NestedMeta::Meta(Meta::Path(path)) => {
163            parse_paren(&ident, &path.get_ident().expect("Multi-word attribute"))
164        }
165        NestedMeta::Meta(Meta::NameValue(MetaNameValue {
166            path: name,
167            lit: value,
168            ..
169        })) => parse_name_value(&ident, name.get_ident().expect("Bad name"), &value),
170        _ => panic!("Confused by attribute syntax"),
171    })
172}
173
174fn parse_attrs(attrs: &[Attribute]) -> Vec<Attr> {
175    attrs
176        .iter()
177        // Filter the doc, structdoc and serde attributes
178        .filter(|attr| {
179            attr.path.is_ident("structdoc")
180                || attr.path.is_ident("doc")
181                || attr.path.is_ident("serde")
182        })
183        // Interpret each as meta (all of them should be fine)
184        .map(|attr| attr.parse_meta().expect("Unparsable attribute"))
185        .flat_map(|meta| match meta {
186            Meta::List(MetaList { path, nested, .. }) => {
187                let ident = path.get_ident().expect("Multi-word attribute").to_owned();
188                Either::Left(parse_nested_meta(ident, nested))
189            }
190            Meta::NameValue(MetaNameValue { path, lit, .. }) => {
191                assert_eq!(
192                    path.get_ident().expect("Non-ident attribute"),
193                    "doc",
194                    "Broken attribute"
195                );
196                if let Lit::Str(string) = lit {
197                    Either::Right(iter::once(Attr::Doc(string.value())))
198                } else {
199                    panic!("Invalid doc text (must be string)");
200                }
201            }
202            _ => panic!("Wrong attribute"),
203        })
204        .collect()
205}
206
207fn mangle_name(name: &Ident, container_attrs: &[Attr], field_attrs: &[Attr]) -> String {
208    for attr in field_attrs {
209        if let Attr::Rename(name) = attr {
210            return name.clone();
211        }
212    }
213    for attr in container_attrs {
214        if let Attr::RenameAll(mode) = attr {
215            return mode.apply(&name.to_string());
216        }
217    }
218    name.to_string()
219}
220
221fn get_doc(attrs: &[Attr]) -> String {
222    let lines = iter::once(&Attr::Doc(String::new()))
223        .chain(attrs)
224        .filter_map(|a| if let Attr::Doc(d) = a { Some(d) } else { None })
225        .join("\n");
226    unindent::unindent(&lines)
227}
228
229fn get_mods(what: &Ident, attrs: &[Attr]) -> TokenStream {
230    let mut mods = TokenStream::new();
231    if attrs.iter().any(|a| pat_eq!(Attr::Default, a)) {
232        mods.extend(quote!(#what.set_flag(::structdoc::Flags::OPTIONAL);));
233    }
234    if attrs.iter().any(|a| pat_eq!(Attr::Flatten, a)) {
235        mods.extend(quote!(#what.set_flag(::structdoc::Flags::FLATTEN);));
236    }
237    if attrs.iter().any(|a| pat_eq!(Attr::Hidden, a)) {
238        mods.extend(quote!(#what.set_flag(::structdoc::Flags::HIDE);));
239    }
240    mods
241}
242
243fn leaf(ty: &str) -> TokenStream {
244    quote!(::structdoc::Documentation::leaf(#ty))
245}
246
247fn find_leaf(attrs: &[Attr]) -> Option<&str> {
248    for attr in attrs {
249        if let Attr::Leaf(s) = attr {
250            return Some(s);
251        }
252    }
253    None
254}
255
256fn find_with(attrs: &[Attr]) -> Option<&LitStr> {
257    for attr in attrs {
258        if let Attr::With(s) = attr {
259            return Some(s);
260        }
261    }
262    None
263}
264
265fn call_with(s: &LitStr) -> TokenStream {
266    let with: Path = s.parse().unwrap();
267    quote!(#with())
268}
269
270fn named_field(field: &Field, container_attrs: &[Attr]) -> TokenStream {
271    let ident = field
272        .ident
273        .as_ref()
274        .expect("A struct with anonymous field?!");
275    let field_attrs = parse_attrs(&field.attrs);
276    let name = mangle_name(ident, &container_attrs, &field_attrs);
277    let ty = &field.ty;
278    let doc = get_doc(&field_attrs);
279    let mods = get_mods(&Ident::new("field", Span::call_site()), &field_attrs);
280    // We don't dive into hiddens, we just list them here. They don't need to have the
281    // implementation.
282    let found_leaf = find_leaf(&field_attrs);
283    let is_leaf = found_leaf.is_some() || field_attrs.iter().any(|a| pat_eq!(Attr::Hidden, a));
284    let field_document = if let Some(with) = find_with(&field_attrs) {
285        call_with(with)
286    } else if is_leaf {
287        leaf(found_leaf.unwrap_or_default())
288    } else {
289        quote!(<#ty as ::structdoc::StructDoc>::document())
290    };
291
292    quote! {
293        let mut field = #field_document;
294        #mods
295        let field = ::structdoc::Field::new(field, #doc);
296        fields.push((#name.into(), field));
297    }
298}
299
300fn derive_struct(fields: &Punctuated<Field, Comma>, attrs: &[Attribute]) -> TokenStream {
301    let struct_attrs = parse_attrs(attrs);
302    // TODO: Validate the attributes make sense here
303    let insert_fields = fields.iter().map(|field| named_field(field, &struct_attrs));
304
305    quote! {
306        let mut fields = ::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
307        #(#insert_fields)*
308        ::structdoc::Documentation::struct_(fields)
309    }
310}
311
312fn find_tag(attrs: &[Attr]) -> Option<&Tag> {
313    for attr in attrs {
314        if let Attr::Tag(tag) = attr {
315            return Some(tag);
316        }
317    }
318    None
319}
320
321fn find_tag_content(attrs: &[Attr]) -> Option<&str> {
322    for attr in attrs {
323        if let Attr::TagContent(s) = attr {
324            return Some(s);
325        }
326    }
327    None
328}
329
330fn derive_enum(variants: &Punctuated<Variant, Comma>, attrs: &[Attribute]) -> TokenStream {
331    let enum_attrs = parse_attrs(attrs);
332    let insert_varianst = variants.iter().map(|variant| {
333        let variant_attrs = parse_attrs(&variant.attrs);
334        let name = mangle_name(&variant.ident, &enum_attrs, &variant_attrs);
335        let doc = get_doc(&variant_attrs);
336        let mods = get_mods(&Ident::new("variant", Span::call_site()), &variant_attrs);
337        let found_leaf = find_leaf(&variant_attrs);
338        let is_leaf =
339            found_leaf.is_some() || variant_attrs.iter().any(|a| pat_eq!(Attr::Hidden, a));
340        let constructor = if let Some(with) = find_with(&variant_attrs) {
341            call_with(with)
342        } else if is_leaf {
343            leaf(found_leaf.unwrap_or_default())
344        } else {
345            match &variant.fields {
346                Fields::Unit => leaf(""),
347                Fields::Named(fields) => {
348                    let mut attrs = Vec::new();
349                    attrs.extend(variant_attrs);
350                    attrs.extend(enum_attrs.clone());
351                    let insert_fields = fields.named.iter().map(|field| named_field(field, &attrs));
352                    quote! {
353                        {
354                            let mut fields =
355                                ::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
356                            #(#insert_fields)*
357                            ::structdoc::Documentation::struct_(fields)
358                        }
359                    }
360                }
361                Fields::Unnamed(fields) if fields.unnamed.is_empty() => leaf(""),
362                Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
363                    let ty = &fields.unnamed[0].ty;
364                    quote!(<#ty as ::structdoc::StructDoc>::document())
365                }
366                Fields::Unnamed(fields) => {
367                    panic!(
368                        "Don't know what to do with tuple variant with {} fields",
369                        fields.unnamed.len(),
370                    );
371                }
372            }
373        };
374        quote! {
375            let mut variant = #constructor;
376            #mods
377            let variant = ::structdoc::Field::new(variant, #doc);
378            variants.push((#name.into(), variant));
379        }
380    });
381
382    #[rustfmt::skip] // Tries to make long lines
383    let tag = match (find_tag(&enum_attrs), find_tag_content(&enum_attrs)) {
384        (None, _) => quote!(External),
385        (Some(Tag::Internal { tag }), Some(content)) => {
386            quote!(Adjacent { tag: #tag.to_owned(), content: #content.to_owned() })
387        },
388        (Some(Tag::Internal { tag }), _) => quote!(Internal { tag: #tag.to_owned() }),
389        (Some(Tag::Untagged), _) => quote!(Untagged),
390    };
391
392    quote! {
393        let mut variants = ::std::vec::Vec::<(&str, ::structdoc::Field)>::new();
394        #(#insert_varianst)*
395        ::structdoc::Documentation::enum_(variants, ::structdoc::Tagging::#tag)
396    }
397}
398
399fn derive_transparent(field: &Field) -> TokenStream {
400    let ty = &field.ty;
401    quote!(<#ty as ::structdoc::StructDoc>::document())
402}
403
404// Note: We declare the structdoc attribute. But we also parasite on serde attribute if present.
405#[proc_macro_derive(StructDoc, attributes(structdoc))]
406pub fn structdoc_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
407    let mut input: DeriveInput = syn::parse(input).unwrap();
408    let types = input.generics.type_params().cloned().collect::<Vec<_>>();
409    let clause = input.generics.make_where_clause();
410    for t in types {
411        let t = t.ident;
412        clause
413            .predicates
414            .push(syn::parse(quote!(#t: ::structdoc::StructDoc).into()).unwrap());
415    }
416    let name = &input.ident;
417    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
418    let inner = match input.data {
419        Data::Struct(DataStruct {
420            fields: Fields::Named(fields),
421            ..
422        }) => derive_struct(&fields.named, &input.attrs),
423        Data::Struct(DataStruct {
424            fields: Fields::Unnamed(ref fields),
425            ..
426        }) if fields.unnamed.len() == 1 => derive_transparent(&fields.unnamed[0]),
427        Data::Enum(DataEnum { variants, .. }) => derive_enum(&variants, &input.attrs),
428        _ => unimplemented!("Only named structs and enums for now :-("),
429    };
430    (quote! {
431        impl #impl_generics ::structdoc::StructDoc for #name #ty_generics
432        #where_clause
433        {
434            fn document() -> ::structdoc::Documentation {
435                #inner
436            }
437        }
438    })
439    .into()
440}