use darling::util::Flag;
use darling::{FromDeriveInput, FromField, ast};
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
#[derive(Clone)]
struct MetaExpr(#[allow(dead_code)] syn::Expr);
impl darling::FromMeta for MetaExpr {
fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
Ok(MetaExpr(expr.clone()))
}
fn from_value(value: &syn::Lit) -> darling::Result<Self> {
Ok(MetaExpr(lit_to_expr(value)))
}
}
#[derive(Clone)]
enum DefaultKind {
Inherit,
Explicit(syn::Expr),
}
impl darling::FromMeta for DefaultKind {
fn from_word() -> darling::Result<Self> {
Ok(DefaultKind::Inherit)
}
fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
Ok(DefaultKind::Explicit(expr.clone()))
}
fn from_value(value: &syn::Lit) -> darling::Result<Self> {
Ok(DefaultKind::Explicit(lit_to_expr(value)))
}
}
fn lit_to_expr(lit: &syn::Lit) -> syn::Expr {
syn::Expr::Lit(syn::ExprLit {
attrs: Vec::new(),
lit: lit.clone(),
})
}
#[derive(FromField)]
#[darling(attributes(confroid), forward_attrs(doc))]
#[allow(dead_code)]
struct FieldOpts {
ident: Option<syn::Ident>,
ty: syn::Type,
name: Option<String>,
default: Option<DefaultKind>,
example: Option<MetaExpr>,
auto_vec: Flag,
auto_vec_delimiter: Option<String>,
attrs: Vec<syn::Attribute>,
}
#[derive(FromDeriveInput)]
#[darling(attributes(confroid), supports(struct_named))]
struct InputOpts {
ident: syn::Ident,
generics: syn::Generics,
data: ast::Data<darling::util::Ignored, FieldOpts>,
}
#[proc_macro_derive(Config, attributes(confroid))]
pub fn derive_config(input: TokenStream) -> TokenStream {
let di = parse_macro_input!(input as DeriveInput);
let opts = match InputOpts::from_derive_input(&di) {
Ok(opts) => opts,
Err(err) => return err.write_errors().into(),
};
match expand(&opts) {
Ok(tokens) => tokens.into(),
Err(err) => err.write_errors().into(),
}
}
fn expand(opts: &InputOpts) -> darling::Result<proc_macro2::TokenStream> {
let ident = &opts.ident;
let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
let fields = opts
.data
.as_ref()
.take_struct()
.expect("supports(struct_named) guarantees a struct")
.fields;
let mut errors = darling::Error::accumulator();
let mut field_inits = Vec::with_capacity(fields.len());
let mut field_docs = Vec::with_capacity(fields.len());
for field in fields {
let fname = field
.ident
.as_ref()
.expect("named struct fields always have an ident");
let fname_str = fname.to_string();
let var_seg = field
.name
.clone()
.unwrap_or_else(|| screaming_snake(&fname_str));
let fty = &field.ty;
let read_expr = if field.auto_vec.is_present() {
match vec_inner(fty) {
Some(elem_ty) => {
let delim = field
.auto_vec_delimiter
.clone()
.unwrap_or_else(|| ",".to_string());
auto_vec_read(&var_seg, &fname_str, elem_ty, &delim)
}
None => {
errors.push(
darling::Error::custom("`auto_vec` requires a `Vec<T>` field")
.with_span(fty),
);
continue;
}
}
} else {
quote! {
{
let __ctx = ctx.field(#var_seg, #fname_str);
<#fty as ::confroid::FromEnv>::from_env(&__ctx, env)
}
}
};
let init = match &field.default {
None => quote! { #fname: (#read_expr)? },
Some(DefaultKind::Inherit) => quote! {
#fname: match #read_expr {
::core::result::Result::Ok(__v) => __v,
::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
<#fty as ::core::default::Default>::default(),
::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
}
},
Some(DefaultKind::Explicit(expr)) => {
let default_val = default_value(expr);
quote! {
#fname: match #read_expr {
::core::result::Result::Ok(__v) => __v,
::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
#default_val,
::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
}
}
}
};
field_inits.push(init);
let doc_tokens = match extract_doc(&field.attrs) {
Some(doc) => quote! { ::core::option::Option::Some(#doc) },
None => quote! { ::core::option::Option::None },
};
let is_collection = type_is_named(fty, "Vec") || type_is_named(fty, "HashMap");
let is_option = type_is_named(fty, "Option");
let none = quote! { ::core::option::Option::None };
let default_doc = match &field.default {
Some(DefaultKind::Inherit) if !is_collection && !is_option => quote! {
::core::option::Option::Some(
::std::format!("{}", <#fty as ::core::default::Default>::default())
)
},
Some(DefaultKind::Explicit(expr)) if !is_collection => quote! {
::core::option::Option::Some(::std::format!("{}", #expr))
},
_ => none.clone(),
};
let example_doc = match &field.example {
Some(MetaExpr(expr)) if !is_collection => quote! {
::core::option::Option::Some(::std::format!("{}", #expr))
},
_ => none.clone(),
};
field_docs.push(quote! {
::confroid::FieldDoc {
var_seg: #var_seg,
name: #fname_str,
doc: #doc_tokens,
default: #default_doc,
example: #example_doc,
children: <#fty as ::confroid::Documented>::doc_fields(),
}
});
}
errors.finish()?;
let documented_impl = if cfg!(feature = "docs") {
let ident_str = ident.to_string();
quote! {
#[automatically_derived]
impl #impl_generics ::confroid::Documented for #ident #ty_generics #where_clause {
fn doc_name() -> &'static str {
#ident_str
}
fn doc_fields() -> ::core::option::Option<::std::vec::Vec<::confroid::FieldDoc>> {
::core::option::Option::Some(::std::vec![
#(#field_docs),*
])
}
}
}
} else {
quote! {}
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::confroid::FromEnv for #ident #ty_generics #where_clause {
fn from_env(
ctx: &::confroid::Ctx,
env: &::confroid::Env,
) -> ::confroid::Result<Self> {
::core::result::Result::Ok(Self {
#(#field_inits),*
})
}
fn is_present(ctx: &::confroid::Ctx, env: &::confroid::Env) -> bool {
!env.immediate_children(&ctx.var).is_empty()
}
}
#documented_impl
})
}
fn auto_vec_read(
var_seg: &str,
fname_str: &str,
elem_ty: &syn::Type,
delim: &str,
) -> proc_macro2::TokenStream {
quote! {
{
let __ctx = ctx.field(#var_seg, #fname_str);
match env.get(&__ctx.var) {
::core::option::Option::None => ::core::result::Result::Err(
::confroid::ConfroidError::EnvVarNotFound {
var_name: __ctx.var.clone(),
field: __ctx.path.clone(),
}
),
::core::option::Option::Some(__raw) if __raw.is_empty() =>
::core::result::Result::Ok(::std::vec::Vec::new()),
::core::option::Option::Some(__raw) => {
let mut __out: ::std::vec::Vec<#elem_ty> = ::std::vec::Vec::new();
let mut __result = ::core::result::Result::Ok(());
for __part in __raw.split(#delim) {
match __part.parse::<#elem_ty>() {
::core::result::Result::Ok(__v) => __out.push(__v),
::core::result::Result::Err(__e) => {
__result = ::core::result::Result::Err(
::confroid::ConfroidError::EnvVarInvalid {
var_name: __ctx.var.clone(),
field: __ctx.path.clone(),
value: __part.to_string(),
parser_error: __e.to_string(),
}
);
break;
}
}
}
__result.map(|()| __out)
}
}
}
}
}
fn default_value(expr: &syn::Expr) -> proc_macro2::TokenStream {
if is_numeric_literal(expr) {
quote! { #expr }
} else {
quote! { ::core::convert::Into::into(#expr) }
}
}
fn is_numeric_literal(expr: &syn::Expr) -> bool {
match expr {
syn::Expr::Lit(syn::ExprLit { lit, .. }) => {
matches!(lit, syn::Lit::Int(_) | syn::Lit::Float(_))
}
syn::Expr::Unary(syn::ExprUnary {
op: syn::UnOp::Neg(_),
expr,
..
}) => is_numeric_literal(expr),
syn::Expr::Group(g) => is_numeric_literal(&g.expr),
syn::Expr::Paren(p) => is_numeric_literal(&p.expr),
_ => false,
}
}
fn type_is_named(ty: &syn::Type, name: &str) -> bool {
matches!(ty, syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == name))
}
fn vec_inner(ty: &syn::Type) -> Option<&syn::Type> {
let syn::Type::Path(tp) = ty else {
return None;
};
let seg = tp.path.segments.last()?;
if seg.ident != "Vec" {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
args.args.iter().find_map(|arg| match arg {
syn::GenericArgument::Type(inner) => Some(inner),
_ => None,
})
}
fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
let mut lines = Vec::new();
for attr in attrs {
if !attr.path().is_ident("doc") {
continue;
}
if let syn::Meta::NameValue(nv) = &attr.meta
&& let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
lines.push(s.value().trim().to_string());
}
}
if lines.is_empty() {
None
} else {
Some(lines.join(" "))
}
}
fn screaming_snake(s: &str) -> String {
let mut out = String::new();
let mut prev_is_word = false;
for ch in s.chars() {
if ch == '_' {
out.push('_');
prev_is_word = false;
continue;
}
if ch.is_uppercase() && prev_is_word {
out.push('_');
}
out.extend(ch.to_uppercase());
prev_is_word = ch.is_lowercase() || ch.is_ascii_digit();
}
out
}