diff --git a/src/lib.rs b/src/lib.rs index 2affa53..ad5cc5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,17 +3,37 @@ use quote::quote; use syn::{parse_macro_input, Lit, ItemEnum, DeriveInput, Fields, Data}; use quote::format_ident; -#[proc_macro_derive(HttpRequest, attributes(http_get))] +#[proc_macro_derive(HttpRequest, attributes(http_get, http_response))] pub fn derive_http_get_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(); - // Derive response enum name by replacing "Q" suffix with "R" - let response_name_str = if query_name_str.ends_with("Q") { - query_name_str.trim_end_matches("Q").to_string() + "R" + // Parse optional #[http_response = "..."] attribute via parse_nested_meta + 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_or_else(|e| panic!("Error parsing http_response attribute: {}", e)); + } + } + + // Determine response type name + let response_name_str = if let Some(custom_resp) = response_name_opt { + custom_resp + } else if query_name_str == "Q" { + "R".to_string() + } else if query_name_str.ends_with('Q') { + format!("{}R", &query_name_str[..query_name_str.len() - 1]) } else { - panic!("HttpRequest derive macro expects the struct name to end with 'Q'"); + panic!("HttpRequest derive macro expects the type name to be 'Q' or end with 'Q', or specify #[http_response = \"...\"] to override"); }; let response_name = format_ident!("{}", response_name_str); @@ -23,19 +43,19 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream { if attr.path().is_ident("http_get") { attr.parse_nested_meta(|meta| { if meta.path.is_ident("url") { - let value: Lit = meta.value()?.parse()?; - if let Lit::Str(litstr) = value { + let lit: Lit = meta.value()?.parse()?; + if let Lit::Str(litstr) = lit { base_url = Some(litstr.value()); } } Ok(()) - }).unwrap(); + }).unwrap_or_else(|e| panic!("Error parsing http_get attribute: {}", e)); } } let base_url = base_url.expect("Missing #[http_get(url = \"...\")] attribute"); let base_url_lit = syn::LitStr::new(&base_url, proc_macro2::Span::call_site()); - // Collect query parameters from fields named "lnk_p_*" + // Collect query parameters from fields named "lnk_p_*" (only for structs) 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| { @@ -57,7 +77,6 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream { Vec::new() }; - // Generate the impl let expanded = quote! { #[async_trait::async_trait] impl Queryable for #query_name { @@ -75,10 +94,15 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream { let mut url = #base_url_lit.to_string(); if !query_params.is_empty() { - let query_string: String = query_params.iter() - .map(|(k, v)| format!("{}={}", k, encode(v))) - .collect::>() - .join("&"); + let mut query_string = String::new(); + let mut first = true; + for (k, v) in &query_params { + if !first { + query_string.push('&'); + } + first = false; + query_string.push_str(&format!("{}={}", k, encode(v))); + } url.push('?'); url.push_str(&query_string); } @@ -95,7 +119,6 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream { let response = request.send().await?; let bytes = response.body().await?; - // Deserialize into associated R type let parsed: Self::R = serde_json::from_slice(&bytes)?; Ok(parsed) }