config_docs_macros/
lib.rs1extern 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#[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}