use super::StructItem;
use crate::util::*;
use heck::{ToKebabCase, ToShoutySnakeCase};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use std::fmt::Display;
use syn::{spanned::Spanned, Error, Field, Ident, LitStr, Type};
pub struct FlattenItem {
field_name: Ident,
field_type: Type,
is_optional_type: Option<Type>,
long_prefix: Option<LitStr>,
env_prefix: Option<LitStr>,
description_prefix: Option<String>,
skip_short: Option<LitCharArray>,
}
fn make_long_prefix(ident: &impl Display, span: Span) -> Option<LitStr> {
let formatted = format!("{}-", ident.to_string().to_kebab_case());
Some(LitStr::new(&formatted, span))
}
fn make_env_prefix(ident: &impl Display, span: Span) -> Option<LitStr> {
let formatted = format!("{}_", ident.to_string().to_shouty_snake_case());
Some(LitStr::new(&formatted, span))
}
impl FlattenItem {
pub fn new(field: &Field, _struct_item: &StructItem) -> Result<Self, Error> {
let field_name = field
.ident
.clone()
.ok_or_else(|| Error::new(field.span(), "missing identifier"))?;
let field_type = field.ty.clone();
let is_optional_type = type_is_option(&field.ty)?;
let mut result = Self {
field_name,
field_type,
is_optional_type,
long_prefix: None,
env_prefix: None,
description_prefix: None,
skip_short: None,
};
let mut doc_string: Option<String> = None;
let mut help_prefix: Option<Option<LitStr>> = None;
for attr in &field.attrs {
maybe_append_doc_string(&mut doc_string, &attr.meta)?;
if attr.path().is_ident("conf") || attr.path().is_ident("arg") {
attr.parse_nested_meta(|meta| {
let path = meta.path.clone();
if path.is_ident("flatten") {
Ok(())
} else if path.is_ident("long_prefix") {
set_once(
&path,
&mut result.long_prefix,
parse_optional_value::<LitStr>(meta)?
.or(make_long_prefix(&result.field_name, path.span())),
)
} else if path.is_ident("env_prefix") {
set_once(
&path,
&mut result.env_prefix,
parse_optional_value::<LitStr>(meta)?
.or(make_env_prefix(&result.field_name, path.span())),
)
} else if path.is_ident("help_prefix") {
set_once(
&path,
&mut help_prefix,
Some(parse_optional_value::<LitStr>(meta)?),
)
} else if path.is_ident("prefix") {
let (long_prefix, env_prefix) = match parse_optional_value::<LitStr>(meta)?
{
Some(prefix) => (
make_long_prefix(&prefix.value(), path.span()),
make_env_prefix(&prefix.value(), path.span()),
),
None => (
make_long_prefix(&result.field_name, path.span()),
make_env_prefix(&result.field_name, path.span()),
),
};
set_once(&path, &mut result.long_prefix, long_prefix)?;
set_once(&path, &mut result.env_prefix, env_prefix)?;
Ok(())
} else if path.is_ident("skip_short") {
set_once(
&path,
&mut result.skip_short,
Some(parse_required_value::<LitCharArray>(meta)?),
)
} else {
Err(meta.error("unrecognized conf flatten option"))
}
})?;
}
}
result.description_prefix = help_prefix
.map(|inner| inner.as_ref().map(LitStr::value).or(doc_string))
.unwrap_or_default();
Ok(result)
}
pub fn get_field_name(&self) -> &Ident {
&self.field_name
}
fn get_id_prefix(&self) -> String {
self.field_name.to_string() + "."
}
pub fn get_field_type(&self) -> Type {
self.field_type.clone()
}
pub fn gen_push_program_options(
&self,
program_options_ident: &Ident,
) -> Result<TokenStream, syn::Error> {
let field_type = &self.field_type;
let id_prefix = self.get_id_prefix();
let long_prefix = self
.long_prefix
.as_ref()
.map(LitStr::value)
.unwrap_or_default();
let env_prefix = self
.env_prefix
.as_ref()
.map(LitStr::value)
.unwrap_or_default();
let description_prefix = self.description_prefix.as_deref().unwrap_or_default();
let skip_short = self.skip_short.as_ref().map(|array| &array.elements);
let common_program_option_modifications = quote! {
.apply_flatten_prefixes(#id_prefix, #long_prefix, #env_prefix, #description_prefix)
.skip_short_forms(&[#skip_short])
};
Ok(if let Some(inner_type) = self.is_optional_type.as_ref() {
quote! {
#program_options_ident.extend(
#inner_type::get_program_options()?.iter().cloned().map(
|program_option|
program_option
#common_program_option_modifications
.make_optional()
)
);
}
} else {
quote! {
#program_options_ident.extend(
#field_type::get_program_options()?.iter().cloned().map(
|program_option|
program_option
#common_program_option_modifications
)
);
}
})
}
pub fn gen_initializer(
&self,
conf_context_ident: &Ident,
) -> Result<(TokenStream, bool), syn::Error> {
let field_type = &self.field_type;
let id_prefix = self.get_id_prefix();
if let Some(inner_type) = self.is_optional_type.as_ref() {
Ok((
quote! {
Ok(if let Some(option_appeared_result) = <#inner_type as ::conf::Conf>::any_program_options_appeared(&#conf_context_ident.for_flattened(#id_prefix)).map_err(|err| vec![err])? {
let #conf_context_ident = #conf_context_ident.for_flattened_optional(#id_prefix, <#inner_type as ::conf::Conf>::get_name(), option_appeared_result);
Some(<#inner_type as ::conf::Conf>::from_conf_context(#conf_context_ident)?)
} else {
None
})
},
true,
))
} else {
Ok((
quote! {
let #conf_context_ident = #conf_context_ident.for_flattened(#id_prefix);
<#field_type as ::conf::Conf>::from_conf_context(#conf_context_ident)
},
true,
))
}
}
pub fn any_program_options_appeared_expr(
&self,
conf_context_ident: &Ident,
) -> Result<TokenStream, syn::Error> {
let field_type = &self.field_type;
let id_prefix = self.get_id_prefix();
if let Some(inner_type) = self.is_optional_type.as_ref() {
Ok(
quote! { <#inner_type as ::conf::Conf>::any_program_options_appeared(& #conf_context_ident .for_flattened(#id_prefix)) },
)
} else {
Ok(
quote! { <#field_type as ::conf::Conf>::any_program_options_appeared(& #conf_context_ident .for_flattened(#id_prefix)) },
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_long_prefix() {
let result = make_long_prefix(&"my_field", Span::call_site()).unwrap();
assert_eq!(result.value(), "my-field-");
}
#[test]
fn test_make_env_prefix() {
let result = make_env_prefix(&"my_field", Span::call_site()).unwrap();
assert_eq!(result.value(), "MY_FIELD_");
}
}