use crate::util::*;
use heck::{ToKebabCase, ToSnakeCase};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
meta::ParseNestedMeta, spanned::Spanned, token, Error, Fields, FieldsUnnamed, Ident, LitStr,
Type, Variant,
};
pub struct VariantSerdeItem {
pub rename: Option<LitStr>,
pub skip: bool,
span: Span,
}
impl VariantSerdeItem {
pub fn new(meta: ParseNestedMeta<'_>) -> Result<Self, Error> {
let mut result = Self {
rename: None,
skip: false,
span: meta.input.span(),
};
if meta.input.peek(token::Paren) {
meta.parse_nested_meta(|meta| {
let path = meta.path.clone();
if path.is_ident("rename") {
set_once(
&path,
&mut result.rename,
Some(parse_required_value::<LitStr>(meta)?),
)
} else if path.is_ident("skip") {
result.skip = true;
Ok(())
} else {
Err(meta.error("unrecognized conf(serde) option"))
}
})?;
}
Ok(result)
}
}
impl GetSpan for VariantSerdeItem {
fn get_span(&self) -> Span {
self.span
}
}
pub struct VariantItem {
variant_name: Ident,
variant_type: Option<Type>, is_optional_type: Option<Type>, command_name: LitStr,
serde: Option<VariantSerdeItem>,
doc_string: Option<String>,
}
impl VariantItem {
pub fn new(variant: &Variant, _enum_ident: &Ident) -> Result<Self, Error> {
let variant_name = variant.ident.clone();
let (variant_type, is_optional_type) = match variant.fields {
Fields::Unit => { (None, None) },
Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => {
if unnamed.len() != 1 {
return Err(Error::new(
unnamed.span(),
"Subcommands variant must contain zero or one unnamed fields which implement Conf",
));
}
let field = unnamed.first().unwrap();
let variant_type = field.ty.clone();
let is_optional_type = type_is_option(&variant_type)?;
(Some(variant_type), is_optional_type)
},
_ => {
return Err(Error::new(
variant.fields.span(),
"Subcommands variant must contain zero or one unnamed fields which implement Conf",
));
}
};
let mut result = Self {
command_name: LitStr::new(
&variant_name.to_string().to_kebab_case(),
variant_name.span(),
),
variant_name,
variant_type,
is_optional_type,
serde: None,
doc_string: None,
};
let mut command_name_override: Option<LitStr> = None;
for attr in &variant.attrs {
maybe_append_doc_string(&mut result.doc_string, &attr.meta)?;
if attr.path().is_ident("conf") || attr.path().is_ident("subcommands") {
attr.parse_nested_meta(|meta| {
let path = meta.path.clone();
if path.is_ident("subcommand") {
Ok(())
} else if path.is_ident("name") {
set_once(
&path,
&mut command_name_override,
Some(parse_required_value::<LitStr>(meta)?),
)
} else if path.is_ident("serde") {
set_once(&path, &mut result.serde, Some(VariantSerdeItem::new(meta)?))
} else {
Err(meta.error("unrecognized conf subcommands option"))
}
})?;
}
}
if let Some(command_name) = command_name_override {
result.command_name = command_name;
}
Ok(result)
}
pub fn get_name(&self) -> &Ident {
&self.variant_name
}
pub fn get_command_name(&self) -> &LitStr {
&self.command_name
}
pub fn get_serde_name(&self) -> LitStr {
self.serde
.as_ref()
.and_then(|serde| serde.rename.clone())
.unwrap_or_else(|| {
LitStr::new(
&self.variant_name.to_string().to_snake_case(),
self.variant_name.span(),
)
})
}
pub fn get_serde_skip(&self) -> bool {
self.serde.as_ref().map(|serde| serde.skip).unwrap_or(false)
}
pub fn gen_from_conf_context_match_arm(&self, conf_context_ident: &Ident) -> Result<TokenStream, Error> {
let name = self.get_name();
let command_name = self.get_command_name();
if let Some(ty) = self.variant_type.as_ref() {
Ok(quote! {
#command_name => Ok(Self::#name(<#ty as Conf>::from_conf_context(#conf_context_ident)?))
})
} else {
Ok(quote! {
#command_name => Ok(Self::#name)
})
}
}
pub fn gen_from_conf_serde_context_match_arm(&self, conf_context_ident: &Ident, next_value_producer_ident: &Ident) -> Result<TokenStream, Error> {
let name = self.get_name();
let command_name = self.get_command_name();
if self.get_serde_skip() {
Ok(quote! {})
} else if let Some(ty) = self.variant_type.as_ref() {
let serde_name = self.get_serde_name();
Ok(quote! {
#command_name => {
let document_name = #conf_context_ident.document_name;
let seed = <#ty as ConfSerde>::Seed::from(ctxt);
Ok(Self::#name(#next_value_producer_ident.next_value_seed(seed).map_err(|err| {
vec![InnerError::serde(
document_name,
#serde_name,
err
)]
})??))
}
})
} else {
Ok(quote! {
#command_name => Ok(Self::#name)
})
}
}
pub fn gen_push_parsers(
&self,
parsers_ident: &Ident,
parsed_env_ident: &Ident,
) -> Result<TokenStream, Error> {
let command_name = &self.command_name;
if let Some(ty) = self.variant_type.as_ref() {
let inner_type = self.is_optional_type.as_ref().unwrap_or(ty);
Ok(quote! {
#parsers_ident.push(
<#inner_type as ::conf::Conf>::get_parser(#parsed_env_ident)?
.rename(#command_name)
);
})
} else {
Ok(quote! {
#parsers_ident.push(
::conf::Parser::new(::conf::ParserConfig::default(), &[], &[], #parsed_env_ident)?
.rename(#command_name)
);
})
}
}
}