risingwave_common_proc_macro/
config_doc.rs1use itertools::Itertools;
16use quote::quote;
17use syn::{Attribute, Data, DataStruct, DeriveInput, Field, Fields};
18
19pub fn generate_config_doc_fn(input: DeriveInput) -> proc_macro2::TokenStream {
20 let mut doc = StructFieldDocs::new();
21
22 let struct_name = input.ident;
23 match input.data {
24 Data::Struct(ref data) => doc.extract_field_docs(data),
25 _ => panic!("This macro only supports structs"),
26 };
27
28 let vec_fields = doc.token_vec_fields();
29 let call_nested_fields = doc.token_call_nested_fields();
30 quote! {
31 impl #struct_name {
32 pub fn config_docs(name: String, docs: &mut std::collections::BTreeMap<String, Vec<(String, String)>>) {
33 docs.insert(name.clone(), #vec_fields);
34 #call_nested_fields;
35 }
36 }
37 }
38}
39
40fn extract_comment(attrs: &Vec<Attribute>) -> String {
41 attrs
42 .iter()
43 .filter_map(|attr| {
44 if let Ok(meta) = attr.parse_meta()
45 && meta.path().is_ident("doc")
46 && let syn::Meta::NameValue(syn::MetaNameValue {
47 lit: syn::Lit::Str(comment),
48 ..
49 }) = meta
50 {
51 Some(comment.value())
52 } else {
53 None
54 }
55 })
56 .filter_map(|comment| {
57 let trimmed = comment.trim();
58 if trimmed.is_empty() {
59 None
60 } else {
61 Some(trimmed.to_owned())
62 }
63 })
64 .join(" ")
65}
66
67fn is_nested_config_field(field: &Field) -> bool {
68 field.attrs.iter().any(|attr| {
69 if let Some(attr_name) = attr.path.get_ident() {
70 attr_name == "config_doc" && attr.tokens.to_string() == "(nested)"
71 } else {
72 false
73 }
74 })
75}
76
77fn is_omitted_config_field(field: &Field) -> bool {
78 field.attrs.iter().any(|attr| {
79 if let Some(attr_name) = attr.path.get_ident() {
80 attr_name == "config_doc" && attr.tokens.to_string() == "(omitted)"
81 } else {
82 false
83 }
84 })
85}
86
87fn field_name(f: &Field) -> String {
88 f.ident
89 .as_ref()
90 .expect("field name should not be empty")
91 .to_string()
92}
93
94struct StructFieldDocs {
95 nested_fields: Vec<(String, syn::Type)>,
97
98 fields: Vec<(String, String)>,
99}
100
101impl StructFieldDocs {
102 fn new() -> Self {
103 Self {
104 nested_fields: vec![],
105 fields: vec![],
106 }
107 }
108
109 fn extract_field_docs(&mut self, data: &DataStruct) {
110 match &data.fields {
111 Fields::Named(fields) => {
112 self.fields = fields
113 .named
114 .iter()
115 .filter_map(|field| {
116 if is_omitted_config_field(field) {
117 return None;
118 }
119 if is_nested_config_field(field) {
120 self.nested_fields
121 .push((field_name(field), field.ty.clone()));
122 return None;
123 }
124 let field_name = field.ident.as_ref()?.to_string();
125 let rustdoc = extract_comment(&field.attrs);
126 Some((field_name, rustdoc))
127 })
128 .collect_vec();
129 }
130 _ => unreachable!("field should be named"),
131 }
132 }
133
134 fn token_vec_fields(&self) -> proc_macro2::TokenStream {
135 let token_fields: Vec<proc_macro2::TokenStream> = self
136 .fields
137 .iter()
138 .map(|(name, doc)| {
139 quote! { (#name.to_string(), #doc.to_string()) }
140 })
141 .collect();
142
143 quote! {
144 vec![#(#token_fields),*]
145 }
146 }
147
148 fn token_call_nested_fields(&self) -> proc_macro2::TokenStream {
149 let tokens: Vec<proc_macro2::TokenStream> = self
150 .nested_fields
151 .iter()
152 .map(|(ident, ty)| {
153 quote! {
154 if name.is_empty() {
155 #ty::config_docs(#ident.to_string(), docs);
156 } else {
157 #ty::config_docs(format!("{}.{}", name, #ident), docs);
158 }
159 }
160 })
161 .collect();
162 quote! { #(#tokens)* }
163 }
164}