mod clap;
mod config_parser;
mod syn_utils;
use darling::FromDeriveInput;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{DeriveInput, Ident, parse_macro_input};
use clap::{ClapArg, ClapCommand};
use config_parser::{ConfigFormat, ConfigParser};
use syn_utils::TypeExt;
#[proc_macro_derive(ConfigParser, attributes(config))]
pub fn derive_parser_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let config_parser = match ConfigParser::from_derive_input(&input) {
Ok(v) => v,
Err(e) => return proc_macro::TokenStream::from(e.write_errors()),
};
let ident = config_parser.ident();
let config_path_fn = generate_config_path_fn(&config_parser);
let config_format_fn = generate_config_format_fn(&config_parser);
let into_args_fn = generate_into_args_fn(&config_parser);
let from_args_fn = generate_from_args_fn(&config_parser);
let deserialize_fns = generate_deserialize_fns(&config_parser);
let (config_ident, config_struct) = generate_config_struct(&config_parser);
let (opts_ident, opts_struct) = generate_opts_struct(&config_parser);
quote! {
#config_struct
#opts_struct
impl #config_ident {
#deserialize_fns
}
impl ::clap_config_fallback::IntoArgs for #opts_ident {
#into_args_fn
}
impl ::clap_config_fallback::FromArgs for #opts_ident {
#from_args_fn
}
impl ::clap_config_fallback::ConfigSource for #opts_ident {
#config_path_fn
#config_format_fn
}
impl ::clap_config_fallback::ConfigParser for #ident {
type Opts = #opts_ident;
type Config = #config_ident;
}
}
.into()
}
fn generate_from_args_fn(config_parser: &ConfigParser) -> TokenStream {
let field_assignments = config_parser.fields().map(|field| {
let field_ident = field.ident();
if field
.attributes()
.find_map(ClapCommand::from_attr)
.is_some()
{
let opts_ident = format_ident!("{}Opts", field.ty().ident().unwrap());
quote! {
#field_ident: #opts_ident::from_args(&args)
}
} else if field.ty().is("bool") {
quote! {
#field_ident: args.get_flag(stringify!(#field_ident)).then_some(true)
}
} else {
quote! {
#field_ident: args.get_one(stringify!(#field_ident)).cloned()
}
}
});
quote! {
fn from_args(args: &::clap::ArgMatches) -> Self {
Self {
#(#field_assignments),*
}
}
}
}
fn generate_config_format_fn(config_parser: &ConfigParser) -> TokenStream {
let format = match config_parser
.fields()
.find(|field| field.is_path())
.and_then(|field| field.format())
{
Some(ConfigFormat::Toml) => format_ident!("Toml"),
Some(ConfigFormat::Yaml) => format_ident!("Yaml"),
Some(ConfigFormat::Json) => format_ident!("Json"),
None | Some(ConfigFormat::Auto) => return TokenStream::default(),
};
quote! {
fn config_format(&self) -> ::std::option::Option<::clap_config_fallback::ConfigFormat> {
::std::option::Option::Some(::clap_config_fallback::ConfigFormat::#format)
}
}
}
fn generate_config_path_fn(config_parser: &ConfigParser) -> TokenStream {
let Some(field) = config_parser.fields().find(|field| field.is_path()) else {
return quote! {
fn config_path(&self) -> ::std::option::Option<&str> {
::std::option::Option::None
}
};
};
let field_ident = field.ident();
let default_path = field
.attributes()
.filter_map(ClapArg::from_attr)
.find_map(|arg| arg.default_value().map(str::to_owned));
if let Some(default_path) = default_path {
quote! {
fn config_path(&self) -> ::std::option::Option<&str> {
self.#field_ident.as_deref().or(Some(#default_path))
}
}
} else {
quote! {
fn config_path(&self) -> ::std::option::Option<&str> {
self.#field_ident.as_deref()
}
}
}
}
fn generate_into_args_fn(config_parser: &ConfigParser) -> TokenStream {
let args = config_parser.fields().map(|field| {
let field_ident = field.ident();
if field
.attributes()
.find_map(ClapCommand::from_attr)
.is_some()
{
quote! {
args.extend(self.#field_ident.into_args());
}
} else {
let formatted_value = field
.value_format()
.map(|value_format| quote! { #value_format })
.unwrap_or_else(|| quote! { #field_ident.to_string() });
match field
.attributes()
.filter_map(ClapArg::from_attr)
.find_map(|arg| arg.flag_name(field_ident))
{
Some(flag_name) if field.ty().is("bool") => {
quote! {
if let Some(true) = self.#field_ident {
args.push(#flag_name.to_string());
}
}
}
Some(flag_name) => {
quote! {
if let Some(#field_ident) = self.#field_ident {
args.push(#flag_name.to_string());
args.push(#formatted_value);
}
}
}
None => {
quote! {
if let Some(#field_ident) = self.#field_ident {
args.push(#formatted_value);
}
}
}
}
}
});
quote! {
fn into_args(self) -> impl Iterator<Item = String> {
let mut args = Vec::default();
#(#args)*
args.into_iter()
}
}
}
fn generate_deserialize_fns(config_parser: &ConfigParser) -> TokenStream {
let deserialize_fns = config_parser.fields().filter(|field| !field.is_skipped()).filter_map(|field| {
if let Some(path) = field
.attributes()
.filter_map(ClapArg::from_attr)
.find_map(|arg| arg.value_parser())
{
let fn_ident = format_ident!("deserialize_{}", field.ident());
let field_ty = field.ty().to_option();
Some(quote! {
fn #fn_ident<'de, D>(deserializer: D) -> ::std::result::Result<#field_ty, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
let s: ::std::option::Option<String> = ::serde::Deserialize::deserialize(deserializer)?;
s
.map(|s| #path(s.as_str()).map_err(::serde::de::Error::custom))
.transpose()
}
})
} else {
None
}
});
quote! {
#(#deserialize_fns)*
}
}
fn generate_config_struct(config_parser: &ConfigParser) -> (Ident, TokenStream) {
let ident = format_ident!("{}Config", config_parser.ident());
let fields = config_parser
.fields()
.filter(|field| !field.is_skipped())
.map(|field| {
let field_ident = field.ident();
let clap_attrs = field
.attributes()
.filter_map(ClapArg::from_attr)
.collect::<Vec<_>>();
let alias_attrs = clap_attrs
.iter()
.flat_map(|arg| arg.aliases())
.map(|alias| quote! { #[serde(alias = #alias)] });
let deserialize_with_attr =
clap_attrs
.iter()
.find_map(|arg| arg.value_parser())
.map(|_| {
let deserialize_fn = format!("{}::deserialize_{}", ident, field_ident);
quote! { #[serde(deserialize_with = #deserialize_fn)] }
});
if field
.attributes()
.find_map(ClapCommand::from_attr)
.is_some()
{
let field_ty = format_ident!("{}Config", field.ty().ident().unwrap());
quote! {
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
#field_ident: ::std::option::Option<#field_ty>
}
} else {
let field_ty = &field.ty().to_option();
quote! {
#(#alias_attrs)*
#deserialize_with_attr
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
#field_ident: #field_ty
}
}
});
(
ident.clone(),
quote! {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct #ident {
#(#fields),*
}
},
)
}
fn generate_opts_struct(config_parser: &ConfigParser) -> (Ident, TokenStream) {
let ident = format_ident!("{}Opts", config_parser.ident());
let fields = config_parser.fields().map(|field| {
let field_ident = field.ident();
if field
.attributes()
.find_map(ClapCommand::from_attr)
.is_some()
{
let field_ty = format_ident!("{}Opts", field.ty().ident().unwrap());
return quote! {
#[command(flatten)]
#field_ident: #field_ty
};
}
let field_attrs = field.attributes().cloned().map(ClapArg::sanitize);
let field_ty = field.ty().to_option();
if field.ty().is("bool") {
quote! {
#(#field_attrs)*
#[arg(action = clap::ArgAction::SetTrue)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
#field_ident: #field_ty
}
} else {
quote! {
#(#field_attrs)*
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
#field_ident: #field_ty
}
}
});
(
ident.clone(),
quote! {
#[derive(Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
struct #ident {
#(#fields),*
}
},
)
}