config_docs_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Field, Lit, Meta, MetaNameValue};
6
7/// Derives `ConfigDocs` using the provided doc-comments
8#[proc_macro_derive(ConfigDocs)]
9pub fn derive_config_docs(input: TokenStream) -> TokenStream {
10    let input = parse_macro_input!(input as DeriveInput);
11    let struct_name = input.ident;
12
13    let expanded = if let Data::Struct(data) = input.data {
14        let field_docs = data.fields.iter().map(|field| {
15            let field_name = field
16                .ident
17                .as_ref()
18                .map_or_else(|| String::from(""), |ident| ident.to_string());
19
20            let doc_comment = get_doc_string(field);
21            let ty = &field.ty;
22
23            Some(quote! {
24                docs.push((#field_name, #doc_comment));
25                docs.extend(<#ty as ConfigDocs>::config_docs());
26            })
27        });
28
29        quote! {
30            impl ConfigDocs for #struct_name {
31                fn config_docs() -> &'static [(&'static str, &'static str)] {
32                    let mut docs = Vec::new();
33                    #(#field_docs)*
34                    Box::leak(docs.into_boxed_slice())
35                }
36            }
37        }
38    } else {
39        quote! {
40            compile_error!("ConfigDocs can only be derived for structs");
41        }
42    };
43
44    expanded.into()
45}
46
47fn parse_doc_string(field: &Field) -> Vec<String> {
48    let doc_strs = field
49        .attrs
50        .iter()
51        .filter(|attr| attr.path().is_ident("doc"))
52        .filter_map(|attr| {
53            if let Meta::NameValue(MetaNameValue {
54                value:
55                    Expr::Lit(ExprLit {
56                        lit: Lit::Str(s), ..
57                    }),
58                ..
59            }) = &attr.meta
60            {
61                return Some(s.value());
62            }
63            None
64        })
65        .map(|mut s| {
66            if s.starts_with(" ") {
67                s.split_off(1)
68            } else {
69                s
70            }
71        })
72        .map(|s| s.to_string())
73        .skip_while(|s| s.is_empty())
74        .collect();
75
76    doc_strs
77}
78
79fn get_doc_string(field: &Field) -> String {
80    parse_doc_string(field)
81        .into_iter()
82        .next()
83        .unwrap_or_default()
84}