827c13b69e
dont build netstack in CI additional rust 2024 fixes fixes removed temp.rs first round of cleanup removed duplicated NS types moved gateway probe to the monorepo
389 lines
12 KiB
Rust
389 lines
12 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 = "MyApp/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_macro_crate::{FoundCrate, crate_name};
|
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
use quote::{format_ident, 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 {
|
|
match crate_name("nym-http-api-client") {
|
|
Ok(FoundCrate::Itself) => quote!(crate),
|
|
Ok(FoundCrate::Name(name)) => {
|
|
let ident = Ident::new(&name, Span::call_site());
|
|
quote!( ::#ident )
|
|
}
|
|
Err(_) => {
|
|
// Fallback if the crate is not found by name (unlikely if deps set up correctly)
|
|
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,
|
|
}, // foo = EXPR
|
|
Call {
|
|
key: Ident,
|
|
args: Punctuated<Expr, Token![,]>,
|
|
_p: token::Paren,
|
|
}, // foo(a,b)
|
|
DefaultHeaders {
|
|
_key: Ident,
|
|
map: HeaderMapInit,
|
|
}, // default_headers { ... }
|
|
Flag {
|
|
key: Ident,
|
|
}, // foo
|
|
}
|
|
|
|
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 == 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)* }
|
|
}
|
|
|
|
// ------------------ 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()
|
|
}
|
|
|
|
// ------------------ client_defaults! with optional priority header ------------------
|
|
|
|
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 })
|
|
}
|
|
}
|
|
|
|
/// 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" => "MyApp/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();
|
|
|
|
// Generate a description of what this config does (before consuming items)
|
|
let config_description = if cfg!(feature = "debug-inventory") {
|
|
let descriptions = items
|
|
.0
|
|
.iter()
|
|
.map(|item| match item {
|
|
Item::Assign { key, value, .. } => {
|
|
format!("{}={:?}", quote!(#key), quote!(#value).to_string())
|
|
}
|
|
Item::Call { key, args, .. } => {
|
|
let args_str = args
|
|
.iter()
|
|
.map(|a| quote!(#a).to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
format!("{}({})", quote!(#key), args_str)
|
|
}
|
|
Item::Flag { key } => {
|
|
format!("{}()", quote!(#key))
|
|
}
|
|
Item::DefaultHeaders { .. } => "default_headers{{...}}".to_string(),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
|
|
quote! {
|
|
pub const __CONFIG_DESC: &str = #descriptions;
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// Now consume items to generate the body
|
|
let body = to_stmts(items, &core);
|
|
|
|
// Generate a unique identifier for this submission
|
|
let submission_id = format!("__client_defaults_{}", uuid::Uuid::new_v4().simple());
|
|
let submission_ident = syn::Ident::new(&submission_id, proc_macro2::Span::call_site());
|
|
|
|
// Debug output at compile time if enabled
|
|
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())
|
|
);
|
|
}
|
|
|
|
// Add debug_print_inventory call if the feature is enabled
|
|
let debug_call = if cfg!(feature = "debug-inventory") {
|
|
quote! {
|
|
#config_description
|
|
|
|
// Ensure the debug function gets called when config is applied
|
|
pub fn __cfg_with_debug(
|
|
b: #core::ReqwestClientBuilder
|
|
) -> #core::ReqwestClientBuilder {
|
|
eprintln!("[HTTP-INVENTORY] Applying: {} (priority={})", __CONFIG_DESC, #priority);
|
|
__cfg(b)
|
|
}
|
|
}
|
|
} else {
|
|
quote! {}
|
|
};
|
|
|
|
// Use the debug wrapper if feature is enabled
|
|
let apply_fn = if cfg!(feature = "debug-inventory") {
|
|
quote! { __cfg_with_debug }
|
|
} else {
|
|
quote! { __cfg }
|
|
};
|
|
|
|
let out = quote! {
|
|
#[allow(non_snake_case)]
|
|
mod #submission_ident {
|
|
use super::*;
|
|
#[allow(unused)]
|
|
pub fn __cfg(
|
|
mut b: #core::ReqwestClientBuilder
|
|
) -> #core::ReqwestClientBuilder {
|
|
#body
|
|
b
|
|
}
|
|
|
|
#debug_call
|
|
|
|
#core::inventory::submit! {
|
|
#core::registry::ConfigRecord {
|
|
priority: #priority,
|
|
apply: #apply_fn,
|
|
}
|
|
}
|
|
}
|
|
};
|
|
out.into()
|
|
}
|