made updated libs work and made macro a one liner

This commit is contained in:
2025-09-09 12:36:43 -04:00
parent 3043170396
commit 793e53304f

View File

@ -1,72 +1,144 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use proc_macro2::Span;
use syn::{parse_macro_input, Lit, ItemEnum, DeriveInput, Fields, Data}; use quote::{quote};
use quote::format_ident; use syn::{
use http_core::{Queryable, ApiDispatch, HasHttp, Keys}; parse_macro_input, ItemStruct, ItemEnum, Fields, Type, Meta, Lit, Expr,
punctuated::Punctuated, token::Comma, MetaNameValue, parse::Parser
};
#[proc_macro_derive(HttpRequest, attributes(http_response, http_error_type))] #[proc_macro_attribute]
pub fn derive_http_request(input: TokenStream) -> TokenStream { pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); // parse the struct we're attached to
let query_name = &input.ident; let mut input = parse_macro_input!(item as ItemStruct);
let query_name_str = query_name.to_string(); let struct_ident = &input.ident;
// Parse optional #[http_response = "..."] // defaults
let mut response_name_opt: Option<String> = None; let mut method_s = "GET".to_string();
for attr in &input.attrs { let mut url_s = "".to_string();
if attr.path().is_ident("http_response") { let mut response_s = format!("{}Resp", struct_ident);
attr.parse_nested_meta(|meta| { let mut error_s = "Box<dyn std::error::Error>".to_string();
if meta.path.is_ident("http_response") {
let lit: Lit = meta.value()?.parse()?;
if let Lit::Str(litstr) = lit {
response_name_opt = Some(litstr.value());
}
}
Ok(())
}).unwrap();
}
}
let response_name_str = response_name_opt.unwrap_or_else(|| format!("{}Resp", query_name_str));
let response_name = format_ident!("{}", response_name_str);
// Parse optional #[http_error_type = "..."] (default to Box<dyn Error>) // Convert attr TokenStream -> proc_macro2 TokenStream so we can inspect/try parses safely
let mut error_type = syn::parse_str::<syn::Path>("Box<dyn std::error::Error>").unwrap(); let attr_ts: proc_macro2::TokenStream = proc_macro2::TokenStream::from(attr);
for attr in &input.attrs {
if attr.path().is_ident("http_error_type") { if !attr_ts.is_empty() {
attr.parse_nested_meta(|meta| { // First try: parse as syn::Meta (preferred)
if meta.path.is_ident("http_error_type") { match syn::parse2::<Meta>(attr_ts.clone()) {
let lit: Lit = meta.value()?.parse()?; Ok(meta) => match meta {
if let Lit::Str(litstr) = lit { Meta::List(meta_list) => {
error_type = syn::parse_str(&litstr.value()).unwrap(); // parse the inner tokens into name = value pairs
let nested: Punctuated<MetaNameValue, Comma> =
Punctuated::parse_terminated.parse2(meta_list.tokens)
.expect("failed to parse http attribute list");
for nv in nested {
if let Some(ident) = nv.path.get_ident() {
let key = ident.to_string();
// nv.value is an Expr in syn 2.x; expect Expr::Lit(Lit::Str)
if let Expr::Lit(expr_lit) = nv.value {
if let Lit::Str(litstr) = expr_lit.lit {
let val = litstr.value();
match key.as_str() {
"method" => method_s = val,
"url" => url_s = val,
"response" => response_s = val,
"error" => if !val.is_empty() { error_s = val },
_ => {}
}
}
}
}
}
}
Meta::NameValue(nv) => {
// handle weird case like `#[http = "foo"]` (unlikely) — accept name-value if it has ident
if let Some(ident) = nv.path.get_ident() {
if let Expr::Lit(expr_lit) = nv.value {
if let Lit::Str(litstr) = expr_lit.lit {
let key = ident.to_string();
let val = litstr.value();
match key.as_str() {
"method" => method_s = val,
"url" => url_s = val,
"response" => response_s = val,
"error" => if !val.is_empty() { error_s = val },
_ => {}
}
}
}
}
}
Meta::Path(_) => {
// attribute present but without key/value — keep defaults
}
},
Err(_) => {
// Fallback: maybe the tokens are just a comma-separated `k = "v", ...` list without meta wrapper.
if let Ok(nested) = Punctuated::<MetaNameValue, Comma>::parse_terminated.parse2(attr_ts.clone()) {
for nv in nested {
if let Some(ident) = nv.path.get_ident() {
let key = ident.to_string();
if let Expr::Lit(expr_lit) = nv.value {
if let Lit::Str(litstr) = expr_lit.lit {
let val = litstr.value();
match key.as_str() {
"method" => method_s = val,
"url" => url_s = val,
"response" => response_s = val,
"error" => if !val.is_empty() { error_s = val },
_ => {}
}
}
}
}
}
} }
} }
Ok(())
}).unwrap();
} }
} }
// Collect query parameters from fields prefixed with lnk_p_ // Attach #[http_error_type = "..."] for build.rs introspection
let query_param_code = if let Data::Struct(data_struct) = &input.data { let error_lit = syn::LitStr::new(&error_s, Span::call_site());
if let Fields::Named(fields_named) = &data_struct.fields { input.attrs.push(syn::parse_quote!(#[http_error_type = #error_lit]));
fields_named.named.iter().filter_map(|field| {
let ident = field.ident.as_ref()?; // Re-attach compact http attr (so your build.rs logic still sees it)
let field_name = ident.to_string(); let method_lit = syn::LitStr::new(&method_s, Span::call_site());
if field_name.starts_with("lnk_p_") { let url_lit = syn::LitStr::new(&url_s, Span::call_site());
let key = &field_name["lnk_p_".len()..]; let resp_lit = syn::LitStr::new(&response_s, Span::call_site());
Some(quote! { input.attrs.push(syn::parse_quote!(#[http(method = #method_lit, url = #url_lit, response = #resp_lit)]));
// Build query param snippets for lnk_p_* fields
let mut qparam_snippets: Vec<proc_macro2::TokenStream> = Vec::new();
if let Fields::Named(fields_named) = &input.fields {
for field in &fields_named.named {
if let Some(ident) = &field.ident {
if let Some(key) = ident.to_string().strip_prefix("lnk_p_") {
let key_lit = syn::LitStr::new(key, Span::call_site());
qparam_snippets.push(quote! {
if let Some(val) = &self.#ident { if let Some(val) = &self.#ident {
query_params.push((#key.to_string(), val.to_string())); query_params.push((#key_lit.to_string(), val.to_string()));
}
});
}
}
}
} }
})
} else { None }
}).collect::<Vec<_>>()
} else { Vec::new() }
} else { Vec::new() };
// Parse response & error into syn::Type so complex paths (crate::X) are allowed
let response_ty: Type = syn::parse_str(&response_s).unwrap_or_else(|_| {
syn::parse_str::<Type>("serde_json::Value").expect("fallback parse")
});
let error_ty: Type = syn::parse_str(&error_s).unwrap_or_else(|_| {
syn::parse_str::<Type>("Box<dyn std::error::Error>").expect("fallback parse")
});
// Build the impl
let expanded = quote! { let expanded = quote! {
#input
#[async_trait::async_trait] #[async_trait::async_trait]
impl http_core::Queryable for #query_name { impl http_core::Queryable for #struct_ident {
type R = #response_name; type R = #response_ty;
type E = #error_type; type E = #error_ty;
async fn send( async fn send(
&self, &self,
@ -79,11 +151,11 @@ pub fn derive_http_request(input: TokenStream) -> TokenStream {
use urlencoding::encode; use urlencoding::encode;
use http_core::HasHttp; use http_core::HasHttp;
// collect lnk_p_* query params
let mut query_params: Vec<(String,String)> = Vec::new(); let mut query_params: Vec<(String,String)> = Vec::new();
#(#query_param_code)*
// pick URL // expand lnk_p_* fields
#(#qparam_snippets)*
let mut url = if let Some(u) = override_url { let mut url = if let Some(u) = override_url {
u.to_string() u.to_string()
} else if sandbox { } else if sandbox {
@ -93,35 +165,34 @@ pub fn derive_http_request(input: TokenStream) -> TokenStream {
}; };
if !query_params.is_empty() { if !query_params.is_empty() {
let query_string = query_params.into_iter() let qs = query_params.into_iter()
.map(|(k,v)| format!("{}={}", k, encode(&v))) .map(|(k,v)| format!("{}={}", k, encode(&v)))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("&"); .join("&");
url.push('?'); url.push('?');
url.push_str(&query_string); url.push_str(&qs);
} }
// choose method let method = method_override.unwrap_or(#method_lit);
let client = Client::default(); let client = Client::default();
let mut request = match method_override.unwrap_or("GET") { let mut request = match method {
"GET" => client.get(url), "GET" => client.get(url.clone()),
"POST" => client.post(url), "POST" => client.post(url.clone()),
"PUT" => client.put(url), "PUT" => client.put(url.clone()),
"DELETE" => client.delete(url), "DELETE" => client.delete(url.clone()),
"PATCH" => client.patch(url), "PATCH" => client.patch(url.clone()),
m => panic!("Unsupported method override: {}", m), _ => client.get(url.clone()),
}; };
// add headers
if let Some(hdrs) = headers { if let Some(hdrs) = headers {
for (k,v) in hdrs { for (k,v) in hdrs {
request = request.append_header((k, v)); request = request.append_header((k, v));
} }
} }
let response = request.send().await?; let response = request.send().await.map_err(Into::into)?;
let bytes = response.body().await?; let bytes = response.body().await.map_err(Into::into)?;
let parsed: Self::R = serde_json::from_slice(&bytes)?; let parsed: Self::R = serde_json::from_slice(&bytes).map_err(Into::into)?;
Ok(parsed) Ok(parsed)
} }
} }