envoke_derive 0.1.6

A proc macro for loading environment variables into struct fields automatically.
Documentation
use convert_case::Casing;
use proc_macro2::Span;
use quote::quote;
use syn::{Data, Fields, FieldsNamed, Type};

use crate::{
    attr::{ContainerAttributes, DefaultValue},
    errors::Error,
    Field,
};

pub fn get_fields(span: &Span, data: Data) -> syn::Result<FieldsNamed> {
    match data {
        Data::Struct(s) => match s.fields {
            Fields::Named(fields) => Ok(fields),
            _ => Err(Error::UnsupportedStructType.to_syn_error(*span)),
        },
        _ => Err(Error::UnsupportedTarget.to_syn_error(*span)),
    }
}

pub fn is_optional(ty: &Type) -> bool {
    match ty {
        Type::Path(path) => path.path.segments[0].ident == "Option",
        _ => false,
    }
}

pub fn default_call(default: &DefaultValue, field: &Field) -> proc_macro2::TokenStream {
    let ident = &field.ident;
    let ident = quote! { #ident }.to_string();

    let ty = &field.ty;
    let ty = quote! { #ty }.to_string();

    let is_optional = is_optional(&field.ty);
    match default {
        DefaultValue::Type(ty) => {
            quote! { <#ty>::default() }
        }
        DefaultValue::Path(path) => {
            let mut call = quote! { #path };
            if is_optional {
                call = quote! { Some(#call) }
            }

            call
        }
        DefaultValue::Lit(lit) => {
            let mut call = quote! {
                #lit.try_into().map_err(|_| envoke::Error::ConvertError {
                    field: #ident.to_string(),
                    ty: #ty.to_string()
                })?
            };
            if is_optional {
                call = quote! { Some(#call) }
            }

            call
        }
        DefaultValue::Call { path, args } => {
            let mut call = quote! { #path(#(#args),*) };
            if is_optional {
                call = quote! { Some(#call) }
            }

            call
        }
    }
}

fn process_call(field: &Field) -> proc_macro2::TokenStream {
    let ident = &field.ident;
    let ident = quote! { #ident }.to_string();
    let mut call = quote! {};

    if let Some(validate_fn) = &field.attrs.validate_fn.before {
        call = quote! {
            #validate_fn(&value).map_err(|e| envoke::Error::ValidationError {
                field: #ident.to_string(),
                err: e.into()
            })?;
        };
    }

    if let Some(parse_fn) = &field.attrs.parse_fn {
        call = quote! {
            #call
            let value = #parse_fn(value);
        }
    }

    if let Some(validate_fn) = &field.attrs.validate_fn.after {
        call = quote! {
            #call
            #validate_fn(&value).map_err(|e| envoke::Error::ValidationError {
                field: #ident.to_string(),
                err: e.into()
            })?;
        };
    }

    call
}

pub fn env_call(
    envs: &Vec<String>,
    attrs: &ContainerAttributes,
    field: &Field,
) -> proc_macro2::TokenStream {
    let ty = match (&field.attrs.parse_fn.is_some(), &field.attrs.arg_type) {
        (true, Some(ty)) => ty,
        _ => &field.ty,
    };

    let delim = attrs.get_delimiter();
    let prefix = if !field.attrs.no_prefix {
        format!("{}{delim}", attrs.get_prefix())
    } else {
        String::new()
    };

    let suffix = if !field.attrs.no_suffix {
        format!("{delim}{}", attrs.get_suffix())
    } else {
        String::new()
    };

    let envs: Vec<String> = envs
        .iter()
        .map(|e| format!("{prefix}{e}{suffix}"))
        .map(|env| {
            attrs
                .rename_all
                .as_ref()
                .map_or(env.clone(), |case| env.to_case(case.into()))
        })
        .collect();

    let delim = field.attrs.delimiter.as_deref().unwrap_or(",");
    let is_optional = is_optional(ty);
    let base_call = match is_optional {
        true => {
            quote! { envoke::OptEnvloader::<#ty>::load_once(&[#(#envs),*], #delim) }
        }
        false => {
            quote! { envoke::Envloader::<#ty>::load_once(&[#(#envs),*], #delim) }
        }
    };

    let process_call = process_call(field);
    match &field.attrs.default {
        Some(default) => {
            let default_call = default_call(default, field);
            quote! {
                {
                    match #base_call {
                        Ok(value) => {
                            #process_call
                            value
                        },
                        Err(_) => #default_call,
                    }
                }
            }
        }
        None => quote! {
            {
                let value = #base_call?;
                #process_call
                value
            }
        },
    }
}