use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{DeriveInput, parse_macro_input};
mod derive {
pub(crate) mod build;
pub(crate) mod crate_path;
pub(crate) mod generate;
pub(crate) mod load_impl;
pub(crate) mod parse;
}
mod selected_subcommand_merge;
mod subcommand_docs;
use derive::build::cli_tokens::build_cli_struct_tokens;
use derive::build::{
CollectionStrategies, build_config_env_var, build_default_struct_fields,
build_default_struct_init, build_env_provider, collect_collection_strategies,
compute_config_env_var, compute_dotfile_name, default_app_name,
};
use derive::generate::declarative::generate_declarative_impl;
use derive::generate::docs::{DocsArgs, generate_docs_impl};
use derive::generate::ortho_impl::generate_trait_implementation;
use derive::load_impl::{
DiscoveryTokens, LoadImplArgs, LoadImplIdents, LoadImplTokens, build_load_impl,
};
use derive::parse::{SerdeRenameAll, parse_input, serde_rename_all};
#[proc_macro_derive(OrthoConfig, attributes(ortho_config))]
pub fn derive_ortho_config(input_tokens: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input_tokens as DeriveInput);
let (ident, fields, struct_attrs, field_attrs) = match parse_input(&derive_input) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let serde_rename_all = match serde_rename_all(&derive_input.attrs) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let krate = derive::crate_path::resolve(struct_attrs.crate_path.as_ref());
let component_args = MacroComponentArgs {
ident: &ident,
fields: &fields,
struct_attrs: &struct_attrs,
field_attrs: &field_attrs,
serde_rename_all,
krate: &krate,
};
let components = match build_macro_components(&component_args) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let core_tokens = generate_trait_implementation(&ident, &components, &krate);
let declarative_impl = generate_declarative_impl(
&ident,
&components.collection_strategies,
components.post_merge_hook,
&krate,
);
let docs_impl = match generate_docs_impl(&DocsArgs {
ident: &ident,
fields: &fields,
field_attrs: &field_attrs,
struct_attrs: &struct_attrs,
serde_rename_all,
cli_fields: &components.cli_field_metadata,
krate: &krate,
}) {
Ok(tokens) => tokens,
Err(err) => return err.to_compile_error().into(),
};
let expanded = quote! {
#core_tokens
#declarative_impl
#docs_impl
};
TokenStream::from(expanded)
}
#[proc_macro_derive(SelectedSubcommandMerge, attributes(ortho_subcommand, ortho_config))]
pub fn derive_selected_subcommand_merge(input_tokens: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input_tokens as DeriveInput);
match selected_subcommand_merge::derive_selected_subcommand_merge(derive_input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
#[proc_macro_derive(OrthoConfigSubcommandDocs, attributes(ortho_config))]
pub fn derive_subcommand_docs(input_tokens: TokenStream) -> TokenStream {
let derive_input = parse_macro_input!(input_tokens as DeriveInput);
match subcommand_docs::derive_subcommand_docs(derive_input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
#[derive(Clone)]
pub(crate) struct CliFieldInfo {
pub serialized_key: String,
pub arg_id: String,
pub is_default_as_absent: bool,
}
struct MacroComponents {
defaults_ident: syn::Ident,
default_struct_fields: Vec<proc_macro2::TokenStream>,
cli_ident: syn::Ident,
cli_struct_fields: Vec<proc_macro2::TokenStream>,
load_impl: proc_macro2::TokenStream,
prefix_fn: Option<proc_macro2::TokenStream>,
collection_strategies: CollectionStrategies,
cli_field_info: Vec<CliFieldInfo>,
cli_field_metadata: Vec<derive::build::CliFieldMetadata>,
post_merge_hook: bool,
}
#[derive(Clone, Copy)]
struct LoadTokenRefs<'a> {
env_provider: &'a proc_macro2::TokenStream,
default_struct_init: &'a [proc_macro2::TokenStream],
config_env_var: &'a proc_macro2::TokenStream,
dotfile_name: &'a syn::LitStr,
}
fn build_discovery_tokens(
struct_attrs: &derive::parse::StructAttrs,
ident: &syn::Ident,
) -> Option<DiscoveryTokens> {
let default_app_name_value = default_app_name(struct_attrs, ident);
struct_attrs
.discovery
.as_ref()
.map(|attrs| DiscoveryTokens {
app_name: attrs.app_name.clone().unwrap_or(default_app_name_value),
env_var: attrs
.env_var
.clone()
.unwrap_or_else(|| compute_config_env_var(struct_attrs)),
config_file_name: attrs.config_file_name.clone(),
dotfile_name: attrs.dotfile_name.clone(),
project_file_name: attrs.project_file_name.clone(),
})
}
#[derive(Clone, Copy)]
struct LoadImplConfig<'a> {
struct_attrs: &'a derive::parse::StructAttrs,
discovery_tokens: Option<&'a DiscoveryTokens>,
has_config_path: bool,
}
struct LoadImplResult<'a> {
args: LoadImplArgs<'a>,
legacy_app_name_storage: String,
}
struct MacroComponentArgs<'a> {
ident: &'a syn::Ident,
fields: &'a [syn::Field],
struct_attrs: &'a derive::parse::StructAttrs,
field_attrs: &'a [derive::parse::FieldAttrs],
serde_rename_all: Option<SerdeRenameAll>,
krate: &'a proc_macro2::TokenStream,
}
fn build_load_impl_args<'a>(
idents: LoadImplIdents<'a>,
token_refs: LoadTokenRefs<'a>,
config: LoadImplConfig<'a>,
krate: &'a proc_macro2::TokenStream,
) -> LoadImplResult<'a> {
let legacy_app_name_storage = config
.struct_attrs
.prefix
.as_ref()
.map(|prefix| prefix.trim_end_matches('_').to_ascii_lowercase())
.unwrap_or_default();
let LoadTokenRefs {
env_provider,
default_struct_init,
config_env_var,
dotfile_name,
} = token_refs;
let tokens = LoadImplTokens {
env_provider,
default_struct_init,
config_env_var,
dotfile_name,
legacy_app_name: legacy_app_name_storage.clone(),
discovery: config.discovery_tokens,
krate,
};
let args = LoadImplArgs {
idents,
tokens,
has_config_path: config.has_config_path,
};
LoadImplResult {
args,
legacy_app_name_storage,
}
}
fn build_macro_components(args: &MacroComponentArgs<'_>) -> syn::Result<MacroComponents> {
let MacroComponentArgs {
ident,
fields,
struct_attrs,
field_attrs,
serde_rename_all,
krate,
} = args;
let defaults_ident = format_ident!("__{}Defaults", ident);
let default_struct_fields = build_default_struct_fields(fields, field_attrs);
let cli_ident = format_ident!("__{}Cli", ident);
let cli_build_result =
build_cli_struct_tokens(fields, field_attrs, struct_attrs, *serde_rename_all)?;
let default_struct_init = build_default_struct_init(fields, field_attrs);
let env_provider = build_env_provider(struct_attrs, krate);
let config_env_var = build_config_env_var(struct_attrs);
let dotfile_name_string = compute_dotfile_name(struct_attrs);
let dotfile_name = syn::LitStr::new(&dotfile_name_string, proc_macro2::Span::call_site());
let collection_strategies = collect_collection_strategies(fields, field_attrs)?;
let has_config_path = true;
let discovery_tokens = build_discovery_tokens(struct_attrs, ident);
let load_impl_idents = LoadImplIdents {
cli_ident: &cli_ident,
config_ident: ident,
defaults_ident: &defaults_ident,
};
let load_token_refs = LoadTokenRefs {
env_provider: &env_provider,
default_struct_init: &default_struct_init,
config_env_var: &config_env_var,
dotfile_name: &dotfile_name,
};
let load_impl_config = LoadImplConfig {
struct_attrs,
discovery_tokens: discovery_tokens.as_ref(),
has_config_path,
};
let LoadImplResult {
args: load_impl_args,
legacy_app_name_storage: _legacy_app_name_storage,
} = build_load_impl_args(load_impl_idents, load_token_refs, load_impl_config, krate);
let load_impl = build_load_impl(&load_impl_args);
let prefix_fn = struct_attrs.prefix.as_ref().map(|prefix| {
quote! {
fn prefix() -> &'static str {
#prefix
}
}
});
Ok(MacroComponents {
defaults_ident,
default_struct_fields,
cli_ident,
cli_struct_fields: cli_build_result.fields,
load_impl,
prefix_fn,
collection_strategies,
cli_field_info: cli_build_result.field_info,
cli_field_metadata: cli_build_result.metadata,
post_merge_hook: struct_attrs.post_merge_hook,
})
}
#[cfg(test)]
mod tests;