clientix-codegen 0.2.0

Library for building HTTP clients and requests declaratively with procedural macros - no need to write complex imperative or functional logic.
Documentation
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{Attribute, Meta, Signature, TraitItem};
use syn::__private::TokenStream2;
use clientix_core::prelude::reqwest::header::{ACCEPT, CONTENT_TYPE};
use clientix_core::prelude::reqwest::Method;
use crate::attributes::method::MethodAttributes;
use crate::method::header::HeaderCompiler;
use crate::method::signature::SignatureCompiler;

const GET_METHOD_MACRO: &str = "get";
const POST_METHOD_MACRO: &str = "post";
const PUT_METHOD_MACRO: &str = "put";
const DELETE_METHOD_MACRO: &str = "delete";
const HEAD_METHOD_MACRO: &str = "head";
const PATCH_METHOD_MACRO: &str = "patch";
const HEADER_METHOD_MACRO: &str = "header";


#[derive(Clone, Debug)]
pub struct MethodCompiler {
    macros: Vec<Attribute>,
    headers: Vec<HeaderCompiler>,
    signature: SignatureCompiler,
    attributes: MethodAttributes,
    dry_run: bool
}

impl MethodCompiler {

    pub fn new(signature: Signature, attributes: Vec<Attribute>, async_supported: bool) -> Self {
        let mut macros = Vec::new();
        let mut headers = Vec::new();
        let mut method_attributes = None;

        attributes.iter()
            .map(|attr_expr| match &attr_expr.meta {
                Meta::Path(value) => (value, TokenStream2::new(), attr_expr),
                Meta::List(value) => (&value.path, value.tokens.to_token_stream(), attr_expr),
                Meta::NameValue(value) => (&value.path, TokenStream2::new(), attr_expr),
            })
            .for_each(|(path, attrs, attr_expr)| {
                match path {
                    ref path if path.is_ident(HEADER_METHOD_MACRO) => {
                        headers.push(HeaderCompiler::parse(attrs, false));
                        macros.insert(0, attr_expr.clone());
                    },
                    ref path if path.is_ident(GET_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::GET, attrs, false));
                        macros.push(attr_expr.clone());
                    }
                    ref path if path.is_ident(POST_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::POST, attrs, false));
                        macros.push(attr_expr.clone());
                    }
                    ref path if path.is_ident(PUT_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::PUT, attrs, false));
                        macros.push(attr_expr.clone());
                    },
                    ref path if path.is_ident(DELETE_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::DELETE, attrs, false));
                        macros.push(attr_expr.clone());
                    },
                    ref path if path.is_ident(HEAD_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::HEAD, attrs, false));
                        macros.push(attr_expr.clone());
                    },
                    ref path if path.is_ident(PATCH_METHOD_MACRO) => {
                        method_attributes = Some(MethodAttributes::parse(Method::PATCH, attrs, false));
                        macros.push(attr_expr.clone());
                    },
                    _ => {},
                };
            });

        if let Some(attributes) = method_attributes {
            let signature = SignatureCompiler::parse(signature, async_supported, attributes.produces().cloned(), false);

            Self {
                macros,
                headers,
                signature,
                attributes,
                dry_run: false
            }
        } else {
            panic!("not valid macro");
        }
    }

    pub fn parse(method: Method, item: TokenStream, attrs: TokenStream) -> Self {
        match syn::parse2(TokenStream2::from(item)) {
            Ok(TraitItem::Fn(item)) => {
                let attributes = MethodAttributes::parse(method, TokenStream2::from(attrs), true);
                let signature = SignatureCompiler::parse(item.sig, false, attributes.produces().cloned(), true);

                Self {
                    macros: vec![],
                    headers: vec![],
                    signature,
                    attributes,
                    dry_run: false,
                }
            }
            Ok(_) => panic!("unsupported item"),
            Err(err) => panic!("{}", err.to_string().as_str()),
        }
    }

    pub fn compile_declaration(&self) -> TokenStream2 {
        let attributes = &self.macros;
        let signature =  self.signature.signature();

        if self.dry_run {
            quote!(#signature;)
        } else {
            quote! {
                #(#attributes)*
                #[allow(async_fn_in_trait)]
                #signature;
            }
        }
    }

    pub fn compile_definition(&self) -> TokenStream2 {
        let sig = self.signature.signature();

        let compiled_method = self.compile_method();
        let compiled_path = self.compile_path();
        let compiled_headers = self.compile_headers();
        let compiled_queries = self.compile_queries();
        let compiled_body = self.compile_body();
        let compiled_result = self.compile_output();

        quote! {
            pub #sig {
                use clientix::client::request::ClientixRequestBuilder;

                #compiled_method
                #compiled_path
                #compiled_headers
                #compiled_queries
                #compiled_body
                #compiled_result
            }
        }
    }

    fn compile_method(&self) -> TokenStream2 {
        TokenStream2::from(match *self.attributes.method() {
            Method::GET => quote!(let builder = self.client.get();),
            Method::POST => quote!(let builder = self.client.post();),
            Method::PUT => quote!(let builder = self.client.put();),
            Method::DELETE => quote!(let builder = self.client.delete();),
            Method::HEAD => quote!(let builder = self.client.head();),
            Method::PATCH => quote!(let builder = self.client.patch();),
            _ => panic!("missing method type")
        })
    }

    fn compile_path(&self) -> TokenStream2 {
        self.signature.compile_segments(self.attributes.path())
    }

    fn compile_headers(&self) -> TokenStream2 {
        let mut stream = self.signature.compile_headers();

        for header in &self.headers {
            stream.extend(header.compile(self.signature.segments()))
        }

        if let Some(content_type) = self.attributes.consumes() {
            stream.extend(HeaderCompiler::new(Some(CONTENT_TYPE.to_string()), Some(content_type.to_string()), false).compile(self.signature.segments()));
        }

        if let Some(accept_type) = self.attributes.produces() {
            stream.extend(HeaderCompiler::new(Some(ACCEPT.to_string()), Some(accept_type.to_string()), false).compile(self.signature.segments()));
        }

        stream
    }

    fn compile_queries(&self) -> TokenStream2 {
        self.signature.compile_queries()
    }

    fn compile_body(&self) -> TokenStream2 {
        self.signature.compile_body(self.attributes.consumes().cloned())
    }

    fn compile_output(&self) -> TokenStream2 {
        self.signature.compile_output()
    }

}