api_req_derive 0.5.2

Derive macro for api_req
Documentation
use proc_macro::TokenStream;
use quote::{ToTokens as _, quote};
use regex::Regex;
use syn::{
    DeriveInput, Error, Expr, ExprArray, ExprTuple, Ident, LitStr, parse_macro_input, parse2,
    spanned::Spanned,
};

pub(crate) fn derive_payload(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let mut path: Expr = syn::parse_str("String::new()").unwrap();
    let mut method: Expr = syn::parse_str("::api_req::Method::GET").unwrap();
    let mut headers_key: Vec<Expr> = vec![];
    let mut headers_value: Vec<Expr> = vec![];
    let mut req: Ident = syn::parse_str("query").unwrap();
    let mut before_deserialize: Option<Expr> = None;
    let mut deserialize: Option<Expr> = None;
    let re = Regex::new(r"\{(\w+)\}").unwrap();

    if let Some(attr) = input
        .attrs
        .iter()
        .find(|&attr| attr.path().is_ident("api_req"))
        && let Err(e) = attr.parse_nested_meta(|meta| {
            match &meta.path {
                item if item.is_ident("path") => {
                    let value = meta.value()?;
                    let p = value.parse::<LitStr>()?;
                    let format_args = re
                        .captures_iter(&p.value())
                        .map(|c| c.extract::<1>())
                        .map(|(_, cap)| cap[0].to_string())
                        .collect::<Vec<_>>();
                    let p = format_args
                        .into_iter()
                        .fold(format!(r#""{}""#, p.value()), |acc, x| {
                            format!("{acc}, {x}=self.{x}")
                        });
                    let p = format!("format!({p})");
                    path = syn::parse_str(&p)?;
                }
                item if item.is_ident("method") => {
                    let value = meta.value()?;
                    method = value.parse()?;
                }
                item if item.is_ident("headers") => {
                    let value = meta.value()?;
                    let kvs: ExprArray = value.parse()?;
                    for kv_expr in kvs.elems {
                        let kv: ExprTuple = parse2(kv_expr.to_token_stream())?;
                        let mut kv = kv.elems.into_iter().collect::<Vec<_>>();
                        if kv.len() != 2 {
                            Err(Error::new(kv_expr.span(), format!(
                                "(header_key, header_value) expected, which contains 2 elems, but got {} elems", kv.len()
                            )))?
                        }
                        headers_key.push(kv.remove(0));
                        let mut value = kv.remove(0);
                        if let Ok(v) = parse2::<LitStr>(value.to_token_stream()) {
                            let format_args = re
                                .captures_iter(&v.value())
                                .map(|c| c.extract::<1>())
                                .map(|(_, cap)| cap[0].to_string())
                                .collect::<Vec<_>>();
                            let v = format_args
                                .into_iter()
                                .fold(format!(r#""{}""#, v.value()), |acc, x| {
                                    format!("{acc}, {x}=self.{x}")
                                });
                            let v = format!("format!({v})");
                            value = syn::parse_str(&v)?;
                        }
                        headers_value.push(value);
                    }
                }
                item if item.is_ident("req") => {
                    let value = meta.value()?;
                    req = value.parse()?;
                }
                item if item.is_ident("before_deserialize") => {
                    let value = meta.value()?;
                    before_deserialize = Some(value.parse()?);
                }
                item if item.is_ident("deserialize") => {
                    let value = meta.value()?;
                    deserialize = Some(value.parse()?);
                }
                item => Err(Error::new(item.span(), "unsupported meta"))?,
            }
            Ok(())
        })
    {
        return e.to_compile_error().into();
    }

    let headers = if headers_key.is_empty() {
        quote! { None }
    } else {
        quote! {
            let mut headers = ::api_req::header::HeaderMap::new();
            #(
                headers.insert(#headers_key, #headers_value.parse().unwrap());
            )*
            Some(headers)
        }
    };

    let before_deserialize_expanded = match before_deserialize {
        Some(expr) => quote! {
            Some(#expr.map_err(|e| ::api_req::error::ApiErr::Other(format!("Before deserialize failed:\n{}", e))))
        },
        None => quote! {
            None
        },
    };

    let deserialize_expanded = match deserialize {
        Some(expr) => quote! { #expr },
        None => quote! { ::api_req::__serde_json::from_str },
    };

    let expanded = quote! {
        impl #impl_generics ::api_req::Payload for #name #ty_generics #where_clause {
            const METHOD: ::api_req::Method = #method;

            fn headers(&self) -> Option<::api_req::header::HeaderMap> {
                #headers
            }

            fn path(&self) -> Option<String> {
                Some(#path)
            }

            fn req_option(&self, mut req: ::api_req::RequestBuilder) -> ::api_req::RequestBuilder {
                if let Some(headers) = self.headers() {
                    req = req.headers(headers);
                }
                req.#req(self)
            }

            fn before_deserialize() -> Option<fn(String) -> ::api_req::error::ApiResult<String>> {
                #before_deserialize_expanded
            }

            fn deserialize<O: ::api_req::__serde::de::DeserializeOwned>(input: String) -> ::api_req::error::ApiResult<O> {
                #deserialize_expanded(&input).map_err(|e| ::api_req::error::ApiErr::UnDeserializeable(e.to_string()))
            }
        }
    };

    TokenStream::from(expanded)
}