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;
use syn::{Ident, ItemTrait, TraitItem, Visibility};
use syn::__private::{Span, TokenStream2};
use crate::attributes::client::ClientAttributes;
use crate::method::MethodCompiler;

#[derive(Clone, Debug)]
pub struct ClientCompiler {
    ident: Ident,
    visibility: Visibility,
    attributes: ClientAttributes,
    methods: Vec<MethodCompiler>
}

impl ClientCompiler {

    pub fn parse(item: TokenStream2, attrs: TokenStream2) -> Self {
        let attributes = ClientAttributes::parse(attrs);

        let item: ItemTrait = match syn::parse2(item) {
            Ok(input) => input,
            Err(err) => panic!("{}", err)
        };

        let ident = item.ident;
        let visibility = item.vis;

        let methods = item.items.into_iter()
            .filter_map(|item| match item {
                TraitItem::Fn(fn_item) => Some(fn_item),
                _ => None
            })
            .map(|item| MethodCompiler::new(item.sig, item.attrs, attributes.async_supported()))
            .collect::<Vec<_>>();

        Self { ident, visibility, attributes, methods }
    }

    pub fn compile(&self) -> TokenStream2 {
        let compiled_interface = self.compile_interface();
        let compiled_builder = self.compile_builder();
        let compiled_client = self.compile_client();

        TokenStream2::from(quote! {
            #compiled_interface
            #compiled_builder
            #compiled_client
        })
    }

    fn compile_interface(&self) -> TokenStream2 {
        let client_interface_name = Ident::new(&format!("{}{}", &self.ident, "Interface"), Span::call_site());
        let client_interface_declarations_fn = self.methods.iter()
            .map(|method| method.compile_declaration())
            .collect::<Vec<_>>();

        TokenStream2::from(quote! {
            trait #client_interface_name {
                #(#client_interface_declarations_fn)*
            }
        })
    }

    fn compile_builder(&self) -> TokenStream2 {
        let client_struct_name = &self.ident;
        let client_visibility = &self.visibility;
        let client_builder_name = Ident::new(&format!("{}{}", &self.ident, "Builder"), Span::call_site());
        let client_url = if let Some(url) = self.attributes.url() { quote!(.url(#url)) } else { quote!() };
        let client_path = if let Some(path) = self.attributes.path() { quote!(.path(#path)) } else { quote!() };
        let client_type_method = if self.attributes.async_supported() { quote!(asynchronous()) } else { quote!(blocking()) };

        TokenStream2::from(quote! {
            #client_visibility struct #client_builder_name {
                clientix_builder: clientix::client::ClientixBuilder
            }

            impl #client_builder_name {
                pub fn new() -> Self {
                    let clientix_builder = clientix::client::Clientix::builder()
                        #client_url
                        #client_path;

                    Self { clientix_builder }
                }

                pub fn url(mut self, url: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.url(url);
                    self
                }

                pub fn path(mut self, path: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.path(path);
                    self
                }

                pub fn user_agent(mut self, user_agent: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.user_agent(user_agent);
                    self
                }

                pub fn header(mut self, key: &str, value: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.header(key, value, false);
                    self
                }

                pub fn basic_auth(mut self, username: &str, password: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.basic_auth(username, password);
                    self
                }
                
                pub fn bearer_auth(mut self, token: &str) -> Self {
                    self.clientix_builder = self.clientix_builder.bearer_auth(token);
                    self
                }
                
                pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
                    self.clientix_builder = self.clientix_builder.headers(headers);
                    self
                }

                pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
                    self.clientix_builder = self.clientix_builder.timeout(timeout);
                    self
                }

                pub fn read_timeout(mut self, read_timeout: std::time::Duration) -> Self {
                    self.clientix_builder = self.clientix_builder.read_timeout(read_timeout);
                    self
                }

                pub fn connect_timeout(mut self, connect_timeout: std::time::Duration) -> Self {
                    self.clientix_builder = self.clientix_builder.connect_timeout(connect_timeout);
                    self
                }

                pub fn connection_verbose(mut self, connection_verbose: bool) -> Self {
                    self.clientix_builder = self.clientix_builder.connection_verbose(connection_verbose);
                    self
                }

                pub fn setup(self) -> #client_struct_name {
                    let clientix = self.clientix_builder.build();

                    #client_struct_name {
                        client: clientix.#client_type_method,
                        config: clientix.config().clone()
                    }
                }
            }
        })
    }

    fn compile_client(&self) -> TokenStream2 {
        let client_struct_name = &self.ident;
        let client_visibility = &self.visibility;
        let client_builder_name = Ident::new(&format!("{}{}", &self.ident, "Builder"), Span::call_site());

        let client_type = TokenStream2::from(if self.attributes.async_supported() {
            quote! {clientix::client::asynchronous::AsyncClient}
        } else {
            quote! {clientix::client::blocking::BlockingClient}
        });

        let client_definitions = self.methods.iter()
            .map(|method| method.compile_definition())
            .collect::<Vec<_>>();

        TokenStream2::from(quote! {
            #client_visibility struct #client_struct_name {
                client: #client_type,
                config: clientix::client::ClientConfig
            }

            impl #client_struct_name {
                pub fn config() -> #client_builder_name {
                    #client_builder_name::new()
                }

                pub fn new() -> Self {
                    #client_struct_name::config().setup()
                }
            }

            impl #client_struct_name {
                #(#client_definitions)*
            }
        })
    }

}

pub fn parse_client(item: TokenStream, attrs: TokenStream) -> TokenStream {
    let client_compiler = ClientCompiler::parse(TokenStream2::from(item), TokenStream2::from(attrs));
    let compiled_client = client_compiler.compile();

    TokenStream::from(quote!(#compiled_client))
}