envoke_derive 0.1.3

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

use crate::{attr::ContainerAttributes, Field};

pub fn get_fields(data: Data) -> FieldsNamed {
    match data {
        Data::Struct(s) => match s.fields {
            Fields::Named(fields) => fields,
            _ => panic!("error: fill can only be used for named fields"),
        },
        _ => panic!("error: fill can can only be derived for structs"),
    }
}

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

fn get_inner_types(ty: &Type) -> Option<Vec<&Type>> {
    match ty {
        Type::Path(path) => match path.path.segments.get(0) {
            Some(segment) => match &segment.arguments {
                PathArguments::AngleBracketed(args) => {
                    let inners = args
                        .args
                        .iter()
                        .filter_map(|e| match e {
                            GenericArgument::Type(ty) => Some(ty),
                            _ => None,
                        })
                        .collect();

                    Some(inners)
                }
                _ => None,
            },
            None => None,
        },
        _ => None,
    }
}

pub fn default_call(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 mut call = match &field.attrs.default {
        Some(default) => match default {
            crate::attr::DefaultValue::Type(ty) => {
                quote! { <#ty>::default() }
            }
            crate::attr::DefaultValue::Path(path) => {
                quote! { #path }
            }
            crate::attr::DefaultValue::Lit(lit) => {
                quote! {
                    #lit.try_into().map_err(|_| envoke::Error::ConvertError {
                        field: #ident.to_string(),
                        ty: #ty.to_string()
                    })?
                }
            }
            crate::attr::DefaultValue::Call { path, args } => {
                quote! { #path(#(#args),*) }
            }
        },
        None => quote! { panic!("fatal error occurred") },
    };

    let is_optional = is_optional(&field.ty);
    if is_optional {
        call = quote! { Some(#call) }
    }

    call
}
pub fn env_call(attrs: &ContainerAttributes, field: &Field) -> proc_macro2::TokenStream {
    if let Some(envs) = &field.attrs.envs {
        let ident = &field.ident;
        let ident = quote! { #ident }.to_string();

        let ty = match (&field.attrs.parse_fn.is_some(), &field.attrs.arg_type) {
            (true, None) => {
                panic!("field attribute `arg_type` is required if `parse_fn` is specified")
            }
            (true, Some(ty)) => ty,
            (false, _) => &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 => {
                let inner_types = get_inner_types(ty).unwrap();
                quote! { <envoke::Envloader<#ty> as FromSingleOpt<(#(#inner_types),*)>>::load_once(&[#(#envs),*], #delim) }
            }
            false => {
                quote! { envoke::Envloader::<#ty>::load_once(&[#(#envs),*], #delim) }
            }
        };

        let mut call = match field.attrs.default.is_some() {
            true => {
                let default_call = default_call(field);
                quote! {
                    {
                        match #base_call {
                            Ok(value) => value,
                            Err(_) => #default_call,
                        }
                    }
                }
            }
            false => {
                quote! {
                    { #base_call? }
                }
            }
        };

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

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

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

        call
    } else {
        quote! {
            panic!("fatal error occurred")
        }
    }
}