c17a205ada
* Inline closures, no randomness * Fix cfg usage
385 lines
11 KiB
Rust
385 lines
11 KiB
Rust
//! Proc-macros for configuring HTTP clients globally via the `inventory` crate.
|
|
//!
|
|
//! This crate provides macros that allow any crate in the workspace to contribute
|
|
//! configuration modifications to `reqwest::ClientBuilder` instances through a
|
|
//! compile-time registry pattern.
|
|
//!
|
|
//! # Overview
|
|
//!
|
|
//! The macros work by:
|
|
//! 1. Collecting configuration functions from across all crates at compile time
|
|
//! 2. Sorting them by priority (lower numbers run first)
|
|
//! 3. Applying them sequentially to build HTTP clients with consistent settings
|
|
//!
|
|
//! # Examples
|
|
//!
|
|
//! ## Basic Usage with `client_defaults!`
|
|
//!
|
|
//! ```ignore
|
|
//! use nym_http_api_client_macro::client_defaults;
|
|
//!
|
|
//! // Register default configurations with priority
|
|
//! client_defaults!(
|
|
//! priority = 10; // Optional, defaults to 0
|
|
//! timeout = std::time::Duration::from_secs(30),
|
|
//! gzip = true,
|
|
//! user_agent = "Nym/1.0"
|
|
//! );
|
|
//! ```
|
|
//!
|
|
//! ## Using `client_cfg!` for one-off configurations
|
|
//!
|
|
//! ```ignore
|
|
//! use nym_http_api_client_macro::client_cfg;
|
|
//!
|
|
//! let configure = client_cfg!(
|
|
//! timeout = std::time::Duration::from_secs(60),
|
|
//! default_headers {
|
|
//! "X-Custom-Header" => "value",
|
|
//! "Authorization" => "auth_token"
|
|
//! }
|
|
//! );
|
|
//!
|
|
//! let builder = reqwest::ClientBuilder::new();
|
|
//! let configured = configure(builder);
|
|
//! ```
|
|
//!
|
|
//! # DSL Reference
|
|
//!
|
|
//! The macro DSL supports several patterns:
|
|
//! - `key = value` - Calls `builder.key(value)`
|
|
//! - `key(arg1, arg2)` - Calls `builder.key(arg1, arg2)`
|
|
//! - `flag` - Calls `builder.flag()` with no arguments
|
|
//! - `default_headers { "name" => "value", ... }` - Sets default headers
|
|
//!
|
|
//! # Priority System
|
|
//!
|
|
//! Configurations are applied in priority order (lower numbers first):
|
|
//! - Negative priorities: Early configuration (e.g., -100 for base settings)
|
|
//! - Zero (default): Standard configuration
|
|
//! - Positive priorities: Late configuration (e.g., 100 for overrides)
|
|
|
|
use proc_macro::TokenStream;
|
|
use proc_macro2::TokenStream as TokenStream2;
|
|
use quote::quote;
|
|
use syn::{
|
|
Expr, Ident, LitInt, Result, Token, braced,
|
|
parse::{Parse, ParseStream},
|
|
parse_macro_input,
|
|
punctuated::Punctuated,
|
|
token,
|
|
};
|
|
|
|
// ------------------ core crate path resolution ------------------
|
|
|
|
fn core_path() -> TokenStream2 {
|
|
use proc_macro_crate::{FoundCrate, crate_name};
|
|
|
|
match crate_name("nym-http-api-client") {
|
|
Ok(FoundCrate::Itself) => quote!(crate),
|
|
Ok(FoundCrate::Name(name)) => {
|
|
let ident = Ident::new(&name, proc_macro2::Span::call_site());
|
|
quote!( ::#ident )
|
|
}
|
|
Err(_) => quote!(::nym_http_api_client),
|
|
}
|
|
}
|
|
|
|
// ------------------ DSL parsing ------------------
|
|
|
|
struct Items(Punctuated<Item, Token![,]>);
|
|
|
|
impl Parse for Items {
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
Ok(Self(Punctuated::parse_terminated(input)?))
|
|
}
|
|
}
|
|
|
|
enum Item {
|
|
Assign {
|
|
key: Ident,
|
|
_eq: Token![=],
|
|
value: Expr,
|
|
},
|
|
Call {
|
|
key: Ident,
|
|
args: Punctuated<Expr, Token![,]>,
|
|
_p: token::Paren,
|
|
},
|
|
DefaultHeaders {
|
|
_key: Ident,
|
|
map: HeaderMapInit,
|
|
},
|
|
Flag {
|
|
key: Ident,
|
|
},
|
|
}
|
|
|
|
impl Parse for Item {
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
let key: Ident = input.parse()?;
|
|
|
|
if input.peek(Token![=]) {
|
|
let _eq: Token![=] = input.parse()?;
|
|
let value: Expr = input.parse()?;
|
|
return Ok(Self::Assign { key, _eq, value });
|
|
}
|
|
|
|
if input.peek(token::Paren) {
|
|
let content;
|
|
let _p = syn::parenthesized!(content in input);
|
|
let args = Punctuated::<Expr, Token![,]>::parse_terminated(&content)?;
|
|
return Ok(Self::Call { key, args, _p });
|
|
}
|
|
|
|
if input.peek(token::Brace) && key == quote::format_ident!("default_headers") {
|
|
let map = input.parse::<HeaderMapInit>()?;
|
|
return Ok(Self::DefaultHeaders { _key: key, map });
|
|
}
|
|
|
|
Ok(Self::Flag { key })
|
|
}
|
|
}
|
|
|
|
struct HeaderPair {
|
|
k: Expr,
|
|
_arrow: Token![=>],
|
|
v: Expr,
|
|
}
|
|
|
|
impl Parse for HeaderPair {
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
Ok(Self {
|
|
k: input.parse()?,
|
|
_arrow: input.parse()?,
|
|
v: input.parse()?,
|
|
})
|
|
}
|
|
}
|
|
|
|
struct HeaderMapInit {
|
|
_brace: token::Brace,
|
|
pairs: Punctuated<HeaderPair, Token![,]>,
|
|
}
|
|
|
|
impl Parse for HeaderMapInit {
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
let content;
|
|
let _brace = braced!(content in input);
|
|
let pairs = Punctuated::<HeaderPair, Token![,]>::parse_terminated(&content)?;
|
|
Ok(Self { _brace, pairs })
|
|
}
|
|
}
|
|
|
|
// Generate statements that mutate a builder named `b` using the resolved core path.
|
|
fn to_stmts(items: Items, core: &TokenStream2) -> TokenStream2 {
|
|
let mut stmts = Vec::new();
|
|
|
|
for it in items.0 {
|
|
match it {
|
|
Item::Assign { key, value, .. } => {
|
|
let m = key;
|
|
stmts.push(quote! { b = b.#m(#value); });
|
|
}
|
|
Item::Call { key, args, .. } => {
|
|
let m = key;
|
|
let args = args.iter();
|
|
stmts.push(quote! { b = b.#m( #( #args ),* ); });
|
|
}
|
|
Item::DefaultHeaders { map, .. } => {
|
|
let (ks, vs): (Vec<_>, Vec<_>) = map.pairs.into_iter().map(|p| (p.k, p.v)).unzip();
|
|
stmts.push(quote! {
|
|
let mut __cm = #core::reqwest::header::HeaderMap::new();
|
|
#(
|
|
{
|
|
use #core::reqwest::header::{HeaderName, HeaderValue};
|
|
let __k = HeaderName::try_from(#ks)
|
|
.unwrap_or_else(|e| panic!("Invalid header name: {}", e));
|
|
let __v = HeaderValue::try_from(#vs)
|
|
.unwrap_or_else(|e| panic!("Invalid header value: {}", e));
|
|
__cm.insert(__k, __v);
|
|
}
|
|
)*
|
|
b = b.default_headers(__cm);
|
|
});
|
|
}
|
|
Item::Flag { key } => {
|
|
let m = key;
|
|
stmts.push(quote! { b = b.#m(); });
|
|
}
|
|
}
|
|
}
|
|
|
|
quote! { #(#stmts)* }
|
|
}
|
|
|
|
struct MaybePrioritized {
|
|
priority: i32,
|
|
items: Items,
|
|
}
|
|
|
|
impl Parse for MaybePrioritized {
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
// Optional header: `priority = <int> ;`
|
|
let fork = input.fork();
|
|
let mut priority = 0i32;
|
|
|
|
if fork.peek(Ident) && fork.parse::<Ident>()? == "priority" && fork.peek(Token![=]) {
|
|
// commit
|
|
let _ = input.parse::<Ident>()?; // priority
|
|
let _ = input.parse::<Token![=]>()?; // =
|
|
let lit: LitInt = input.parse()?;
|
|
priority = lit.base10_parse()?;
|
|
let _ = input.parse::<Token![;]>()?; // ;
|
|
}
|
|
|
|
let items = input.parse::<Items>()?;
|
|
Ok(Self { priority, items })
|
|
}
|
|
}
|
|
|
|
fn describe_items(items: &Items) -> String {
|
|
use std::fmt::Write;
|
|
|
|
let mut buf = String::new();
|
|
|
|
for (idx, item) in items.0.iter().enumerate() {
|
|
if idx > 0 {
|
|
buf.push_str(", ");
|
|
}
|
|
|
|
match item {
|
|
Item::Assign { key, value, .. } => {
|
|
let k = quote!(#key).to_string();
|
|
let v = quote!(#value).to_string();
|
|
let _ = write!(buf, "{}={}", k, v);
|
|
}
|
|
Item::Call { key, args, .. } => {
|
|
let k = quote!(#key).to_string();
|
|
let args_str = args
|
|
.iter()
|
|
.map(|a| quote!(#a).to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let _ = write!(buf, "{}({})", k, args_str);
|
|
}
|
|
Item::Flag { key } => {
|
|
let k = quote!(#key).to_string();
|
|
let _ = write!(buf, "{}()", k);
|
|
}
|
|
Item::DefaultHeaders { .. } => {
|
|
buf.push_str("default_headers{...}");
|
|
}
|
|
}
|
|
}
|
|
|
|
buf
|
|
}
|
|
|
|
// ------------------ client_cfg! ------------------
|
|
|
|
/// Creates a closure that configures a `ReqwestClientBuilder`.
|
|
///
|
|
/// This macro generates a closure that can be used to configure a single
|
|
/// `reqwest::ClientBuilder` instance without affecting global defaults.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// use nym_http_api_client_macro::client_cfg;
|
|
///
|
|
/// let config = client_cfg!(
|
|
/// timeout = std::time::Duration::from_secs(30),
|
|
/// gzip = true
|
|
/// );
|
|
/// let client = config(reqwest::ClientBuilder::new()).build().unwrap();
|
|
/// ```
|
|
#[proc_macro]
|
|
pub fn client_cfg(input: TokenStream) -> TokenStream {
|
|
let items = parse_macro_input!(input as Items);
|
|
let core = core_path();
|
|
let body = to_stmts(items, &core);
|
|
let out = quote! {
|
|
|mut b: #core::ReqwestClientBuilder| { #body b }
|
|
};
|
|
out.into()
|
|
}
|
|
|
|
/// Registers global default configurations for HTTP clients.
|
|
///
|
|
/// This macro submits a configuration record to the global registry that will
|
|
/// be applied to all HTTP clients created with `default_builder()`.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// - `priority` (optional): Integer priority for ordering (lower runs first, default: 0)
|
|
/// - Configuration items: Any valid `reqwest::ClientBuilder` method calls
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```ignore
|
|
/// use nym_http_api_client_macro::client_defaults;
|
|
///
|
|
/// client_defaults!(
|
|
/// priority = -50; // Run early in the configuration chain
|
|
/// connect_timeout = std::time::Duration::from_secs(10),
|
|
/// pool_max_idle_per_host = 32,
|
|
/// default_headers {
|
|
/// "User-Agent" => "Nym/1.0",
|
|
/// "Accept" => "application/json"
|
|
/// }
|
|
/// );
|
|
/// ```
|
|
#[proc_macro]
|
|
pub fn client_defaults(input: TokenStream) -> TokenStream {
|
|
let MaybePrioritized { priority, items } = parse_macro_input!(input as MaybePrioritized);
|
|
let core = core_path();
|
|
|
|
// Deterministic debug description string (used only when debug feature is enabled).
|
|
let description = describe_items(&items);
|
|
|
|
// Turn the DSL into statements that mutate `b`.
|
|
let body = to_stmts(items, &core);
|
|
|
|
// Optional compile-time diagnostics for the macro author (does not affect output).
|
|
if std::env::var("DEBUG_HTTP_INVENTORY").is_ok() {
|
|
eprintln!(
|
|
"cargo:warning=[HTTP-INVENTORY] Registering config with priority={} from {}: {}",
|
|
priority,
|
|
std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()),
|
|
description,
|
|
);
|
|
}
|
|
|
|
// Debug logging injected into the generated closure, gated by the
|
|
// *macro crate's* `debug-inventory` feature (checked at expansion time).
|
|
let debug_block = if cfg!(feature = "debug-inventory") {
|
|
quote! {
|
|
eprintln!(
|
|
"[HTTP-INVENTORY] Applying: {} (priority={})",
|
|
#description,
|
|
#priority
|
|
);
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// `apply` is a capture-free closure; it will coerce to a fn pointer
|
|
// if `ConfigRecord::apply` is typed as `fn(ReqwestClientBuilder) -> ReqwestClientBuilder`.
|
|
let out = quote! {
|
|
#core::inventory::submit! {
|
|
#core::registry::ConfigRecord {
|
|
priority: #priority,
|
|
apply: |mut b: #core::ReqwestClientBuilder| {
|
|
#debug_block
|
|
#body
|
|
b
|
|
},
|
|
}
|
|
}
|
|
};
|
|
|
|
out.into()
|
|
}
|