use darling::{FromDeriveInput, FromField};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(gonfig, Gonfig))]
struct GonfigOpts {
ident: syn::Ident,
generics: syn::Generics,
data: darling::ast::Data<(), GonfigField>,
#[darling(default)]
env_prefix: Option<String>,
#[darling(default)]
allow_cli: bool,
#[darling(default)]
allow_config: bool,
}
#[derive(Debug, FromField)]
#[darling(attributes(gonfig, skip_gonfig, skip))]
struct GonfigField {
ident: Option<syn::Ident>,
#[allow(dead_code)]
ty: syn::Type,
#[darling(default)]
env_name: Option<String>,
#[darling(default)]
cli_name: Option<String>,
#[darling(default)]
skip_gonfig: bool,
#[darling(default)]
skip: bool,
#[allow(dead_code)]
#[darling(default)]
flatten: bool,
#[allow(dead_code)]
#[darling(default)]
nested: bool,
#[darling(default)]
default: Option<String>,
}
#[proc_macro_derive(Gonfig, attributes(gonfig, skip_gonfig, skip, Gonfig))]
pub fn derive_gonfig(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let opts = match GonfigOpts::from_derive_input(&input) {
Ok(opts) => opts,
Err(e) => return TokenStream::from(e.write_errors()),
};
let expanded = generate_gonfig_impl(&opts);
TokenStream::from(expanded)
}
fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
let name = &opts.ident;
let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
let allow_env = true; let allow_cli = opts.allow_cli;
let allow_config = opts.allow_config;
let env_prefix = opts.env_prefix.as_ref().cloned().unwrap_or_default();
let fields = opts
.data
.as_ref()
.take_struct()
.expect("Only structs are supported")
.fields;
let mut regular_mappings = Vec::new();
let mut default_mappings = Vec::new();
let mut nested_fields = Vec::new();
let mut all_fields = Vec::new();
for f in fields.iter().filter(|f| !f.skip_gonfig && !f.skip) {
let field_name = f.ident.as_ref().unwrap();
let field_str = field_name.to_string();
let field_type = &f.ty;
if f.nested {
nested_fields.push((field_name.clone(), field_type.clone()));
all_fields.push((field_name.clone(), true)); continue;
}
all_fields.push((field_name.clone(), false));
{
let cli_key = if let Some(custom_name) = &f.cli_name {
custom_name.clone()
} else {
field_str.replace('_', "-")
};
let custom_env_opt = if let Some(custom) = &f.env_name {
quote! { Some(#custom.to_string()) }
} else {
quote! { None }
};
regular_mappings.push(quote! {
(
#field_str.to_string(),
#custom_env_opt,
#cli_key.to_string()
)
});
if let Some(default_value) = &f.default {
default_mappings.push(quote! {
(#field_str.to_string(), #default_value.to_string())
});
}
}
}
let has_nested = !nested_fields.is_empty();
let nested_field_names: Vec<_> = nested_fields.iter().map(|(name, _)| name).collect();
let nested_field_types: Vec<_> = nested_fields.iter().map(|(_, ty)| ty).collect();
quote! {
impl #impl_generics #name #ty_generics #where_clause {
pub fn from_gonfig() -> ::gonfig::Result<Self> {
Self::from_gonfig_with_parent_prefix("")
}
pub fn from_gonfig_with_parent_prefix(parent_prefix: &str) -> ::gonfig::Result<Self> {
Self::from_gonfig_with_builder_and_parent(::gonfig::ConfigBuilder::new(), parent_prefix)
}
pub fn from_gonfig_with_builder(builder: ::gonfig::ConfigBuilder) -> ::gonfig::Result<Self> {
Self::from_gonfig_with_builder_and_parent(builder, "")
}
fn from_gonfig_with_builder_and_parent(mut builder: ::gonfig::ConfigBuilder, parent_prefix: &str) -> ::gonfig::Result<Self> {
let composed_prefix = if parent_prefix.is_empty() {
#env_prefix.to_string()
} else if #env_prefix.is_empty() {
parent_prefix.to_string()
} else {
format!("{}_{}", parent_prefix, #env_prefix)
};
let field_mappings: Vec<(String, Option<String>, String)> = vec![#(#regular_mappings),*];
let default_values: Vec<(String, String)> = vec![#(#default_mappings),*];
if #allow_env {
let mut env = ::gonfig::Environment::new();
if !composed_prefix.is_empty() {
env = env.with_prefix(&composed_prefix);
}
for (field_name, custom_env_name, _cli_key) in &field_mappings {
let env_key = if let Some(custom) = custom_env_name {
custom.clone()
} else if !composed_prefix.is_empty() {
format!("{}_{}", composed_prefix, field_name.to_uppercase())
} else {
field_name.to_uppercase()
};
env = env.with_field_mapping(field_name, &env_key);
}
builder = builder.with_env_custom(env);
}
if #allow_cli {
let mut cli = ::gonfig::Cli::from_args();
for (field_name, _custom_env_name, cli_key) in &field_mappings {
cli = cli.with_field_mapping(field_name, cli_key);
}
builder = builder.with_cli_custom(cli);
}
if #allow_config {
if ::std::path::Path::new("config.toml").exists() {
builder = match builder.with_file("config.toml") {
Ok(b) => b,
Err(e) => return Err(e),
};
} else if ::std::path::Path::new("config.yaml").exists() {
builder = match builder.with_file("config.yaml") {
Ok(b) => b,
Err(e) => return Err(e),
};
} else if ::std::path::Path::new("config.json").exists() {
builder = match builder.with_file("config.json") {
Ok(b) => b,
Err(e) => return Err(e),
};
}
}
if !default_values.is_empty() {
let mut defaults_json = ::serde_json::Map::new();
for (field_name, default_value) in default_values {
let value = default_value.parse::<::serde_json::Value>()
.unwrap_or_else(|_| ::serde_json::Value::String(default_value));
defaults_json.insert(field_name, value);
}
builder = builder.with_defaults(::serde_json::Value::Object(defaults_json))?;
}
if #has_nested {
#(
let #nested_field_names = <#nested_field_types>::from_gonfig_with_parent_prefix(&composed_prefix)?;
)*
let mut config_value = builder.build_value()?;
if let ::serde_json::Value::Object(ref mut map) = config_value {
#(
map.remove(stringify!(#nested_field_names));
)*
}
let mut result: Self = ::serde_json::from_value(config_value)
.map_err(|e| ::gonfig::Error::Serialization(
format!("Failed to deserialize config: {}", e)
))?;
#(
result.#nested_field_names = #nested_field_names;
)*
Ok(result)
} else {
builder.build::<Self>()
}
}
pub fn gonfig_builder() -> ::gonfig::ConfigBuilder {
let mut builder = ::gonfig::ConfigBuilder::new();
let field_mappings: Vec<(String, Option<String>, String)> = vec![#(#regular_mappings),*];
let prefix = #env_prefix;
if #allow_env {
let mut env = ::gonfig::Environment::new();
if !prefix.is_empty() {
env = env.with_prefix(prefix);
}
for (field_name, custom_env_name, _cli_key) in &field_mappings {
let env_key = if let Some(custom) = custom_env_name {
custom.clone()
} else if !prefix.is_empty() {
format!("{}_{}", prefix, field_name.to_uppercase())
} else {
field_name.to_uppercase()
};
env = env.with_field_mapping(field_name, &env_key);
}
builder = builder.with_env_custom(env);
}
if #allow_cli {
let mut cli = ::gonfig::Cli::from_args();
for (field_name, _custom_env_name, cli_key) in &field_mappings {
cli = cli.with_field_mapping(field_name, cli_key);
}
builder = builder.with_cli_custom(cli);
}
builder
}
}
}
}