passless_config_doc/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Data, DeriveInput, Expr, Fields, Lit, Meta, Type, parse_macro_input};
4
5/// Extract doc comments from attributes
6fn extract_doc(attrs: &[syn::Attribute]) -> String {
7    attrs
8        .iter()
9        .filter_map(|attr| {
10            if attr.path().is_ident("doc")
11                && let Meta::NameValue(nv) = &attr.meta
12                && let Expr::Lit(expr_lit) = &nv.value
13                && let Lit::Str(lit_str) = &expr_lit.lit
14            {
15                return Some(lit_str.value().trim().to_string());
16            }
17            None
18        })
19        .collect::<Vec<_>>()
20        .join(" ")
21}
22
23/// Get the type name from a Type
24fn get_type_name(ty: &Type) -> Option<String> {
25    if let Type::Path(type_path) = ty
26        && let Some(segment) = type_path.path.segments.last()
27    {
28        return Some(segment.ident.to_string());
29    }
30    None
31}
32
33/// Check if a type is a config struct (ends with "Config")
34fn is_config_type(ty: &Type) -> bool {
35    get_type_name(ty)
36        .map(|name| name.ends_with("Config"))
37        .unwrap_or(false)
38}
39
40/// Derive macro to extract doc comments and generate TOML config with comments
41///
42/// This macro generates `field_docs()` and for AppConfig, also generates
43/// `to_toml_with_comments()` that creates a fully documented TOML config.
44///
45/// Documentation is extracted from:
46/// - Struct-level doc comments (for section headers)
47/// - Field-level doc comments (for field descriptions)
48#[proc_macro_derive(ConfigDoc, attributes(config_example))]
49pub fn derive_config_doc(input: TokenStream) -> TokenStream {
50    let input = parse_macro_input!(input as DeriveInput);
51    let name = &input.ident;
52    let struct_doc = extract_doc(&input.attrs);
53
54    let fields = match &input.data {
55        Data::Struct(data) => match &data.fields {
56            Fields::Named(fields) => &fields.named,
57            _ => panic!("ConfigDoc only supports structs with named fields"),
58        },
59        _ => panic!("ConfigDoc only supports structs"),
60    };
61
62    let mut field_info = Vec::new();
63
64    for field in fields {
65        let field_name = field.ident.as_ref().unwrap();
66        let doc = extract_doc(&field.attrs);
67        let type_name = get_type_name(&field.ty);
68        let is_config = is_config_type(&field.ty);
69
70        field_info.push((field_name.clone(), doc, is_config, type_name));
71    }
72
73    let field_names: Vec<_> = field_info
74        .iter()
75        .map(|(n, _, _, _)| n.to_string())
76        .collect();
77    let field_help: Vec<_> = field_info.iter().map(|(_, d, _, _)| d).collect();
78
79    // Build field access expressions for to_toml_fields
80    let field_accessors: Vec<_> = field_info
81        .iter()
82        .filter(|(_, _, is_config, _)| !is_config)
83        .map(|(field_name, _, _, _)| {
84            quote! {
85                (stringify!(#field_name), format!("{:?}", self.#field_name))
86            }
87        })
88        .collect();
89
90    let mut expanded = quote! {
91        impl #name {
92            /// Get documentation for all fields
93            /// Returns an array of (field_name, documentation) tuples
94            pub fn field_docs() -> &'static [(&'static str, &'static str)] {
95                &[
96                    #((#field_names, #field_help),)*
97                ]
98            }
99
100            /// Get struct-level documentation
101            pub fn struct_doc() -> &'static str {
102                #struct_doc
103            }
104
105            /// Get field values as TOML-formatted strings
106            pub fn to_toml_fields(&self) -> Vec<(&'static str, String)> {
107                vec![
108                    #(#field_accessors),*
109                ]
110            }
111        }
112    };
113
114    // If this is AppConfig, generate to_toml_with_comments
115    if name == "AppConfig" {
116        let toml_gen = generate_toml_method(&field_info);
117        expanded = quote! {
118            #expanded
119            #toml_gen
120        };
121    }
122
123    TokenStream::from(expanded)
124}
125
126/// Generate the to_toml_with_comments method for AppConfig
127#[allow(clippy::type_complexity)]
128fn generate_toml_method(
129    fields: &[(syn::Ident, String, bool, Option<String>)],
130) -> proc_macro2::TokenStream {
131    let mut output_parts = Vec::new();
132
133    // Header
134    output_parts.push(quote! {
135        output.push_str("# Passless Configuration File Example\n");
136        output.push_str("# Place this file at: ~/.config/passless/config.toml\n\n");
137    });
138
139    // Process fields
140    for (field_name, doc, is_config, type_name) in fields {
141        let field_name_str = field_name.to_string();
142
143        if !is_config {
144            // Simple field - add to root section
145            if !doc.is_empty() {
146                output_parts.push(quote! {
147                    output.push_str(&format!("# {}\n", #doc));
148                });
149            }
150
151            // Output the value
152            output_parts.push(quote! {
153                output.push_str(&format!("{} = {:?}\n\n", #field_name_str, self.#field_name));
154            });
155        } else if let Some(type_name) = type_name {
156            // Config struct - generate section dynamically
157            let type_ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
158
159            output_parts.push(quote! {
160                // Section header with struct doc
161                if !#type_ident::struct_doc().is_empty() {
162                    output.push_str("# ");
163                    output.push_str(#type_ident::struct_doc());
164                    output.push_str("\n");
165                }
166
167                // TOML section header
168                output.push_str(&format!("[{}]\n", #field_name_str));
169
170                // Get field values from the nested struct
171                let field_values = self.#field_name.to_toml_fields();
172
173                // Iterate through all fields in the config struct
174                for (name, doc) in #type_ident::field_docs() {
175                    // Field documentation
176                    if !doc.is_empty() {
177                        output.push_str(&format!("# {}\n", doc));
178                    }
179
180
181                    // Find the value for this field
182                    if let Some((_, value)) = field_values.iter().find(|(n, _)| *n == *name) {
183                        output.push_str(&format!("{} = {}\n\n", name, value));
184                    }
185                }
186            });
187        }
188    }
189
190    quote! {
191        impl AppConfig {
192            /// Generate TOML configuration with comments extracted from doc comments
193            pub fn to_toml_with_comments(&self) -> String {
194                let mut output = String::new();
195                #(#output_parts)*
196                output
197            }
198        }
199    }
200}