use convert_case::{Case, Casing};
use darling::{Error, FromField, FromMeta, FromVariant, Result};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{Expr, Field, Fields, FieldsNamed, FieldsUnnamed, Variant};
use super::args::{ArgType, TypedArg};
#[cfg(feature = "help")]
use super::doc::Help;
#[allow(dead_code)]
#[derive(Debug, FromVariant, Default)]
#[darling(default, attributes(command), forward_attrs(allow, doc, cfg))]
struct CommandAttrs {
attrs: Vec<syn::Attribute>,
name: Option<String>,
subcommand: bool,
}
#[derive(Debug)]
enum LongName {
Generated,
Fixed(String),
}
impl FromMeta for LongName {
fn from_string(value: &str) -> Result<Self> {
if value.is_empty() {
return Err(Error::custom("Name must not be empty"));
}
Ok(Self::Fixed(value.to_string()))
}
fn from_word() -> Result<Self> {
Ok(Self::Generated)
}
}
#[derive(Debug)]
enum ShortName {
Generated,
Fixed(char),
}
impl FromMeta for ShortName {
fn from_char(value: char) -> Result<Self> {
Ok(Self::Fixed(value))
}
fn from_string(value: &str) -> Result<Self> {
let mut it = value.chars();
let value = it
.next()
.ok_or(Error::custom("Short name must have single char"))?;
if it.next().is_some() {
return Err(Error::custom("Short name must have single char"));
}
Self::from_char(value)
}
fn from_word() -> Result<Self> {
Ok(Self::Generated)
}
}
#[derive(Debug)]
enum TypedDefault {
Generated,
Fixed(Expr),
}
impl FromMeta for TypedDefault {
fn from_expr(value: &Expr) -> Result<Self> {
Ok(Self::Fixed(value.clone()))
}
fn from_word() -> Result<Self> {
Ok(Self::Generated)
}
}
#[derive(Debug, FromField, Default)]
#[darling(default, attributes(arg), forward_attrs(allow, doc, cfg))]
struct ArgAttrs {
default_value: Option<String>,
default_value_t: Option<TypedDefault>,
short: Option<ShortName>,
long: Option<LongName>,
value_name: Option<String>,
}
#[derive(Debug, FromField, Default)]
#[darling(default, attributes(command), forward_attrs(allow, doc, cfg))]
struct FieldCommandAttrs {
subcommand: bool,
}
#[derive(Debug, Eq, PartialEq)]
pub enum CommandArgType {
Flag {
long: Option<String>,
short: Option<char>,
},
Option {
long: Option<String>,
short: Option<char>,
},
Positional,
}
#[allow(unused)]
impl CommandArgType {
pub fn is_option(&self) -> bool {
matches!(
self,
CommandArgType::Flag { .. } | CommandArgType::Option { .. }
)
}
pub fn is_positional(&self) -> bool {
self == &CommandArgType::Positional
}
}
pub struct CommandArg {
pub arg_type: CommandArgType,
pub default_value: Option<TokenStream>,
pub field_name: String,
pub field_type: TokenStream,
#[cfg(feature = "help")]
pub help: Help,
pub ty: ArgType,
pub value_name: String,
}
impl CommandArg {
fn parse(field: &Field) -> Result<Self> {
let arg_attrs = ArgAttrs::from_field(field)?;
let field_name = field
.ident
.as_ref()
.expect("Only named fields are supported")
.to_string();
let value_name = arg_attrs.value_name.unwrap_or(field_name.to_uppercase());
let short = arg_attrs.short.map(|s| match s {
ShortName::Generated => field_name.chars().next().unwrap(),
ShortName::Fixed(c) => c,
});
if let Some(short) = short {
if !short.is_ascii_alphabetic() {
return Err(Error::custom("Flag char must be alphabetic ASCII"));
}
}
let long = arg_attrs.long.map(|s| match s {
LongName::Generated => field_name.from_case(Case::Snake).to_case(Case::Kebab),
LongName::Fixed(name) => name,
});
if let Some(long) = &long {
if long.chars().any(|c| !c.is_ascii_alphanumeric() && c != '-') {
return Err(Error::custom(
"Option name must consist of alphanumeric ASCII chars",
));
}
}
let aa = TypedArg::new(&field.ty);
let ty = aa.ty();
let field_type = aa.inner();
let field_type = quote! { #field_type };
let arg_type = if long.is_some() || short.is_some() {
if field_type.to_string() == "bool" {
CommandArgType::Flag { long, short }
} else {
CommandArgType::Option { long, short }
}
} else {
CommandArgType::Positional
};
let default_value = match (arg_attrs.default_value, arg_attrs.default_value_t) {
(Some(value), None) => Some(quote! {
<#field_type as _cli::arguments::FromArgument>::from_arg(#value)?,
}),
(None, Some(value)) => {
let value = match value {
TypedDefault::Generated => quote! { Default::default() },
TypedDefault::Fixed(expr) => quote! { #expr },
};
Some(quote! { #value })
}
(None, None) => None,
_ => {
return Err(Error::custom(
"Only one of default_value or default_value_t is allowed",
))
}
};
Ok(Self {
arg_type,
default_value,
field_name,
field_type,
#[cfg(feature = "help")]
help: Help::parse(&field.attrs)?,
ty,
value_name,
})
}
pub fn full_name(&self) -> String {
match &self.arg_type {
CommandArgType::Flag { long, short } => long
.as_ref()
.map(|name| format!("--{}", name))
.or(short.map(|n| format!("-{}", n)))
.unwrap(),
CommandArgType::Option { long, short } => {
let prefix = long
.as_ref()
.map(|name| format!("--{}", name))
.or(short.map(|n| format!("-{}", n)))
.unwrap();
if self.is_optional() {
format!("{} [{}]", prefix, self.value_name)
} else {
format!("{} <{}>", prefix, self.value_name)
}
}
CommandArgType::Positional => {
if self.is_optional() {
format!("[{}]", self.value_name)
} else {
format!("<{}>", self.value_name)
}
}
}
}
pub fn is_optional(&self) -> bool {
self.ty == ArgType::Option
}
}
pub struct Subcommand {
pub field_name: Option<String>,
pub field_type: TokenStream,
pub ty: ArgType,
}
impl Subcommand {
fn parse_field(field: &Field) -> Result<Self> {
let arg = TypedArg::new(&field.ty);
let ty = arg.ty();
let field_type = arg.inner();
let field_type = quote! { #field_type };
let field_name = field.ident.as_ref().map(|ident| ident.to_string());
Ok(Self {
field_name,
field_type,
ty,
})
}
pub fn full_name(&self) -> String {
if self.is_optional() {
"[COMMAND]".to_string()
} else {
"<COMMAND>".to_string()
}
}
pub fn is_optional(&self) -> bool {
self.ty == ArgType::Option
}
}
pub struct Command {
pub name: String,
pub args: Vec<CommandArg>,
#[cfg(feature = "help")]
pub help: Help,
pub ident: Ident,
pub named_args: bool,
pub subcommand: Option<Subcommand>,
}
impl Command {
pub fn parse(variant: &Variant) -> Result<Self> {
let variant_ident = &variant.ident;
let attrs = CommandAttrs::from_variant(variant)?;
let (named_args, (args, subcommand)) = match &variant.fields {
Fields::Unit => (false, (vec![], None)),
Fields::Unnamed(fields) => (false, Self::parse_tuple_variant(&attrs, fields)?),
Fields::Named(fields) => (true, Self::parse_struct_variant(fields)?),
};
let name = attrs.name.unwrap_or_else(|| {
variant_ident
.to_string()
.from_case(Case::Camel)
.to_case(Case::Kebab)
});
Ok(Self {
name,
args,
#[cfg(feature = "help")]
help: Help::parse(&attrs.attrs)?,
ident: variant_ident.clone(),
named_args,
subcommand,
})
}
fn parse_struct_variant(fields: &FieldsNamed) -> Result<(Vec<CommandArg>, Option<Subcommand>)> {
let mut has_positional = false;
let mut subcommand = None;
let mut errors = Error::accumulator();
let args = fields
.named
.iter()
.filter_map(|field| {
errors
.handle_in(|| {
let command_attrs = FieldCommandAttrs::from_field(field)?;
if command_attrs.subcommand {
if has_positional {
return Err(Error::custom(
"Command cannot have both positional arguments and subcommand",
)
.with_span(&field.ident));
}
if subcommand.is_some() {
return Err(Error::custom(
"Command can have only single subcommand",
)
.with_span(&field.ident));
}
subcommand = Some(Subcommand::parse_field(field)?);
Ok(None)
} else {
let arg = CommandArg::parse(field)?;
if arg.arg_type.is_positional() && subcommand.is_some() {
return Err(Error::custom(
"Command cannot have both positional arguments and subcommand",
)
.with_span(&field.ident));
}
has_positional |= arg.arg_type.is_positional();
Ok(Some(arg))
}
})
.flatten()
})
.collect::<Vec<_>>();
errors.finish()?;
Ok((args, subcommand))
}
fn parse_tuple_variant(
attrs: &CommandAttrs,
fields: &FieldsUnnamed,
) -> Result<(Vec<CommandArg>, Option<Subcommand>)> {
if fields.unnamed.len() != 1 {
return Err(Error::custom("Tuple variant must have single argument").with_span(&fields));
}
if !attrs.subcommand {
return Err(Error::custom("Tuple variant must be a subcommand").with_span(&fields));
}
let subcommand = Some(Subcommand::parse_field(&fields.unnamed[0])?);
Ok((vec![], subcommand))
}
}