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, 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_valid_added_in(added_in: &str) -> bool {
90 added_in == "next release" || is_semantic_version(added_in)
91}
92
93fn is_semantic_version(version: &str) -> bool {
94 let mut components = version.split('.');
95 let Some(major) = components.next() else {
96 return false;
97 };
98 let Some(minor) = components.next() else {
99 return false;
100 };
101 let Some(patch) = components.next() else {
102 return false;
103 };
104
105 if components.next().is_some() {
106 return false;
107 }
108
109 [major, minor, patch].into_iter().all(|component| {
110 !component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
111 })
112}
113
114fn is_hidden(attrs: &[Attribute]) -> bool {
115 attrs.iter().any(|attr| attr.path().is_ident("attr_hidden"))
116}
117
118#[proc_macro_attribute]
120pub fn attribute_env_vars_metadata(_attr: TokenStream, input: TokenStream) -> TokenStream {
121 let ast = parse_macro_input!(input as ItemImpl);
122
123 let constants: Vec<_> = ast
124 .items
125 .iter()
126 .filter_map(|item| match item {
127 ImplItem::Const(item) if !is_hidden(&item.attrs) => {
128 let doc = get_doc_comment(&item.attrs);
129 let added_in = get_added_in(&item.attrs);
130 let syn::Expr::Lit(syn::ExprLit {
131 lit: syn::Lit::Str(lit),
132 ..
133 }) = &item.expr
134 else {
135 return None;
136 };
137 let name = lit.value();
138 Some((name, doc, added_in, item.ident.span()))
139 }
140 ImplItem::Fn(item) if !is_hidden(&item.attrs) => {
141 if let Some(pattern) = get_env_var_pattern_from_attr(&item.attrs) {
143 let doc = get_doc_comment(&item.attrs);
144 let added_in = get_added_in(&item.attrs);
145 Some((pattern, doc, added_in, item.sig.span()))
146 } else {
147 None }
149 }
150 _ => None,
151 })
152 .collect();
153
154 let added_in_errors: Vec<_> = constants
156 .iter()
157 .filter_map(|(name, _, added_in, span)| {
158 let msg = match added_in {
159 None => format!(
160 "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\")]`"
161 ),
162 Some(added_in) if !is_valid_added_in(added_in) => format!(
163 "invalid #[attr_added_in(\"{added_in}\")] on `{name}`\nnote: expected `#[attr_added_in(\"x.y.z\")]` or `#[attr_added_in(\"next release\")]`"
164 ),
165 Some(_) => return None,
166 };
167 Some(quote_spanned! {*span => compile_error!(#msg); })
168 })
169 .collect();
170
171 if !added_in_errors.is_empty() {
172 return quote! { #ast #(#added_in_errors)* }.into();
173 }
174
175 let env_var_names = ast.items.iter().filter_map(|item| {
176 if let ImplItem::Const(item) = item {
177 let syn::Expr::Lit(syn::ExprLit {
178 lit: syn::Lit::Str(lit),
179 ..
180 }) = &item.expr
181 else {
182 return None;
183 };
184 Some(lit.value())
185 } else {
186 None
187 }
188 });
189
190 let struct_name = &ast.self_ty;
191 let pairs = constants.iter().map(|(name, doc, added_in, _span)| {
192 if let Some(added_in) = added_in {
193 quote! { (#name, #doc, Some(#added_in)) }
194 } else {
195 quote! { (#name, #doc, None) }
196 }
197 });
198
199 let expanded = quote! {
200 #ast
201
202 impl #struct_name {
203 pub fn metadata<'a>() -> &'a [(&'static str, &'static str, Option<&'static str>)] {
205 &[#(#pairs),*]
206 }
207
208 pub fn all_names() -> &'static [&'static str] {
210 &[#(#env_var_names),*]
211 }
212 }
213 };
214
215 expanded.into()
216}
217
218#[proc_macro_attribute]
219pub fn attr_hidden(_attr: TokenStream, item: TokenStream) -> TokenStream {
220 item
221}
222
223#[proc_macro_attribute]
224pub fn attr_env_var_pattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
225 item
226}
227
228#[proc_macro_attribute]
229pub fn attr_added_in(_attr: TokenStream, item: TokenStream) -> TokenStream {
230 item
231}