made updated libs work and made macro a one liner
This commit is contained in:
219
src/lib.rs
219
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<String> = 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<dyn std::error::Error>".to_string();
|
||||
|
||||
// Parse optional #[http_error_type = "..."] (default to Box<dyn Error>)
|
||||
let mut error_type = syn::parse_str::<syn::Path>("Box<dyn std::error::Error>").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();
|
||||
// Convert attr TokenStream -> proc_macro2 TokenStream so we can inspect/try parses safely
|
||||
let attr_ts: proc_macro2::TokenStream = proc_macro2::TokenStream::from(attr);
|
||||
|
||||
if !attr_ts.is_empty() {
|
||||
// First try: parse as syn::Meta (preferred)
|
||||
match syn::parse2::<Meta>(attr_ts.clone()) {
|
||||
Ok(meta) => match meta {
|
||||
Meta::List(meta_list) => {
|
||||
// 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_
|
||||
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! {
|
||||
// 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<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 {
|
||||
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! {
|
||||
#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::<Vec<_>>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user