Skip to main content

nym_http_api_client_macro/
lib.rs

1//! Proc-macros for configuring HTTP clients globally via the `inventory` crate.
2//!
3//! This crate provides macros that allow any crate in the workspace to contribute
4//! configuration modifications to `reqwest::ClientBuilder` instances through a
5//! compile-time registry pattern.
6//!
7//! # Overview
8//!
9//! The macros work by:
10//! 1. Collecting configuration functions from across all crates at compile time
11//! 2. Sorting them by priority (lower numbers run first)
12//! 3. Applying them sequentially to build HTTP clients with consistent settings
13//!
14//! # Examples
15//!
16//! ## Basic Usage with `client_defaults!`
17//!
18//! ```ignore
19//! use nym_http_api_client_macro::client_defaults;
20//!
21//! // Register default configurations with priority
22//! client_defaults!(
23//!     priority = 10;  // Optional, defaults to 0
24//!     timeout = std::time::Duration::from_secs(30),
25//!     gzip = true,
26//!     user_agent = "Nym/1.0"
27//! );
28//! ```
29//!
30//! ## Using `client_cfg!` for one-off configurations
31//!
32//! ```ignore
33//! use nym_http_api_client_macro::client_cfg;
34//!
35//! let configure = client_cfg!(
36//!     timeout = std::time::Duration::from_secs(60),
37//!     default_headers {
38//!         "X-Custom-Header" => "value",
39//!         "Authorization" => "auth_token"
40//!     }
41//! );
42//!
43//! let builder = reqwest::ClientBuilder::new();
44//! let configured = configure(builder);
45//! ```
46//!
47//! # DSL Reference
48//!
49//! The macro DSL supports several patterns:
50//! - `key = value` - Calls `builder.key(value)`
51//! - `key(arg1, arg2)` - Calls `builder.key(arg1, arg2)`
52//! - `flag` - Calls `builder.flag()` with no arguments
53//! - `default_headers { "name" => "value", ... }` - Sets default headers
54//!
55//! # Priority System
56//!
57//! Configurations are applied in priority order (lower numbers first):
58//! - Negative priorities: Early configuration (e.g., -100 for base settings)
59//! - Zero (default): Standard configuration
60//! - Positive priorities: Late configuration (e.g., 100 for overrides)
61
62use 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
73// ------------------ core crate path resolution ------------------
74
75fn 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
88// ------------------ DSL parsing ------------------
89
90struct 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
174// Generate statements that mutate a builder named `b` using the resolved core path.
175fn 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        // Optional header: `priority = <int> ;`
224        let fork = input.fork();
225        let mut priority = 0i32;
226
227        if fork.peek(Ident) && fork.parse::<Ident>()? == "priority" && fork.peek(Token![=]) {
228            // commit
229            let _ = input.parse::<Ident>()?; // priority
230            let _ = input.parse::<Token![=]>()?; // =
231            let lit: LitInt = input.parse()?;
232            priority = lit.base10_parse()?;
233            let _ = input.parse::<Token![;]>()?; // ;
234        }
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// ------------------ client_cfg! ------------------
280
281/// Creates a closure that configures a `ReqwestClientBuilder`.
282///
283/// This macro generates a closure that can be used to configure a single
284/// `reqwest::ClientBuilder` instance without affecting global defaults.
285///
286/// # Example
287///
288/// ```ignore
289/// use nym_http_api_client_macro::client_cfg;
290///
291/// let config = client_cfg!(
292///     timeout = std::time::Duration::from_secs(30),
293///     gzip = true
294/// );
295/// let client = config(reqwest::ClientBuilder::new()).build().unwrap();
296/// ```
297#[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/// Registers global default configurations for HTTP clients.
309///
310/// This macro submits a configuration record to the global registry that will
311/// be applied to all HTTP clients created with `default_builder()`.
312///
313/// # Parameters
314///
315/// - `priority` (optional): Integer priority for ordering (lower runs first, default: 0)
316/// - Configuration items: Any valid `reqwest::ClientBuilder` method calls
317///
318/// # Example
319///
320/// ```ignore
321/// use nym_http_api_client_macro::client_defaults;
322///
323/// client_defaults!(
324///     priority = -50;  // Run early in the configuration chain
325///     connect_timeout = std::time::Duration::from_secs(10),
326///     pool_max_idle_per_host = 32,
327///     default_headers {
328///         "User-Agent" => "Nym/1.0",
329///         "Accept" => "application/json"
330///     }
331/// );
332/// ```
333#[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    // Deterministic debug description string (used only when debug feature is enabled).
339    let description = describe_items(&items);
340
341    // Turn the DSL into statements that mutate `b`.
342    let body = to_stmts(items, &core);
343
344    // Optional compile-time diagnostics for the macro author (does not affect output).
345    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    // Debug logging injected into the generated closure, gated by the
355    // *macro crate's* `debug-inventory` feature (checked at expansion time).
356    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    // `apply` is a capture-free closure; it will coerce to a fn pointer
369    // if `ConfigRecord::apply` is typed as `fn(ReqwestClientBuilder) -> ReqwestClientBuilder`.
370    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}