use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, FnArg, Ident, ItemFn, LitStr, Pat, Token,
};
const SENSITIVE_PARAMS: &[&str] = &[
"identity_secret",
"shared_secret",
"password",
"wallet_code",
"code",
"pin",
"revocation_code",
"access_token",
"refresh_token",
"api_key",
"auth_payload",
"secret",
"totp_code",
"activation_code",
"two_factor_code",
];
struct EndpointAttr {
method: Ident,
host: Ident,
path: LitStr,
kind: Ident,
}
impl Parse for EndpointAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let method: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let mut host: Option<Ident> = None;
let mut path: Option<LitStr> = None;
let mut kind: Option<Ident> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"host" => host = Some(input.parse()?),
"path" => path = Some(input.parse()?),
"kind" => kind = Some(input.parse()?),
_ => return Err(syn::Error::new_spanned(key, "expected `host`, `path`, or `kind`")),
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(EndpointAttr {
method,
host: host.ok_or_else(|| syn::Error::new(input.span(), "missing `host = ...`"))?,
path: path.ok_or_else(|| syn::Error::new(input.span(), "missing `path = ...`"))?,
kind: kind.ok_or_else(|| syn::Error::new(input.span(), "missing `kind = ...`"))?,
})
}
}
#[proc_macro_attribute]
pub fn steam_endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = parse_macro_input!(attr as EndpointAttr);
let mut func = parse_macro_input!(item as ItemFn);
let method_str = attr.method.to_string();
let method_variant_name = match method_str.as_str() {
"GET" => "Get",
"POST" => "Post",
"PUT" => "Put",
"DELETE" => "Delete",
_ => {
return syn::Error::new_spanned(&attr.method, "expected GET, POST, PUT, or DELETE")
.to_compile_error()
.into();
}
};
let method_variant = Ident::new(method_variant_name, attr.method.span());
let host_ident = attr.host.clone();
let kind_ident = attr.kind.clone();
let host_label = host_ident.to_string().to_lowercase();
let kind_label = kind_ident.to_string().to_lowercase();
let path_str = attr.path.value();
let method_label = method_str.clone();
let fn_name = func.sig.ident.clone();
let fn_name_str = fn_name.to_string();
let has_receiver = func.sig.inputs.iter().any(|a| matches!(a, FnArg::Receiver(_)));
let mut skip_idents: Vec<Ident> = Vec::new();
if has_receiver {
skip_idents.push(Ident::new("self", proc_macro2::Span::call_site()));
}
for arg in &func.sig.inputs {
if let FnArg::Typed(pat_type) = arg {
if let Pat::Ident(pat_ident) = &*pat_type.pat {
if SENSITIVE_PARAMS.contains(&pat_ident.ident.to_string().as_str()) {
skip_idents.push(pat_ident.ident.clone());
}
}
}
}
let _ = skip_idents;
let instrument: syn::Attribute = syn::parse_quote! {
#[::tracing::instrument(
name = #fn_name_str,
skip_all,
fields(
steam.endpoint.method = #method_label,
steam.endpoint.host = #host_label,
steam.endpoint.path = #path_str,
steam.endpoint.kind = #kind_label,
steam.module = ::core::module_path!(),
)
)]
};
func.attrs.insert(0, instrument);
let original_block = func.block.clone();
let new_block: syn::Block = syn::parse_quote! {
{
static __EP: crate::endpoint::EndpointInfo = crate::endpoint::EndpointInfo {
name: #fn_name_str,
module: ::core::module_path!(),
method: crate::endpoint::HttpMethod::#method_variant,
host: crate::endpoint::Host::#host_ident,
path: #path_str,
kind: crate::endpoint::EndpointKind::#kind_ident,
};
crate::endpoint::CURRENT_ENDPOINT
.scope(&__EP, async move #original_block)
.await
}
};
*func.block = new_block;
let const_name = Ident::new(
&format!("__STEAM_ENDPOINT_INFO_{}", fn_name_str.to_uppercase()),
fn_name.span(),
);
let submit = quote! {
#[doc(hidden)]
#[allow(non_upper_case_globals, dead_code)]
const #const_name: () = {
::inventory::submit! {
crate::endpoint::EndpointInfo {
name: #fn_name_str,
module: ::core::module_path!(),
method: crate::endpoint::HttpMethod::#method_variant,
host: crate::endpoint::Host::#host_ident,
path: #path_str,
kind: crate::endpoint::EndpointKind::#kind_ident,
}
}
};
};
let output = quote! {
#func
#submit
};
output.into()
}