1mod options_metadata;
2
3use proc_macro::TokenStream;
4use quote::{quote, quote_spanned};
5use syn::spanned::Spanned;
6use syn::{Attribute, DeriveInput, ImplItem, ItemImpl, LitStr, parse_macro_input};
7
8#[proc_macro_derive(OptionsMetadata, attributes(option, doc, option_group))]
9pub fn derive_options_metadata(input: TokenStream) -> TokenStream {
10 let input = parse_macro_input!(input as DeriveInput);
11
12 options_metadata::derive_impl(input)
13 .unwrap_or_else(syn::Error::into_compile_error)
14 .into()
15}
16
17#[proc_macro_derive(CombineOptions)]
18pub fn derive_combine(input: TokenStream) -> TokenStream {
19 let input = parse_macro_input!(input as DeriveInput);
20 impl_combine(&input)
21}
22
23fn impl_combine(ast: &DeriveInput) -> TokenStream {
24 let name = &ast.ident;
25 let fields = if let syn::Data::Struct(syn::DataStruct {
26 fields: syn::Fields::Named(ref fields),
27 ..
28 }) = ast.data
29 {
30 &fields.named
31 } else {
32 unimplemented!();
33 };
34
35 let combines = fields.iter().map(|f| {
36 let name = &f.ident;
37 quote! {
38 #name: self.#name.combine(other.#name)
39 }
40 });
41
42 let stream = quote! {
43 impl crate::Combine for #name {
44 fn combine(self, other: #name) -> #name {
45 #name {
46 #(#combines),*
47 }
48 }
49 }
50 };
51 stream.into()
52}
53
54fn get_doc_comment(attrs: &[Attribute]) -> String {
55 attrs
56 .iter()
57 .filter_map(|attr| {
58 if attr.path().is_ident("doc") {
59 if let syn::Meta::NameValue(meta) = &attr.meta {
60 if let syn::Expr::Lit(expr) = &meta.value {
61 if let syn::Lit::Str(str) = &expr.lit {
62 return Some(str.value().trim().to_string());
63 }
64 }
65 }
66 }
67 None
68 })
69 .collect::<Vec<_>>()
70 .join("\n")
71}
72
73fn get_env_var_pattern_from_attr(attrs: &[Attribute]) -> Option<String> {
74 attrs
75 .iter()
76 .find(|attr| attr.path().is_ident("attr_env_var_pattern"))
77 .and_then(|attr| attr.parse_args::<LitStr>().ok())
78 .map(|lit_str| lit_str.value())
79}
80
81fn get_added_in(attrs: &[Attribute]) -> Option<String> {
82 attrs
83 .iter()
84 .find(|a| a.path().is_ident("attr_added_in"))
85 .and_then(|attr| attr.parse_args::<LitStr>().ok())
86 .map(|lit_str| lit_str.value())
87}
88
89fn is_hidden(attrs: &[Attribute]) -> bool {
90 attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
91}
92
93#[proc_macro_attribute]
95pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
96 let ast = parse_macro_input!(input as ItemImpl);
97
98 let constants: Vec<_> = ast
99 .items
100 .iter()
101 .filter_map(|item| match item {
102 ImplItem::Const(item) if !is_hidden(&item.attrs) => {
103 let doc = get_doc_comment(&item.attrs);
104 let added_in = get_added_in(&item.attrs);
105 let syn::Expr::Lit(syn::ExprLit {
106 lit: syn::Lit::Str(lit),
107 ..
108 }) = &item.expr
109 else {
110 return None;
111 };
112 let name = lit.value();
113 Some((name, doc, added_in, item.ident.span()))
114 }
115 ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
116 if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
118 let doc = get_doc_comment(&item.attrs);
119 let added_in = get_added_in(&item.attrs);
120 Some((pattern, doc, added_in, item.sig.span()))
121 } else {
122 None }
124 }
125 _ => None,
126 })
127 .collect();
128
129 let added_in_errors: Vec<_> = constants
131 .iter()
132 .filter_map(|(name, _, added_in, span)| {
133 added_in.is_none().then_some({
134 let msg = format!(
135 "missing #[attr_added_in(\"x.y.z\")] on `{name}`\nnote: env vars for an upcoming release should be annotated with `#[attr_added_in(\"next release\")]`"
136 );
137 quote_spanned! {*span => compile_error!(#msg); }
138 })
139 })
140 .collect();
141
142 if !added_in_errors.is_empty() {
143 return quote! { #ast #(#added_in_errors)* }.into();
144 }
145
146 let env_var_names = ast.items.iter().filter_map(|item| {
147 if let ImplItem::Const(item) = item {
148 let syn::Expr::Lit(syn::ExprLit {
149 lit: syn::Lit::Str(lit),
150 ..
151 }) = &item.expr
152 else {
153 return None;
154 };
155 Some(lit.value())
156 } else {
157 None
158 }
159 });
160
161 let struct_name = &ast.self_ty;
162 let pairs = constants.iter().map(|(name, doc, added_in, _span)| {
163 if let Some(added_in) = added_in {
164 quote! { (#name, #doc, Some(#added_in)) }
165 } else {
166 quote! { (#name, #doc, None) }
167 }
168 });
169
170 let expanded = quote! {
171 #ast
172
173 impl #struct_name {
174 pub fn metadata<'a>() -> &'a [(&'static str, &'static str, Option<&'static str>)] {
176 &[#(#pairs),*]
177 }
178
179 pub fn all_names() -> &'static [&'static str] {
181 &[#(#env_var_names),*]
182 }
183 }
184 };
185
186 expanded.into()
187}
188
189#[proc_macro_attribute]
190pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
191 item
192}
193
194#[proc_macro_attribute]
195pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
196 item
197}
198
199#[proc_macro_attribute]
200pub fn attr_added_in(_attr: TokenStream, item: TokenStream) -> TokenStream {
201 item
202}