#![doc = include_str!("../README.md")]
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
mod attrs;
use attrs::FieldAttrs;
fn extract_option_inner_type(ty: &Type) -> &Type {
if let Type::Path(type_path) = ty {
if let Some(seg) = type_path.path.segments.last() {
if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
return inner;
}
}
}
}
ty
}
#[proc_macro_derive(ServiceConf, attributes(conf))]
pub fn derive_serviceconf(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut prefix = String::new();
for attr in &input.attrs {
if !attr.path().is_ident("conf") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("prefix") {
let value = meta.value()?;
let lit: syn::Lit = value.parse()?;
if let syn::Lit::Str(s) = lit {
prefix = s.value();
}
return Ok(());
}
Err(meta.error("unsupported struct-level conf attribute"))
});
}
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
&input,
"ServiceConf only supports structs with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(&input, "ServiceConf only supports structs")
.to_compile_error()
.into();
}
};
for field in fields.iter() {
let field_type = &field.ty;
let attrs = FieldAttrs::from_field(field);
let is_option = if let syn::Type::Path(type_path) = field_type {
type_path
.path
.segments
.last()
.map(|seg| seg.ident == "Option")
.unwrap_or(false)
} else {
false
};
if is_option && attrs.default.is_some() {
return syn::Error::new_spanned(
field,
"Option<T> fields cannot have default attribute (they default to None automatically)",
)
.to_compile_error()
.into();
}
}
let field_initializers = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let attrs = FieldAttrs::from_field(field);
let is_option = if let syn::Type::Path(type_path) = field_type {
type_path.path.segments.last()
.map(|seg| seg.ident == "Option")
.unwrap_or(false)
} else {
false
};
let base_name = attrs.name.unwrap_or_else(|| {
field_name.to_string().to_uppercase()
});
let env_var_name = format!("{}{}", prefix, base_name);
let load_from_file = attrs.from_file;
let deserializer_fn = attrs.deserializer;
let deserialize_expr = if is_option && deserializer_fn.is_none() {
let inner_type = extract_option_inner_type(field_type);
quote! {
::serviceconf::de::deserialize_optional::<#inner_type>(
#env_var_name,
#load_from_file
)?
}
} else if let Some(func_path) = deserializer_fn {
let func: proc_macro2::TokenStream = func_path.parse().unwrap();
if is_option {
let inner_type = extract_option_inner_type(field_type);
quote! {
match ::serviceconf::de::get_env_value(#env_var_name, #load_from_file) {
Ok(__value) => Some(#func(&__value).map_err(|e| ::serviceconf::ServiceConfError::parse_error::<#inner_type>(#env_var_name, e))?),
Err(::serviceconf::ServiceConfError::Missing { .. }) => None,
Err(e) => return Err(e.into()),
}
}
} else {
match attrs.default {
Some(Some(default_value)) => {
quote! {
match ::serviceconf::de::get_env_value(#env_var_name, #load_from_file) {
Ok(__value) => #func(&__value).map_err(|e| ::serviceconf::ServiceConfError::parse_error::<#field_type>(#env_var_name, e))?,
Err(::serviceconf::ServiceConfError::Missing { .. }) => #default_value,
Err(e) => return Err(e.into()),
}
}
}
Some(None) => {
quote! {
match ::serviceconf::de::get_env_value(#env_var_name, #load_from_file) {
Ok(__value) => #func(&__value).map_err(|e| ::serviceconf::ServiceConfError::parse_error::<#field_type>(#env_var_name, e))?,
Err(::serviceconf::ServiceConfError::Missing { .. }) => Default::default(),
Err(e) => return Err(e.into()),
}
}
}
None => {
quote! {
{
let __value = ::serviceconf::de::get_env_value(#env_var_name, #load_from_file)?;
#func(&__value).map_err(|e| ::serviceconf::ServiceConfError::parse_error::<#field_type>(#env_var_name, e))?
}
}
}
}
}
} else {
match attrs.default {
Some(Some(default_value)) => {
quote! {
::serviceconf::de::deserialize_with_default::<#field_type>(
#env_var_name,
#load_from_file,
#default_value
)?
}
}
Some(None) => {
quote! {
::serviceconf::de::deserialize_with_default::<#field_type>(
#env_var_name,
#load_from_file,
Default::default()
)?
}
}
None => {
quote! {
::serviceconf::de::deserialize_required::<#field_type>(
#env_var_name,
#load_from_file
)?
}
}
}
};
quote! {
#field_name: #deserialize_expr
}
});
let expanded = quote! {
impl #struct_name {
pub fn from_env() -> ::serviceconf::anyhow::Result<Self> {
Ok(Self {
#(#field_initializers),*
})
}
}
};
TokenStream::from(expanded)
}