http-provider-macro 0.1.4

A procedural macro for generating type-safe API clients
Documentation
use crate::{
    error::{MacroError, MacroResult},
    input::{ApiClientInput, AuthStrategy},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;

pub mod error;
pub mod interface;
pub mod method;

pub use error::ErrorExpander;
pub use interface::TraitExpander;
pub use method::MethodExpander;

pub struct ApiClientExpander {
    input: ApiClientInput,
}

impl ApiClientExpander {
    pub fn new(input: ApiClientInput) -> Self {
        Self { input }
    }

    pub fn expand(&self) -> MacroResult<TokenStream> {
        self.validate()?;

        let struct_name = &self.input.struct_name;
        let error_name = Ident::new(&format!("{}Error", struct_name), struct_name.span());

        let error_type = ErrorExpander::new(&error_name).expand();
        let trait_def = self.expand_trait_def(&error_name)?;
        let methods = self.expand_methods(&error_name)?;
        let struct_impl = self.expand_struct_impl(&methods);

        Ok(quote! {
            #error_type
            #trait_def
            #struct_impl
        })
    }

    fn expand_trait_def(&self, error_name: &Ident) -> MacroResult<TokenStream> {
        let trait_name = self.trait_name();
        TraitExpander::new(&self.input.endpoints, &trait_name, error_name).expand()
    }

    fn expand_methods(&self, error_name: &Ident) -> MacroResult<Vec<TokenStream>> {
        let global_retries = self.input.global_retries.unwrap_or(0);
        let auth = self.input.auth.as_ref();
        self.input
            .endpoints
            .iter()
            .map(|def| {
                let retry_count = def.retries.unwrap_or(global_retries);
                MethodExpander::new(def, error_name, retry_count, auth).expand()
            })
            .collect()
    }

    fn expand_struct_impl(&self, methods: &[TokenStream]) -> TokenStream {
        let struct_name = &self.input.struct_name;
        let trait_name = self.trait_name();

        let (auth_fields, auth_params, auth_inits) = match &self.input.auth {
            Some(AuthStrategy::Bearer) => (
                quote! { token: String, },
                quote! { token: &str, },
                quote! { token: token.to_string(), },
            ),
            Some(AuthStrategy::Basic) => (
                quote! { username: String, password: String, },
                quote! { username: &str, password: &str, },
                quote! { username: username.to_string(), password: password.to_string(), },
            ),
            Some(AuthStrategy::ApiKey(_)) => (
                quote! { api_key: String, },
                quote! { api_key: &str, },
                quote! { api_key: api_key.to_string(), },
            ),
            None => (quote! {}, quote! {}, quote! {}),
        };

        quote! {
            pub struct #struct_name {
                url: reqwest::Url,
                client: reqwest::Client,
                timeout: std::time::Duration,
                #auth_fields
            }

            impl #struct_name {
                pub fn new(url: reqwest::Url, #auth_params timeout: Option<u64>) -> Self {
                    let client = reqwest::Client::new();
                    let timeout = std::time::Duration::from_millis(timeout.unwrap_or(5000));
                    Self { url, client, timeout, #auth_inits }
                }
            }

            impl #trait_name for #struct_name {
                #(#methods)*
            }
        }
    }

    fn trait_name(&self) -> Ident {
        Ident::new(
            &format!("{}Trait", self.input.struct_name),
            self.input.struct_name.span(),
        )
    }

    fn validate(&self) -> MacroResult<()> {
        if self.input.endpoints.is_empty() {
            return Err(MacroError::NoEndpointsConfigured {
                span: self.input.struct_name.span(),
            });
        }
        Ok(())
    }
}