From 793e53304f988cbb7b62544b01c5a1b903ed3e67 Mon Sep 17 00:00:00 2001 From: buckn Date: Tue, 9 Sep 2025 12:36:43 -0400 Subject: [PATCH] made updated libs work and made macro a one liner --- src/lib.rs | 229 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 79 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e1e22d6..471a92a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,72 +1,144 @@ use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, Lit, ItemEnum, DeriveInput, Fields, Data}; -use quote::format_ident; -use http_core::{Queryable, ApiDispatch, HasHttp, Keys}; +use proc_macro2::Span; +use quote::{quote}; +use syn::{ + 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))] -pub fn derive_http_request(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let query_name = &input.ident; - let query_name_str = query_name.to_string(); +#[proc_macro_attribute] +pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream { + // parse the struct we're attached to + let mut input = parse_macro_input!(item as ItemStruct); + let struct_ident = &input.ident; - // Parse optional #[http_response = "..."] - let mut response_name_opt: Option = None; - for attr in &input.attrs { - if attr.path().is_ident("http_response") { - attr.parse_nested_meta(|meta| { - 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); + // defaults + let mut method_s = "GET".to_string(); + let mut url_s = "".to_string(); + let mut response_s = format!("{}Resp", struct_ident); + let mut error_s = "Box".to_string(); - // Parse optional #[http_error_type = "..."] (default to Box) - let mut error_type = syn::parse_str::("Box").unwrap(); - for attr in &input.attrs { - if attr.path().is_ident("http_error_type") { - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("http_error_type") { - let lit: Lit = meta.value()?.parse()?; - if let Lit::Str(litstr) = lit { - error_type = syn::parse_str(&litstr.value()).unwrap(); - } - } - Ok(()) - }).unwrap(); - } - } + // Convert attr TokenStream -> proc_macro2 TokenStream so we can inspect/try parses safely + let attr_ts: proc_macro2::TokenStream = proc_macro2::TokenStream::from(attr); - // Collect query parameters from fields prefixed with lnk_p_ - let query_param_code = if let Data::Struct(data_struct) = &input.data { - if let Fields::Named(fields_named) = &data_struct.fields { - fields_named.named.iter().filter_map(|field| { - let ident = field.ident.as_ref()?; - let field_name = ident.to_string(); - if field_name.starts_with("lnk_p_") { - let key = &field_name["lnk_p_".len()..]; - Some(quote! { - if let Some(val) = &self.#ident { - query_params.push((#key.to_string(), val.to_string())); + if !attr_ts.is_empty() { + // First try: parse as syn::Meta (preferred) + match syn::parse2::(attr_ts.clone()) { + Ok(meta) => match meta { + Meta::List(meta_list) => { + // parse the inner tokens into name = value pairs + let nested: Punctuated = + 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 }, + _ => {} + } + } + } } - }) - } else { None } - }).collect::>() - } else { Vec::new() } - } else { Vec::new() }; + } + } + 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::::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 }, + _ => {} + } + } + } + } + } + } + } + } + } + // Attach #[http_error_type = "..."] for build.rs introspection + let error_lit = syn::LitStr::new(&error_s, Span::call_site()); + input.attrs.push(syn::parse_quote!(#[http_error_type = #error_lit])); + + // Re-attach compact http attr (so your build.rs logic still sees it) + let method_lit = syn::LitStr::new(&method_s, Span::call_site()); + let url_lit = syn::LitStr::new(&url_s, Span::call_site()); + let resp_lit = syn::LitStr::new(&response_s, Span::call_site()); + 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 = 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 { + query_params.push((#key_lit.to_string(), val.to_string())); + } + }); + } + } + } + } + + // 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::("serde_json::Value").expect("fallback parse") + }); + let error_ty: Type = syn::parse_str(&error_s).unwrap_or_else(|_| { + syn::parse_str::("Box").expect("fallback parse") + }); + + // Build the impl let expanded = quote! { + #input + #[async_trait::async_trait] - impl http_core::Queryable for #query_name { - type R = #response_name; - type E = #error_type; + impl http_core::Queryable for #struct_ident { + type R = #response_ty; + type E = #error_ty; async fn send( &self, @@ -79,11 +151,11 @@ pub fn derive_http_request(input: TokenStream) -> TokenStream { use urlencoding::encode; use http_core::HasHttp; - // collect lnk_p_* query params - let mut query_params: Vec<(String, String)> = Vec::new(); - #(#query_param_code)* + let mut query_params: Vec<(String,String)> = Vec::new(); + + // expand lnk_p_* fields + #(#qparam_snippets)* - // pick URL let mut url = if let Some(u) = override_url { u.to_string() } else if sandbox { @@ -93,35 +165,34 @@ pub fn derive_http_request(input: TokenStream) -> TokenStream { }; 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))) .collect::>() .join("&"); 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 mut request = match method_override.unwrap_or("GET") { - "GET" => client.get(url), - "POST" => client.post(url), - "PUT" => client.put(url), - "DELETE" => client.delete(url), - "PATCH" => client.patch(url), - m => panic!("Unsupported method override: {}", m), + let mut request = match method { + "GET" => client.get(url.clone()), + "POST" => client.post(url.clone()), + "PUT" => client.put(url.clone()), + "DELETE" => client.delete(url.clone()), + "PATCH" => client.patch(url.clone()), + _ => client.get(url.clone()), }; - // add headers if let Some(hdrs) = headers { - for (k, v) in hdrs { + for (k,v) in hdrs { request = request.append_header((k, v)); } } - let response = request.send().await?; - let bytes = response.body().await?; - let parsed: Self::R = serde_json::from_slice(&bytes)?; + let response = request.send().await.map_err(Into::into)?; + let bytes = response.body().await.map_err(Into::into)?; + let parsed: Self::R = serde_json::from_slice(&bytes).map_err(Into::into)?; Ok(parsed) } }