1use proc_macro::TokenStream;
19use quote::quote;
20use syn::{
21 parse::{Parse, ParseStream},
22 parse_macro_input, FnArg, Ident, ItemFn, LitStr, Pat, Token,
23};
24
25const SENSITIVE_PARAMS: &[&str] = &[
30 "identity_secret",
31 "shared_secret",
32 "password",
33 "wallet_code",
34 "code",
35 "pin",
36 "revocation_code",
37 "access_token",
38 "refresh_token",
39 "api_key",
40 "auth_payload",
41 "secret",
42 "totp_code",
43 "activation_code",
44 "two_factor_code",
45];
46
47struct EndpointAttr {
48 method: Ident,
49 host: Ident,
50 path: LitStr,
51 kind: Ident,
52}
53
54impl Parse for EndpointAttr {
55 fn parse(input: ParseStream) -> syn::Result<Self> {
56 let method: Ident = input.parse()?;
57 input.parse::<Token![,]>()?;
58
59 let mut host: Option<Ident> = None;
60 let mut path: Option<LitStr> = None;
61 let mut kind: Option<Ident> = None;
62
63 while !input.is_empty() {
64 let key: Ident = input.parse()?;
65 input.parse::<Token![=]>()?;
66 match key.to_string().as_str() {
67 "host" => host = Some(input.parse()?),
68 "path" => path = Some(input.parse()?),
69 "kind" => kind = Some(input.parse()?),
70 _ => return Err(syn::Error::new_spanned(key, "expected `host`, `path`, or `kind`")),
71 }
72 if !input.is_empty() {
73 input.parse::<Token![,]>()?;
74 }
75 }
76
77 Ok(EndpointAttr {
78 method,
79 host: host.ok_or_else(|| syn::Error::new(input.span(), "missing `host = ...`"))?,
80 path: path.ok_or_else(|| syn::Error::new(input.span(), "missing `path = ...`"))?,
81 kind: kind.ok_or_else(|| syn::Error::new(input.span(), "missing `kind = ...`"))?,
82 })
83 }
84}
85
86#[proc_macro_attribute]
102pub fn steam_endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
103 let attr = parse_macro_input!(attr as EndpointAttr);
104 let mut func = parse_macro_input!(item as ItemFn);
105
106 let method_str = attr.method.to_string();
107 let method_variant_name = match method_str.as_str() {
108 "GET" => "Get",
109 "POST" => "Post",
110 "PUT" => "Put",
111 "DELETE" => "Delete",
112 _ => {
113 return syn::Error::new_spanned(&attr.method, "expected GET, POST, PUT, or DELETE")
114 .to_compile_error()
115 .into();
116 }
117 };
118 let method_variant = Ident::new(method_variant_name, attr.method.span());
119
120 let host_ident = attr.host.clone();
121 let kind_ident = attr.kind.clone();
122
123 let host_label = host_ident.to_string().to_lowercase();
124 let kind_label = kind_ident.to_string().to_lowercase();
125 let path_str = attr.path.value();
126 let method_label = method_str.clone();
127
128 let fn_name = func.sig.ident.clone();
129 let fn_name_str = fn_name.to_string();
130
131 let has_receiver = func.sig.inputs.iter().any(|a| matches!(a, FnArg::Receiver(_)));
136 let mut skip_idents: Vec<Ident> = Vec::new();
137 if has_receiver {
138 skip_idents.push(Ident::new("self", proc_macro2::Span::call_site()));
139 }
140 for arg in &func.sig.inputs {
141 if let FnArg::Typed(pat_type) = arg {
142 if let Pat::Ident(pat_ident) = &*pat_type.pat {
143 if SENSITIVE_PARAMS.contains(&pat_ident.ident.to_string().as_str()) {
144 skip_idents.push(pat_ident.ident.clone());
145 }
146 }
147 }
148 }
149
150 let _ = skip_idents;
156 let instrument: syn::Attribute = syn::parse_quote! {
157 #[::tracing::instrument(
158 name = #fn_name_str,
159 skip_all,
160 fields(
161 steam.endpoint.method = #method_label,
162 steam.endpoint.host = #host_label,
163 steam.endpoint.path = #path_str,
164 steam.endpoint.kind = #kind_label,
165 steam.module = ::core::module_path!(),
166 )
167 )]
168 };
169 func.attrs.insert(0, instrument);
170
171 let original_block = func.block.clone();
182 let new_block: syn::Block = syn::parse_quote! {
183 {
184 static __EP: crate::endpoint::EndpointInfo = crate::endpoint::EndpointInfo {
185 name: #fn_name_str,
186 module: ::core::module_path!(),
187 method: crate::endpoint::HttpMethod::#method_variant,
188 host: crate::endpoint::Host::#host_ident,
189 path: #path_str,
190 kind: crate::endpoint::EndpointKind::#kind_ident,
191 };
192 crate::endpoint::CURRENT_ENDPOINT
193 .scope(&__EP, async move #original_block)
194 .await
195 }
196 };
197 *func.block = new_block;
198
199 let const_name = Ident::new(
205 &format!("__STEAM_ENDPOINT_INFO_{}", fn_name_str.to_uppercase()),
206 fn_name.span(),
207 );
208
209 let submit = quote! {
210 #[doc(hidden)]
211 #[allow(non_upper_case_globals, dead_code)]
212 const #const_name: () = {
213 ::inventory::submit! {
214 crate::endpoint::EndpointInfo {
215 name: #fn_name_str,
216 module: ::core::module_path!(),
217 method: crate::endpoint::HttpMethod::#method_variant,
218 host: crate::endpoint::Host::#host_ident,
219 path: #path_str,
220 kind: crate::endpoint::EndpointKind::#kind_ident,
221 }
222 }
223 };
224 };
225
226 let output = quote! {
227 #func
228 #submit
229 };
230
231 output.into()
232}