nicely_macros 0.2.0

A macro crate for the ask_nicely crate.
Documentation
use darling::FromMeta;
use syn::{self, DeriveInput};

pub(crate) fn gen_client_impl(
    attr: proc_macro::TokenStream,
    input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let attr_args = syn::parse_macro_input!(attr as AttributeArg);

    if let AttributeArg::Ident(env_var) = &attr_args {
        if std::env::var(env_var.to_string()).is_err() {
            return syn::Error::new(
                env_var.span(),
                format!(
                    "Can't find .env file to load {}.\nPlace a .env file in project path.",
                    env_var
                ),
            )
            .into_compile_error()
            .into();
        }
    }

    // Parse the annotated item (struct or enum)
    let DeriveInput { ident, data: _, .. } = syn::parse_macro_input!(input as DeriveInput);

    let mut builder_ident_str = ident.to_string();

    builder_ident_str.push_str("Builder");

    let builder_ident = syn::Ident::from_string(&builder_ident_str).unwrap();

    let base_url_func = match attr_args {
        AttributeArg::BaseUrl(url) => {
            let url = url.value();
            quote::quote! { std::borrow::Cow::Borrowed(#url) }
        },
        AttributeArg::Ident(ident) => {
            let key = ident.to_string();

            match std::env::var(key.trim()) {
                Ok(base_url) => {
                    if url::Url::parse(&base_url).is_err() {
                        return ::syn::Error::new(
                            ident.span(),
                            "Invalid Url: Not a valid BaseUrl.",
                        )
                        .into_compile_error()
                        .into();
                    }
                }
                Err(_) => {
                    return syn::Error::new(ident.span(), format!("Environment variable {} should be present in a .env file for compile time validation.", key))
                        .into_compile_error()
                        .into();
                }
            }

            quote::quote! {
                let value = std::env::var(#key).unwrap();
                std::borrow::Cow::Owned(value)
            }
        }
    };

    quote::quote! {

        #[derive(Debug, Clone)]
        pub struct #ident<'a> {
            auth: ask_nicely::authentication::Authentication<'a>,
            client: reqwest::Client
        }

        #[derive(Debug)]
        pub struct #builder_ident<'a> {
            auth: ask_nicely::authentication::Authentication<'a>,
            builder: reqwest::ClientBuilder
        }

        impl<'a> #builder_ident<'a> {
            pub fn set_auth(mut self, auth: ask_nicely::authentication::Authentication<'a>) -> Self {
                self.auth = auth;
                self
            }

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

            pub fn build(self) -> reqwest::Result<#ident<'a>> {
                let #builder_ident { auth, builder } = self;

                let client = builder.build()?;

                Ok(#ident {
                    auth: auth.clone(),
                    client
                })
            }

        }

        impl<'a> #ident<'a> {
            pub fn new(auth: ask_nicely::authentication::Authentication<'a>) -> #ident<'a> {
                #ident {
                    auth,
                    client: reqwest::Client::new()
                }
            }

            pub fn builder() -> #builder_ident<'a> {
                #builder_ident {
                    auth: ask_nicely::authentication::Authentication::None,
                    builder: reqwest::ClientBuilder::new()
                }
            }

            fn base_url(&'a self) -> std::borrow::Cow<'a, str> {
                #base_url_func
            }

            pub fn set_auth(&'a mut self, auth: ask_nicely::authentication::Authentication<'a>) -> &'a mut Self {
                self.auth = auth;
                self
            }

            pub fn set_client(&mut self, new_client: reqwest::Client) {
                self.client = new_client;
            }
        }

        impl<'a> ask_nicely::client::CanApiClient<'a> for #ident<'a> {
            fn get_auth(&'a self) -> &'a ask_nicely::authentication::Authentication<'a> {
                &self.auth
            }

            fn get_base_url(&self) -> String {
                self.base_url().into_owned()
            }

            fn get_client(&'a self) -> &'a reqwest::Client {
                &self.client
            }
        }

    }
    .into()
}

enum AttributeArg {
    BaseUrl(syn::LitStr),
    Ident(syn::Ident),
}

impl syn::parse::Parse for AttributeArg {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let peeked = input.lookahead1();
        if peeked.peek(syn::Ident) {
            Ok(AttributeArg::Ident(input.parse()?))
        } else if peeked.peek(syn::LitStr) {
            Ok(AttributeArg::BaseUrl(input.parse()?))
        } else {
            Err(syn::Error::new(input.span(), "Expected baseurl  or env variable name  \nSamples:\n\t \"http(s)://baseurl.com\" or BASE_URL"))
        }
    }
}