mod defaults;
mod resolution;
mod tokens;
mod validation;
mod value_types;
use std::collections::HashMap;
use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
use crate::derive::build::CliFieldMetadata;
use crate::derive::parse::{FieldAttrs, SerdeRenameAll, serde_serialized_field_key};
use self::resolution::{resolve_required, resolve_value_type};
use super::AppName;
use super::{example_tokens, link_tokens, note_tokens, option_char_tokens, option_string_tokens};
use defaults::{default_env_name, default_field_id};
use tokens::{build_possible_values, default_tokens, deprecated_tokens};
use validation::{ensure_unique, validate_env_name, validate_file_key};
use value_types::{ValueTypeModel, is_multi_value, value_type_tokens};
pub(super) struct FieldDocArgs<'a> {
pub app_name: &'a AppName,
pub prefix: Option<&'a str>,
pub fields: &'a [syn::Field],
pub field_attrs: &'a [FieldAttrs],
pub serde_rename_all: Option<SerdeRenameAll>,
pub cli_fields: &'a [CliFieldMetadata],
pub krate: &'a proc_macro2::TokenStream,
}
pub(super) fn build_fields_metadata(args: &FieldDocArgs<'_>) -> syn::Result<Vec<TokenStream>> {
if args.fields.len() != args.field_attrs.len() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"doc field metadata mismatch: expected {} FieldAttrs entries but found {}",
args.fields.len(),
args.field_attrs.len()
),
));
}
let cli_lookup = args
.cli_fields
.iter()
.map(|meta| (meta.field_name.as_str(), meta))
.collect::<HashMap<_, _>>();
let mut builder = FieldMetaBuilder {
app_name: args.app_name,
prefix: args.prefix,
serde_rename_all: args.serde_rename_all,
cli_lookup,
env_seen: HashMap::new(),
file_seen: HashMap::new(),
krate: args.krate,
};
let mut output = Vec::with_capacity(args.fields.len());
for (field, attrs) in args.fields.iter().zip(args.field_attrs) {
if attrs.is_subcommand {
continue;
}
output.push(builder.build_field(field, attrs)?);
}
Ok(output)
}
struct FieldMetaBuilder<'a> {
app_name: &'a AppName,
prefix: Option<&'a str>,
serde_rename_all: Option<SerdeRenameAll>,
cli_lookup: HashMap<&'a str, &'a CliFieldMetadata>,
env_seen: HashMap<String, proc_macro2::Span>,
file_seen: HashMap<String, proc_macro2::Span>,
krate: &'a proc_macro2::TokenStream,
}
impl<'a> FieldMetaBuilder<'a> {
fn build_field(
&mut self,
field: &'a syn::Field,
attrs: &'a FieldAttrs,
) -> syn::Result<TokenStream> {
let name = field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new_spanned(field, "tuple fields are not supported"))?;
let field_name = name.to_string();
let help_id = attrs
.doc
.help_id
.clone()
.unwrap_or_else(|| default_field_id(self.app_name, &field_name, "help"));
let long_help_id = attrs
.doc
.long_help_id
.clone()
.unwrap_or_else(|| default_field_id(self.app_name, &field_name, "long_help"));
let value_type = resolve_value_type(attrs, field);
let required = resolve_required(field, attrs)?;
let value_context = ValueContext {
value_tokens: value_type_tokens(value_type.clone(), self.krate),
required,
};
let context = FieldContext {
name,
field_name: &field_name,
field,
attrs,
value_type: value_type.as_ref(),
};
let io_tokens = self.render_io_tokens(&context)?;
let meta_parts = render_meta_parts(attrs, self.krate);
let identity = FieldIdentity {
field_name: &field_name,
help_id: &help_id,
long_help_id: &long_help_id,
};
let components = FieldMetadataComponents {
identity: identity.into_tokens(),
value_context,
io_tokens,
meta_parts,
};
Ok(render_field_metadata(components, self.krate))
}
fn render_io_tokens(&mut self, context: &FieldContext<'_>) -> syn::Result<IoTokens> {
Ok(IoTokens {
cli: self.build_cli_tokens(context)?,
env: self.build_env_tokens(context)?,
file: self.build_file_tokens(context)?,
})
}
fn build_cli_tokens(&self, context: &FieldContext<'_>) -> syn::Result<TokenStream> {
if context.attrs.skip_cli {
return Ok(quote! { None });
}
let meta = self.cli_lookup.get(context.field_name).ok_or_else(|| {
syn::Error::new_spanned(
context.name,
"missing CLI metadata for field; this is a macro bug",
)
})?;
let long = option_string_tokens(Some(meta.long.as_str()));
let short = option_char_tokens(Some(meta.short));
let value_name = option_string_tokens(context.attrs.doc.cli_value_name.as_deref());
let multiple = is_multi_value(&context.field.ty);
let takes_value = !meta.is_bool;
let possible_values = build_possible_values(context.value_type);
let hide_in_help = context.attrs.doc.cli_hide_in_help;
let krate = self.krate;
Ok(quote! {
Some(#krate::docs::CliMetadata {
long: #long,
short: #short,
value_name: #value_name,
multiple: #multiple,
takes_value: #takes_value,
possible_values: vec![ #( #possible_values ),* ],
hide_in_help: #hide_in_help,
})
})
}
fn build_env_tokens(&mut self, context: &FieldContext<'_>) -> syn::Result<TokenStream> {
let env_name = context
.attrs
.doc
.env_name
.clone()
.unwrap_or_else(|| default_env_name(self.prefix, context.field_name));
validate_env_name(context.name, &env_name)?;
ensure_unique("env", context.name, &env_name, &mut self.env_seen)?;
let lit = syn::LitStr::new(&env_name, proc_macro2::Span::call_site());
let krate = self.krate;
Ok(quote! {
#krate::docs::EnvMetadata {
var_name: String::from(#lit),
}
})
}
fn build_file_tokens(&mut self, context: &FieldContext<'_>) -> syn::Result<TokenStream> {
let key_path = if let Some(key) = context.attrs.doc.file_key_path.clone() {
key
} else {
serde_serialized_field_key(context.field, self.serde_rename_all)?
};
validate_file_key(context.name, &key_path)?;
ensure_unique("file", context.name, &key_path, &mut self.file_seen)?;
let lit = syn::LitStr::new(&key_path, proc_macro2::Span::call_site());
let krate = self.krate;
Ok(quote! {
#krate::docs::FileMetadata {
key_path: String::from(#lit),
}
})
}
}
struct FieldContext<'a> {
name: &'a Ident,
field_name: &'a str,
field: &'a syn::Field,
attrs: &'a FieldAttrs,
value_type: Option<&'a ValueTypeModel>,
}
struct FieldIdentity<'a> {
field_name: &'a str,
help_id: &'a str,
long_help_id: &'a str,
}
struct ValueContext {
value_tokens: TokenStream,
required: bool,
}
struct IoTokens {
cli: TokenStream,
env: TokenStream,
file: TokenStream,
}
struct MetaParts {
default_tokens: TokenStream,
deprecated_tokens: TokenStream,
examples: Vec<TokenStream>,
links: Vec<TokenStream>,
notes: Vec<TokenStream>,
}
fn render_meta_parts(attrs: &FieldAttrs, krate: &proc_macro2::TokenStream) -> MetaParts {
MetaParts {
default_tokens: default_tokens(attrs, krate),
deprecated_tokens: deprecated_tokens(attrs, krate),
examples: example_tokens(&attrs.doc.examples, krate),
links: link_tokens(&attrs.doc.links, krate),
notes: note_tokens(&attrs.doc.notes, krate),
}
}
struct FieldMetadataComponents {
identity: FieldIdentityTokens,
value_context: ValueContext,
io_tokens: IoTokens,
meta_parts: MetaParts,
}
struct FieldIdentityTokens {
field_name: syn::LitStr,
help_id: syn::LitStr,
long_help: syn::LitStr,
}
impl FieldIdentity<'_> {
fn into_tokens(self) -> FieldIdentityTokens {
FieldIdentityTokens {
field_name: syn::LitStr::new(self.field_name, proc_macro2::Span::call_site()),
help_id: syn::LitStr::new(self.help_id, proc_macro2::Span::call_site()),
long_help: syn::LitStr::new(self.long_help_id, proc_macro2::Span::call_site()),
}
}
}
fn render_field_metadata(
components: FieldMetadataComponents,
krate: &proc_macro2::TokenStream,
) -> TokenStream {
let FieldMetadataComponents {
identity,
value_context,
io_tokens,
meta_parts,
} = components;
let identity_tokens = render_identity_tokens(identity);
let value_tokens = render_value_tokens(value_context);
let io = render_io_block(io_tokens);
let meta = render_meta_block(meta_parts);
quote! {
#krate::docs::FieldMetadata {
#identity_tokens
#value_tokens
#io
#meta
}
}
}
fn render_identity_tokens(identity: FieldIdentityTokens) -> TokenStream {
let field_name = identity.field_name;
let help_id = identity.help_id;
let long_help = identity.long_help;
quote! {
name: String::from(#field_name),
help_id: String::from(#help_id),
long_help_id: Some(String::from(#long_help)),
}
}
fn render_value_tokens(value: ValueContext) -> TokenStream {
let value_tokens = value.value_tokens;
let required = value.required;
quote! {
value: #value_tokens,
required: #required,
}
}
fn render_io_block(io: IoTokens) -> TokenStream {
let cli = io.cli;
let env = io.env;
let file = io.file;
quote! {
cli: #cli,
env: Some(#env),
file: Some(#file),
}
}
fn render_meta_block(meta: MetaParts) -> TokenStream {
let default = meta.default_tokens;
let deprecated = meta.deprecated_tokens;
let examples = render_vec_field("examples", &meta.examples);
let links = render_vec_field("links", &meta.links);
let notes = render_vec_field("notes", &meta.notes);
quote! {
default: #default,
deprecated: #deprecated,
#examples
#links
#notes
}
}
fn render_vec_field(field_name: &str, items: &[TokenStream]) -> TokenStream {
let ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
quote! { #ident: vec![ #( #items ),* ], }
}