nym_http_api_client_macro/
lib.rs1use proc_macro::TokenStream;
63use proc_macro2::TokenStream as TokenStream2;
64use quote::quote;
65use syn::{
66 Expr, Ident, LitInt, Result, Token, braced,
67 parse::{Parse, ParseStream},
68 parse_macro_input,
69 punctuated::Punctuated,
70 token,
71};
72
73fn core_path() -> TokenStream2 {
76 use proc_macro_crate::{FoundCrate, crate_name};
77
78 match crate_name("nym-http-api-client") {
79 Ok(FoundCrate::Itself) => quote!(crate),
80 Ok(FoundCrate::Name(name)) => {
81 let ident = Ident::new(&name, proc_macro2::Span::call_site());
82 quote!( ::#ident )
83 }
84 Err(_) => quote!(::nym_http_api_client),
85 }
86}
87
88struct Items(Punctuated<Item, Token![,]>);
91
92impl Parse for Items {
93 fn parse(input: ParseStream<'_>) -> Result<Self> {
94 Ok(Self(Punctuated::parse_terminated(input)?))
95 }
96}
97
98enum Item {
99 Assign {
100 key: Ident,
101 _eq: Token![=],
102 value: Expr,
103 },
104 Call {
105 key: Ident,
106 args: Punctuated<Expr, Token![,]>,
107 _p: token::Paren,
108 },
109 DefaultHeaders {
110 _key: Ident,
111 map: HeaderMapInit,
112 },
113 Flag {
114 key: Ident,
115 },
116}
117
118impl Parse for Item {
119 fn parse(input: ParseStream<'_>) -> Result<Self> {
120 let key: Ident = input.parse()?;
121
122 if input.peek(Token![=]) {
123 let _eq: Token![=] = input.parse()?;
124 let value: Expr = input.parse()?;
125 return Ok(Self::Assign { key, _eq, value });
126 }
127
128 if input.peek(token::Paren) {
129 let content;
130 let _p = syn::parenthesized!(content in input);
131 let args = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
132 return Ok(Self::Call { key, args, _p });
133 }
134
135 if input.peek(token::Brace) && key == quote::format_ident!("default_headers") {
136 let map = input.parse::<HeaderMapInit>()?;
137 return Ok(Self::DefaultHeaders { _key: key, map });
138 }
139
140 Ok(Self::Flag { key })
141 }
142}
143
144struct HeaderPair {
145 k: Expr,
146 _arrow: Token![=>],
147 v: Expr,
148}
149
150impl Parse for HeaderPair {
151 fn parse(input: ParseStream<'_>) -> Result<Self> {
152 Ok(Self {
153 k: input.parse()?,
154 _arrow: input.parse()?,
155 v: input.parse()?,
156 })
157 }
158}
159
160struct HeaderMapInit {
161 _brace: token::Brace,
162 pairs: Punctuated<HeaderPair, Token![,]>,
163}
164
165impl Parse for HeaderMapInit {
166 fn parse(input: ParseStream<'_>) -> Result<Self> {
167 let content;
168 let _brace = braced!(content in input);
169 let pairs = Punctuated::<HeaderPair, Token![,]>::parse_terminated(&content)?;
170 Ok(Self { _brace, pairs })
171 }
172}
173
174fn to_stmts(items: Items, core: &TokenStream2) -> TokenStream2 {
176 let mut stmts = Vec::new();
177
178 for it in items.0 {
179 match it {
180 Item::Assign { key, value, .. } => {
181 let m = key;
182 stmts.push(quote! { b = b.#m(#value); });
183 }
184 Item::Call { key, args, .. } => {
185 let m = key;
186 let args = args.iter();
187 stmts.push(quote! { b = b.#m( #( #args ),* ); });
188 }
189 Item::DefaultHeaders { map, .. } => {
190 let (ks, vs): (Vec<_>, Vec<_>) = map.pairs.into_iter().map(|p| (p.k, p.v)).unzip();
191 stmts.push(quote! {
192 let mut __cm = #core::reqwest::header::HeaderMap::new();
193 #(
194 {
195 use #core::reqwest::header::{HeaderName, HeaderValue};
196 let __k = HeaderName::try_from(#ks)
197 .unwrap_or_else(|e| panic!("Invalid header name: {}", e));
198 let __v = HeaderValue::try_from(#vs)
199 .unwrap_or_else(|e| panic!("Invalid header value: {}", e));
200 __cm.insert(__k, __v);
201 }
202 )*
203 b = b.default_headers(__cm);
204 });
205 }
206 Item::Flag { key } => {
207 let m = key;
208 stmts.push(quote! { b = b.#m(); });
209 }
210 }
211 }
212
213 quote! { #(#stmts)* }
214}
215
216struct MaybePrioritized {
217 priority: i32,
218 items: Items,
219}
220
221impl Parse for MaybePrioritized {
222 fn parse(input: ParseStream<'_>) -> Result<Self> {
223 let fork = input.fork();
225 let mut priority = 0i32;
226
227 if fork.peek(Ident) && fork.parse::<Ident>()? == "priority" && fork.peek(Token![=]) {
228 let _ = input.parse::<Ident>()?; let _ = input.parse::<Token![=]>()?; let lit: LitInt = input.parse()?;
232 priority = lit.base10_parse()?;
233 let _ = input.parse::<Token![;]>()?; }
235
236 let items = input.parse::<Items>()?;
237 Ok(Self { priority, items })
238 }
239}
240
241fn describe_items(items: &Items) -> String {
242 use std::fmt::Write;
243
244 let mut buf = String::new();
245
246 for (idx, item) in items.0.iter().enumerate() {
247 if idx > 0 {
248 buf.push_str(", ");
249 }
250
251 match item {
252 Item::Assign { key, value, .. } => {
253 let k = quote!(#key).to_string();
254 let v = quote!(#value).to_string();
255 let _ = write!(buf, "{}={}", k, v);
256 }
257 Item::Call { key, args, .. } => {
258 let k = quote!(#key).to_string();
259 let args_str = args
260 .iter()
261 .map(|a| quote!(#a).to_string())
262 .collect::<Vec<_>>()
263 .join(", ");
264 let _ = write!(buf, "{}({})", k, args_str);
265 }
266 Item::Flag { key } => {
267 let k = quote!(#key).to_string();
268 let _ = write!(buf, "{}()", k);
269 }
270 Item::DefaultHeaders { .. } => {
271 buf.push_str("default_headers{...}");
272 }
273 }
274 }
275
276 buf
277}
278
279#[proc_macro]
298pub fn client_cfg(input: TokenStream) -> TokenStream {
299 let items = parse_macro_input!(input as Items);
300 let core = core_path();
301 let body = to_stmts(items, &core);
302 let out = quote! {
303 |mut b: #core::ReqwestClientBuilder| { #body b }
304 };
305 out.into()
306}
307
308#[proc_macro]
334pub fn client_defaults(input: TokenStream) -> TokenStream {
335 let MaybePrioritized { priority, items } = parse_macro_input!(input as MaybePrioritized);
336 let core = core_path();
337
338 let description = describe_items(&items);
340
341 let body = to_stmts(items, &core);
343
344 if std::env::var("DEBUG_HTTP_INVENTORY").is_ok() {
346 eprintln!(
347 "cargo:warning=[HTTP-INVENTORY] Registering config with priority={} from {}: {}",
348 priority,
349 std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()),
350 description,
351 );
352 }
353
354 let debug_block = if cfg!(feature = "debug-inventory") {
357 quote! {
358 eprintln!(
359 "[HTTP-INVENTORY] Applying: {} (priority={})",
360 #description,
361 #priority
362 );
363 }
364 } else {
365 quote! {}
366 };
367
368 let out = quote! {
371 #core::inventory::submit! {
372 #core::registry::ConfigRecord {
373 priority: #priority,
374 apply: |mut b: #core::ReqwestClientBuilder| {
375 #debug_block
376 #body
377 b
378 },
379 }
380 }
381 };
382
383 out.into()
384}