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 if meta.path().is_ident("doc") {
46 if let syn::Meta::NameValue(syn::MetaNameValue {
47 lit: syn::Lit::Str(comment),
48 ..
49 }) = meta
50 {
51 return Some(comment.value());
52 }
53 }
54 }
55 None
56 })
57 .filter_map(|comment| {
58 let trimmed = comment.trim();
59 if trimmed.is_empty() {
60 None
61 } else {
62 Some(trimmed.to_owned())
63 }
64 })
65 .join(" ")
66}
67
68fn is_nested_config_field(field: &Field) -> bool {
69 field.attrs.iter().any(|attr| {
70 if let Some(attr_name) = attr.path.get_ident() {
71 attr_name == "config_doc" && attr.tokens.to_string() == "(nested)"
72 } else {
73 false
74 }
75 })
76}
77
78fn is_omitted_config_field(field: &Field) -> bool {
79 field.attrs.iter().any(|attr| {
80 if let Some(attr_name) = attr.path.get_ident() {
81 attr_name == "config_doc" && attr.tokens.to_string() == "(omitted)"
82 } else {
83 false
84 }
85 })
86}
87
88fn field_name(f: &Field) -> String {
89 f.ident
90 .as_ref()
91 .expect("field name should not be empty")
92 .to_string()
93}
94
95struct StructFieldDocs {
96 nested_fields: Vec<(String, syn::Type)>,
98
99 fields: Vec<(String, String)>,
100}
101
102impl StructFieldDocs {
103 fn new() -> Self {
104 Self {
105 nested_fields: vec![],
106 fields: vec![],
107 }
108 }
109
110 fn extract_field_docs(&mut self, data: &DataStruct) {
111 match &data.fields {
112 Fields::Named(fields) => {
113 self.fields = fields
114 .named
115 .iter()
116 .filter_map(|field| {
117 if is_omitted_config_field(field) {
118 return None;
119 }
120 if is_nested_config_field(field) {
121 self.nested_fields
122 .push((field_name(field), field.ty.clone()));
123 return None;
124 }
125 let field_name = field.ident.as_ref()?.to_string();
126 let rustdoc = extract_comment(&field.attrs);
127 Some((field_name, rustdoc))
128 })
129 .collect_vec();
130 }
131 _ => unreachable!("field should be named"),
132 }
133 }
134
135 fn token_vec_fields(&self) -> proc_macro2::TokenStream {
136 let token_fields: Vec<proc_macro2::TokenStream> = self
137 .fields
138 .iter()
139 .map(|(name, doc)| {
140 quote! { (#name.to_string(), #doc.to_string()) }
141 })
142 .collect();
143
144 quote! {
145 vec![#(#token_fields),*]
146 }
147 }
148
149 fn token_call_nested_fields(&self) -> proc_macro2::TokenStream {
150 let tokens: Vec<proc_macro2::TokenStream> = self
151 .nested_fields
152 .iter()
153 .map(|(ident, ty)| {
154 quote! {
155 if name.is_empty() {
156 #ty::config_docs(#ident.to_string(), docs);
157 } else {
158 #ty::config_docs(format!("{}.{}", name, #ident), docs);
159 }
160 }
161 })
162 .collect();
163 quote! { #(#tokens)* }
164 }
165}