risingwave_common_proc_macro/
config_doc.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    // Fields that require recursively retrieving their field docs.
96    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}