passless_config_doc/
lib.rs1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Data, DeriveInput, Expr, Fields, Lit, Meta, Type, parse_macro_input};
4
5fn 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
23fn 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
33fn 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#[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 let field_accessors: Vec<_> = field_info
81 .iter()
82 .filter(|(_, _, is_config, _)| !is_config)
83 .map(|(field_name, _, _, _)| {
84 quote! {
85 (stringify!(#field_name), match toml::Value::try_from(&self.#field_name) {
86 Ok(v) => format!("{}", v),
87 Err(_) => format!("{:?}", self.#field_name),
88 })
89 }
90 })
91 .collect();
92
93 let mut expanded = quote! {
94 impl #name {
95 pub fn field_docs() -> &'static [(&'static str, &'static str)] {
98 &[
99 #((#field_names, #field_help),)*
100 ]
101 }
102
103 pub fn struct_doc() -> &'static str {
105 #struct_doc
106 }
107
108 pub fn to_toml_fields(&self) -> Vec<(&'static str, String)> {
110 vec![
111 #(#field_accessors),*
112 ]
113 }
114 }
115 };
116
117 if name == "AppConfig" {
119 let toml_gen = generate_toml_method(&field_info);
120 expanded = quote! {
121 #expanded
122 #toml_gen
123 };
124 }
125
126 TokenStream::from(expanded)
127}
128
129#[allow(clippy::type_complexity)]
131fn generate_toml_method(
132 fields: &[(syn::Ident, String, bool, Option<String>)],
133) -> proc_macro2::TokenStream {
134 let mut output_parts = Vec::new();
135
136 output_parts.push(quote! {
138 output.push_str("# Passless Configuration File Example\n");
139 output.push_str("# Place this file at: ~/.config/passless/config.toml\n\n");
140 });
141
142 for (field_name, doc, is_config, type_name) in fields {
144 let field_name_str = field_name.to_string();
145
146 if !is_config {
147 if !doc.is_empty() {
149 output_parts.push(quote! {
150 output.push_str(&format!("# {}\n", #doc));
151 });
152 }
153
154 output_parts.push(quote! {
156 output.push_str(&format!("{} = {}\n\n", #field_name_str, match toml::Value::try_from(&self.#field_name) {
157 Ok(v) => format!("{}", v),
158 Err(_) => format!("{:?}", self.#field_name),
159 }));
160 });
161 } else if let Some(type_name) = type_name {
162 let type_ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
164
165 output_parts.push(quote! {
166 if !#type_ident::struct_doc().is_empty() {
168 output.push_str("# ");
169 output.push_str(#type_ident::struct_doc());
170 output.push_str("\n");
171 }
172
173 output.push_str(&format!("[{}]\n", #field_name_str));
175
176 let field_values = self.#field_name.to_toml_fields();
178
179 for (name, doc) in #type_ident::field_docs() {
181 if !doc.is_empty() {
183 output.push_str(&format!("# {}\n", doc));
184 }
185
186
187 if let Some((_, value)) = field_values.iter().find(|(n, _)| *n == *name) {
189 output.push_str(&format!("{} = {}\n\n", name, value));
190 }
191 }
192 });
193 }
194 }
195
196 quote! {
197 impl AppConfig {
198 pub fn to_toml_with_comments(&self) -> String {
200 let mut output = String::new();
201 #(#output_parts)*
202 output
203 }
204 }
205 }
206}