use proc_macro::TokenStream;
use quote::quote;
use syn::{
punctuated::Punctuated, token::Comma, Attribute, Data::Struct, DeriveInput, Expr, Field,
Fields, Ident, Lit, Meta, MetaNameValue, Token,
};
#[proc_macro_derive(Envconfig, attributes(envconfig))]
pub fn derive(input: TokenStream) -> TokenStream {
let derive_input: DeriveInput = syn::parse(input).unwrap();
let gen = impl_envconfig(&derive_input);
gen.into()
}
enum Source {
Environment,
HashMap,
}
fn impl_envconfig(input: &DeriveInput) -> proc_macro2::TokenStream {
let struct_name = &input.ident;
let named_fields = match input.data {
Struct(ref ds) => match ds.fields {
Fields::Named(ref fields) => &fields.named,
_ => panic!("envconfig supports only named fields"),
},
_ => panic!("envconfig only supports non-tuple structs"),
};
let inner_impl = impl_envconfig_for_struct(struct_name, named_fields);
quote!(#inner_impl)
}
fn impl_envconfig_for_struct(
struct_name: &Ident,
fields: &Punctuated<Field, Comma>,
) -> proc_macro2::TokenStream {
let field_assigns_env = fields
.iter()
.map(|field| gen_field_assign(field, &Source::Environment));
let field_assigns_hashmap = fields
.iter()
.map(|field| gen_field_assign(field, &Source::HashMap));
quote! {
impl Envconfig for #struct_name {
fn init_from_env() -> ::std::result::Result<Self, ::envconfig::Error> {
let config = Self {
#(#field_assigns_env,)*
};
::core::result::Result::Ok(config)
}
fn init_from_hashmap(hashmap: &::std::collections::HashMap<String, String>) -> ::std::result::Result<Self, ::envconfig::Error> {
let config = Self {
#(#field_assigns_hashmap,)*
};
::core::result::Result::Ok(config)
}
#[deprecated(since="0.10.0", note="Please use `::init_from_env` instead")]
fn init() -> ::std::result::Result<Self, ::envconfig::Error> {
Self::init_from_env()
}
}
}
}
fn gen_field_assign(field: &Field, source: &Source) -> proc_macro2::TokenStream {
let attr = fetch_envconfig_attr_from_field(field);
if let Some(attr) = attr {
let list = fetch_args_from_attr(field, attr);
let nested_value_opt = find_item_in_list(&list, "nested");
match nested_value_opt {
Some(MatchingItem::NoValue) => return gen_field_assign_for_struct_type(field, source),
Some(MatchingItem::WithValue(_)) => {
panic!("`nested` attribute must not have a value")
}
None => {}
}
let opt_default = match find_item_in_list(&list, "default") {
Some(MatchingItem::WithValue(v)) => Some(v),
Some(MatchingItem::NoValue) => panic!("`default` attribute must have a value"),
None => None,
};
let from_opt = find_item_in_list(&list, "from");
let env_var = match from_opt {
Some(MatchingItem::WithValue(v)) => quote! { #v },
Some(MatchingItem::NoValue) => panic!("`from` attribute must have a value"),
None => field_to_env_var_name(field),
};
gen(field, &env_var, opt_default, source)
} else {
let env_var = field_to_env_var_name(field);
gen(field, &env_var, None, source)
}
}
fn field_to_env_var_name(field: &Field) -> proc_macro2::TokenStream {
let field_name = field.ident.clone().unwrap().to_string().to_uppercase();
quote! { #field_name }
}
fn gen(
field: &Field,
from: &proc_macro2::TokenStream,
opt_default: Option<&Lit>,
source: &Source,
) -> proc_macro2::TokenStream {
let field_type = &field.ty;
if to_s(field_type).starts_with("Option ") {
gen_field_assign_for_optional_type(field, from, opt_default, source)
} else {
gen_field_assign_for_non_optional_type(field, from, opt_default, source)
}
}
fn gen_field_assign_for_struct_type(field: &Field, source: &Source) -> proc_macro2::TokenStream {
let ident: &Option<Ident> = &field.ident;
match &field.ty {
syn::Type::Path(path) => match source {
Source::Environment => quote! {
#ident: #path :: init_from_env()?
},
Source::HashMap => quote! {
#ident: #path :: init_from_hashmap(hashmap)?
},
},
_ => panic!("Expected field type to be a path: {ident:?}",),
}
}
fn gen_field_assign_for_optional_type(
field: &Field,
from: &proc_macro2::TokenStream,
opt_default: Option<&Lit>,
source: &Source,
) -> proc_macro2::TokenStream {
let field_name = &field.ident;
assert!(opt_default.is_none(), "Optional type on field `{}` with default value does not make sense and therefore is not allowed", to_s(field_name));
match source {
Source::Environment => quote! {
#field_name: ::envconfig::load_optional_var::<_,::std::collections::hash_map::RandomState>(#from, None)?
},
Source::HashMap => quote! {
#field_name: ::envconfig::load_optional_var::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap))?
},
}
}
fn gen_field_assign_for_non_optional_type(
field: &Field,
from: &proc_macro2::TokenStream,
opt_default: Option<&Lit>,
source: &Source,
) -> proc_macro2::TokenStream {
let field_name = &field.ident;
if let Some(default) = opt_default {
match source {
Source::Environment => quote! {
#field_name: ::envconfig::load_var_with_default::<_,::std::collections::hash_map::RandomState>(#from, None, #default)?
},
Source::HashMap => quote! {
#field_name: ::envconfig::load_var_with_default::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap), #default)?
},
}
} else {
match source {
Source::Environment => quote! {
#field_name: ::envconfig::load_var::<_,::std::collections::hash_map::RandomState>(#from, None)?
},
Source::HashMap => quote! {
#field_name: ::envconfig::load_var::<_,::std::collections::hash_map::RandomState>(#from, Some(hashmap))?
},
}
}
}
fn fetch_envconfig_attr_from_field(field: &Field) -> Option<&Attribute> {
field.attrs.iter().find(|a| {
let path = &a.path();
let name = quote!(#path).to_string();
name == "envconfig"
})
}
fn fetch_args_from_attr(field: &Field, attr: &Attribute) -> Vec<Meta> {
let opt_meta = &attr.meta;
match opt_meta {
Meta::List(l) => l
.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
.unwrap_or_else(|err| {
panic!(
"{:?} in `envconfig` attribute on field `{}`",
err,
field_name(field)
)
})
.iter()
.cloned()
.collect(),
_ => vec![opt_meta.clone()],
}
}
enum MatchingItem<'a> {
WithValue(&'a Lit),
NoValue,
}
fn find_item_in_list<'l>(list: &'l [Meta], item_name: &str) -> Option<MatchingItem<'l>> {
let matching_items = list
.iter()
.filter(|token_tree| token_tree.path().is_ident(item_name))
.collect::<Vec<_>>();
assert!(
matching_items.len() <= 1,
"Found multiple `{item_name}` attributes in `envconfig` attribute",
);
let matching_result = matching_items.first();
if let Some(meta) = matching_result {
return match meta {
Meta::NameValue(MetaNameValue {
value: Expr::Lit(value),
..
}) => Some(MatchingItem::WithValue(&value.lit)),
Meta::Path(_) => Some(MatchingItem::NoValue),
_ => panic!("Expected `{item_name}` to be a name-value pair or a path"),
};
}
None
}
fn field_name(field: &Field) -> String {
to_s(&field.ident)
}
fn to_s<T: quote::ToTokens>(node: &T) -> String {
quote!(#node).to_string()
}