use darling::FromAttributes as _;
use heck::ToKebabCase as _;
use itertools::Itertools as _;
use syn::{Fields, Ident, Token, Type, Variant, punctuated::Punctuated, spanned::Spanned as _};
use crate::common::{Description, IdentString, ParsedFieldInfo, compute_docs, error_pair};
use super::create_non_colliding_ident;
pub enum Fallback<'a> {
Explicit(&'a Ident),
Internal(Ident),
}
impl Fallback<'_> {
pub fn ident(&self) -> &Ident {
match *self {
Fallback::Explicit(ident) => ident,
Fallback::Internal(ref ident) => ident,
}
}
}
#[derive(darling::FromAttributes)]
#[darling(attributes(debate))]
struct VariantAttr {
fallback: Option<()>,
}
pub enum SubcommandVariantMode<'a> {
Unit,
Newtype { ty: &'a Type },
Struct { fields: Vec<ParsedFieldInfo<'a>> },
}
pub enum SubcommandVariantNormalizedMode<'s, 'a> {
Fields(&'s [ParsedFieldInfo<'a>]),
Newtype(&'a Type),
}
impl<'a> SubcommandVariantMode<'a> {
#[must_use]
pub fn normalized(&self) -> SubcommandVariantNormalizedMode<'_, 'a> {
match *self {
Self::Unit => SubcommandVariantNormalizedMode::Fields(&[]),
Self::Newtype { ty } => SubcommandVariantNormalizedMode::Newtype(ty),
Self::Struct { ref fields } => SubcommandVariantNormalizedMode::Fields(fields),
}
}
}
pub struct ParsedSubcommandVariant<'a> {
pub ident: IdentString<'a>,
pub command: String,
pub docs: Description,
pub mode: SubcommandVariantMode<'a>,
}
pub struct ParsedSubcommandInfo<'a> {
pub fallback: Fallback<'a>,
pub variants: Vec<ParsedSubcommandVariant<'a>>,
}
impl<'a> ParsedSubcommandInfo<'a> {
pub fn from_variants(variants: &'a Punctuated<Variant, Token![,]>) -> syn::Result<Self> {
if variants.is_empty() {
return Err(syn::Error::new(
variants.span(),
"must have at least one variant",
));
}
let mut fallback: Option<&Ident> = None;
let mut parsed_variants = Vec::new();
for variant in variants {
let attr = VariantAttr::from_attributes(&variant.attrs)?;
if let Some(()) = attr.fallback {
if let Some(fallback) = fallback {
return Err(error_pair(
variant.span(),
"can't have more than one fallback variant",
fallback.span(),
"previous fallback here",
));
}
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new(
variant.span(),
"fallback variant must be a unit variant",
));
}
fallback = Some(&variant.ident);
} else {
let ident = IdentString::new(&variant.ident);
let command = ident.as_str().to_kebab_case();
let docs = compute_docs(&variant.attrs)?;
let mode = match variant.fields {
Fields::Unit => SubcommandVariantMode::Unit,
Fields::Named(ref fields) => SubcommandVariantMode::Struct {
fields: fields
.named
.iter()
.map(ParsedFieldInfo::from_field)
.try_collect()?,
},
Fields::Unnamed(ref fields) => SubcommandVariantMode::Newtype {
ty: &fields
.unnamed
.iter()
.exactly_one()
.map_err(|_| {
syn::Error::new(
fields.span(),
"tuple variants must be newtype variants",
)
})?
.ty,
},
};
parsed_variants.push(ParsedSubcommandVariant {
ident,
command,
mode,
docs,
});
}
}
let fallback = match fallback {
Some(fallback) => Fallback::Explicit(fallback),
None => Fallback::Internal(create_non_colliding_ident(
"Fallback",
parsed_variants.iter().map(|variant| &variant.ident),
)),
};
Ok(Self {
fallback,
variants: parsed_variants,
})
}
}