use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
use crate::derive::build::{compute_config_env_var, default_app_name};
use crate::derive::parse::{DocStructAttrs, HeadingOverrides, StructAttrs};
use super::types::{AppName, ConfigFileName};
use super::{example_tokens, link_tokens, note_tokens, option_string_tokens};
fn default_headings() -> HeadingOverrides {
HeadingOverrides {
name: Some(String::from("ortho.headings.name")),
synopsis: Some(String::from("ortho.headings.synopsis")),
description: Some(String::from("ortho.headings.description")),
options: Some(String::from("ortho.headings.options")),
environment: Some(String::from("ortho.headings.environment")),
files: Some(String::from("ortho.headings.files")),
precedence: Some(String::from("ortho.headings.precedence")),
exit_status: Some(String::from("ortho.headings.exit_status")),
examples: Some(String::from("ortho.headings.examples")),
see_also: Some(String::from("ortho.headings.see_also")),
commands: Some(String::from("ortho.headings.commands")),
}
}
pub(super) fn resolve_app_name(struct_attrs: &StructAttrs, ident: &Ident) -> String {
struct_attrs
.discovery
.as_ref()
.and_then(|discovery| discovery.app_name.clone())
.unwrap_or_else(|| default_app_name(struct_attrs, ident))
}
pub(super) fn resolve_about_id(app_name: &AppName, doc: &DocStructAttrs) -> String {
doc.about_id
.clone()
.unwrap_or_else(|| format!("{}.about", &**app_name))
}
#[expect(
clippy::cognitive_complexity,
reason = "`quote!` expansion inflates the complexity score; keep this wrapper minimal."
)]
pub(super) fn build_sections_metadata(
app_name: &AppName,
struct_attrs: &StructAttrs,
krate: &TokenStream,
) -> syn::Result<TokenStream> {
let headings = build_headings_ids(&struct_attrs.doc.headings, krate);
let discovery = build_discovery_metadata(app_name, struct_attrs, krate);
let precedence = build_precedence_metadata(&struct_attrs.doc, krate)?;
let examples = example_tokens(&struct_attrs.doc.examples, krate);
let links = link_tokens(&struct_attrs.doc.links, krate);
let notes = note_tokens(&struct_attrs.doc.notes, krate);
Ok(quote! {
#krate::docs::SectionsMetadata {
headings_ids: #headings,
discovery: #discovery,
precedence: #precedence,
examples: vec![ #( #examples ),* ],
links: vec![ #( #links ),* ],
notes: vec![ #( #notes ),* ],
}
})
}
pub(super) fn build_windows_metadata(
struct_attrs: &StructAttrs,
krate: &TokenStream,
) -> TokenStream {
let Some(windows) = struct_attrs.doc.windows.as_ref() else {
return quote! { None };
};
let module_name = option_string_tokens(windows.module_name.as_deref());
let export_aliases = windows
.export_aliases
.iter()
.map(|alias| {
let lit = syn::LitStr::new(alias, proc_macro2::Span::call_site());
quote! { String::from(#lit) }
})
.collect::<Vec<_>>();
let include_common_parameters = windows.include_common_parameters.unwrap_or(true);
let split_subcommands = windows.split_subcommands.unwrap_or(false);
let help_info_uri = option_string_tokens(windows.help_info_uri.as_deref());
quote! {
Some(#krate::docs::WindowsMetadata {
module_name: #module_name,
export_aliases: vec![ #( #export_aliases ),* ],
include_common_parameters: #include_common_parameters,
split_subcommands_into_functions: #split_subcommands,
help_info_uri: #help_info_uri,
})
}
}
fn build_headings_ids(overrides: &HeadingOverrides, krate: &TokenStream) -> TokenStream {
let headings = merge_headings(overrides);
let process_heading = |field: Option<String>, default_id: &str| -> TokenStream {
let value = field.unwrap_or_else(|| String::from(default_id));
string_tokens(&value)
};
let name_tokens = process_heading(headings.name, "ortho.headings.name");
let synopsis_tokens = process_heading(headings.synopsis, "ortho.headings.synopsis");
let description_tokens = process_heading(headings.description, "ortho.headings.description");
let options_tokens = process_heading(headings.options, "ortho.headings.options");
let environment_tokens = process_heading(headings.environment, "ortho.headings.environment");
let files_tokens = process_heading(headings.files, "ortho.headings.files");
let precedence_tokens = process_heading(headings.precedence, "ortho.headings.precedence");
let exit_status_tokens = process_heading(headings.exit_status, "ortho.headings.exit_status");
let examples_tokens = process_heading(headings.examples, "ortho.headings.examples");
let see_also_tokens = process_heading(headings.see_also, "ortho.headings.see_also");
let commands_tokens = option_string_tokens(headings.commands.as_deref());
quote! {
#krate::docs::HeadingIds {
name: #name_tokens,
synopsis: #synopsis_tokens,
description: #description_tokens,
options: #options_tokens,
environment: #environment_tokens,
files: #files_tokens,
precedence: #precedence_tokens,
exit_status: #exit_status_tokens,
examples: #examples_tokens,
see_also: #see_also_tokens,
commands: #commands_tokens,
}
}
}
fn string_tokens(value: &str) -> TokenStream {
let lit = syn::LitStr::new(value, proc_macro2::Span::call_site());
quote! { String::from(#lit) }
}
fn merge_headings(overrides: &HeadingOverrides) -> HeadingOverrides {
let defaults = default_headings();
HeadingOverrides {
name: overrides.name.clone().or(defaults.name),
synopsis: overrides.synopsis.clone().or(defaults.synopsis),
description: overrides.description.clone().or(defaults.description),
options: overrides.options.clone().or(defaults.options),
environment: overrides.environment.clone().or(defaults.environment),
files: overrides.files.clone().or(defaults.files),
precedence: overrides.precedence.clone().or(defaults.precedence),
exit_status: overrides.exit_status.clone().or(defaults.exit_status),
examples: overrides.examples.clone().or(defaults.examples),
see_also: overrides.see_also.clone().or(defaults.see_also),
commands: overrides.commands.clone().or(defaults.commands),
}
}
fn build_precedence_metadata(
doc: &DocStructAttrs,
krate: &TokenStream,
) -> syn::Result<TokenStream> {
let order_values = doc
.precedence
.as_ref()
.map_or(&[][..], |meta| meta.order.as_slice());
let order = if order_values.is_empty() {
vec![
quote! { #krate::docs::SourceKind::Defaults },
quote! { #krate::docs::SourceKind::File },
quote! { #krate::docs::SourceKind::Env },
quote! { #krate::docs::SourceKind::Cli },
]
} else {
order_values
.iter()
.map(|value| source_kind_tokens(value, krate))
.collect::<Result<Vec<_>, _>>()?
};
let rationale_id = doc
.precedence
.as_ref()
.and_then(|meta| meta.rationale_id.as_deref());
let rationale_tokens = option_string_tokens(rationale_id);
Ok(quote! {
Some(#krate::docs::PrecedenceMeta {
order: vec![ #( #order ),* ],
rationale_id: #rationale_tokens,
})
})
}
fn is_source_kind(value: &str) -> Option<&'static str> {
match value.trim().to_ascii_lowercase().as_str() {
"default" | "defaults" => Some("Defaults"),
"file" | "files" => Some("File"),
"env" | "environment" => Some("Env"),
"cli" | "commandline" | "command-line" => Some("Cli"),
_ => None,
}
}
fn source_kind_tokens(value: &str, krate: &TokenStream) -> syn::Result<TokenStream> {
let variant: syn::Ident = is_source_kind(value)
.map(|v| syn::Ident::new(v, proc_macro2::Span::call_site()))
.ok_or_else(|| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"unknown precedence source '{value}'; expected defaults, file, env, or cli",
),
)
})?;
Ok(quote! { #krate::docs::SourceKind::#variant })
}
fn build_discovery_metadata(
app_name: &AppName,
struct_attrs: &StructAttrs,
krate: &TokenStream,
) -> TokenStream {
let Some(discovery) = struct_attrs.discovery.as_ref() else {
return quote! { None };
};
let config_file_name = discovery
.config_file_name
.clone()
.unwrap_or_else(|| String::from("config.toml"));
let dotfile_name = discovery.dotfile_name.clone().unwrap_or_else(|| {
default_dotfile_name(app_name, &ConfigFileName::new(config_file_name.clone()))
});
let project_file_name = discovery
.project_file_name
.clone()
.unwrap_or_else(|| dotfile_name.clone());
let formats = collect_formats(
&[&config_file_name, &dotfile_name, &project_file_name],
krate,
);
let override_flag_long = if discovery.config_cli_visible.unwrap_or(false) {
let value = discovery
.config_cli_long
.clone()
.unwrap_or_else(|| String::from("config-path"));
option_string_tokens(Some(value.as_str()))
} else {
quote! { None }
};
let override_env = discovery
.env_var
.clone()
.unwrap_or_else(|| compute_config_env_var(struct_attrs));
let override_env_tokens = option_string_tokens(Some(override_env.as_str()));
quote! {
Some(#krate::docs::ConfigDiscoveryMeta {
formats: vec![ #( #formats ),* ],
search_paths: Vec::new(),
override_flag_long: #override_flag_long,
override_env: #override_env_tokens,
xdg_compliant: ::core::cfg!(any(unix, target_os = "redox")),
})
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum FormatKind {
Toml,
Yaml,
Json,
}
fn format_kind_from_extension(name: &str) -> Option<FormatKind> {
match extension_from_name(name).as_deref() {
Some("toml") => Some(FormatKind::Toml),
Some("yaml" | "yml") => Some(FormatKind::Yaml),
Some("json" | "json5") => Some(FormatKind::Json),
_ => None,
}
}
fn collect_formats(names: &[&str], krate: &TokenStream) -> Vec<TokenStream> {
let mut has_toml = false;
let mut has_yaml = false;
let mut has_json = false;
for name in names {
if let Some(kind) = format_kind_from_extension(name) {
has_toml |= kind == FormatKind::Toml;
has_yaml |= kind == FormatKind::Yaml;
has_json |= kind == FormatKind::Json;
}
}
let mut formats = Vec::new();
if has_toml {
formats.push(quote! { #krate::docs::ConfigFormat::Toml });
}
if has_yaml {
formats.push(quote! { #krate::docs::ConfigFormat::Yaml });
}
if has_json {
formats.push(quote! { #krate::docs::ConfigFormat::Json });
}
formats
}
fn extension_from_name(name: &str) -> Option<String> {
name.rsplit_once('.')
.map(|(_, ext)| ext.trim())
.filter(|ext| !ext.is_empty())
.map(str::to_ascii_lowercase)
}
fn default_dotfile_name(app_name: &AppName, config_file_name: &ConfigFileName) -> String {
let extension = config_file_name
.rsplit_once('.')
.map(|(_, ext)| ext)
.filter(|ext| !ext.is_empty());
if app_name.trim().is_empty() {
let mut name = String::from('.');
name.push_str(extension.unwrap_or("config"));
return name;
}
let mut name = String::from('.');
name.push_str(app_name.trim());
if let Some(ext) = extension {
name.push('.');
name.push_str(ext);
}
name
}