Skip to content

Commit

Permalink
feat: support tuple variants for subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
funbiscuit committed Feb 10, 2024
1 parent 27ba888 commit 81fb436
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 138 deletions.
162 changes: 84 additions & 78 deletions embedded-cli-macros/src/command/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use convert_case::{Case, Casing};
use darling::{Error, FromField, FromMeta, FromVariant, Result};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{Field, Fields, Variant};
use syn::{Field, Fields, FieldsNamed, FieldsUnnamed, Variant};

use super::args::{ArgType, TypedArg};

Expand All @@ -15,6 +15,7 @@ use super::doc::Help;
struct CommandAttrs {
attrs: Vec<syn::Attribute>,
name: Option<String>,
subcommand: bool,
}

#[derive(Debug)]
Expand Down Expand Up @@ -207,38 +208,26 @@ impl CommandArg {
}

pub struct Subcommand {
pub field_name: String,
pub field_name: Option<String>,
pub field_type: TokenStream,
pub ty: ArgType,
}

impl Subcommand {
fn parse_field(field: &Field) -> Result<Option<Self>> {
let attrs = FieldCommandAttrs::from_field(field)?;

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()
.expect("Only named fields are supported")
.to_string();

let res = if attrs.subcommand {
Some(Self {
field_name,
field_type,
ty,
})
} else {
None
};
let field_name = field.ident.as_ref().map(|ident| ident.to_string());

Ok(res)
Ok(Self {
field_name,
field_type,
ty,
})
}

pub fn full_name(&self) -> String {
Expand All @@ -260,6 +249,7 @@ pub struct Command {
#[cfg(feature = "help")]
pub help: Help,
pub ident: Ident,
pub named_args: bool,
pub subcommand: Option<Subcommand>,
}

Expand All @@ -268,77 +258,93 @@ impl Command {
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)
});

let mut has_positional = false;
let mut subcommand = None;

let args = match &variant.fields {
Fields::Unit => vec![],
Fields::Unnamed(fields) => {
return Err(Error::custom(
"Unnamed/tuple fields are not supported. Use named fields",
)
.with_span(fields));
}
Fields::Named(fields) => {
let mut errors = Error::accumulator();
let args = fields
.named
.iter()
.filter_map(|field| {
errors.handle_in(|| {
if let Some(sub) = Subcommand::parse_field(field)? {

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(sub);

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()?;

args
}
};

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))
}
}
35 changes: 26 additions & 9 deletions embedded-cli-macros/src/command/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,23 @@ fn command_parsing(ident: &Ident, command: &Command) -> TokenStream {
let variant_name = &command.ident;
let variant_fqn = quote! { #ident::#variant_name };

let rhs = if command.args.is_empty() {
let rhs = if command.args.is_empty() && command.subcommand.is_none() {
quote! { #variant_fqn, }
} else {
let (parsing, arguments) = create_arg_parsing(command);
quote! {
{
#parsing
#variant_fqn { #(#arguments)* }
if command.named_args {
quote! {
{
#parsing
#variant_fqn { #(#arguments)* }
}
}
} else {
quote! {
{
#parsing
#variant_fqn ( #(#arguments)* )
}
}
}
};
Expand Down Expand Up @@ -154,8 +163,16 @@ fn create_arg_parsing(command: &Command) -> (TokenStream, Vec<TokenStream>) {

let subcommand_value_arm;
if let Some(subcommand) = &command.subcommand {
let fi_raw = format_ident!("{}", subcommand.field_name);
let fi = format_ident!("sub_{}", subcommand.field_name);
let fi_raw;
let fi;
if let Some(field_name) = &subcommand.field_name {
let ident_raw = format_ident!("{}", field_name);
fi_raw = quote! { #ident_raw: };
fi = format_ident!("sub_{}", field_name);
} else {
fi_raw = quote! {};
fi = format_ident!("sub_command");
}
let ty = &subcommand.field_type;

subcommand_value_arm = Some(quote! {
Expand All @@ -168,11 +185,11 @@ fn create_arg_parsing(command: &Command) -> (TokenStream, Vec<TokenStream>) {
});

let constructor_arg = match subcommand.ty {
ArgType::Option => quote! { #fi_raw: #fi },
ArgType::Option => quote! { #fi_raw #fi },
ArgType::Normal => {
let name = subcommand.full_name();
quote! {
#fi_raw: #fi.ok_or(_cli::service::ParseError::MissingRequiredArgument {
#fi_raw #fi.ok_or(_cli::service::ParseError::MissingRequiredArgument {
name: #name,
})?
}
Expand Down
42 changes: 18 additions & 24 deletions embedded-cli/tests/cli/help_subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ enum CliBase<'a> {
#[arg(short, long)]
name: Option<&'a str>,

/// Some level
#[arg(short, long)]
level: u8,

/// Make things verbose
#[arg(short)]
verbose: bool,
Expand All @@ -24,15 +28,8 @@ enum CliBase<'a> {
},

/// Another base command
#[command(name = "base2")]
Base2 {
/// Some level
#[arg(short, long)]
level: u8,

#[command(subcommand)]
command: CliBase2Sub<'a>,
},
#[command(name = "base2", subcommand)]
Base2(CliBase2Sub<'a>),
}

#[derive(Debug, Clone, Command, PartialEq)]
Expand Down Expand Up @@ -110,15 +107,13 @@ enum Base {
Base1 {
name: Option<String>,

level: u8,

verbose: bool,

command: Base1Sub,
},
Base2 {
level: u8,

command: Base2Sub,
},
Base2(Base2Sub),
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -172,17 +167,16 @@ impl<'a> From<CliBase<'a>> for Base {
match value {
CliBase::Base1 {
name,
level,
verbose,
command,
} => Self::Base1 {
name: name.map(|n| n.to_string()),
verbose,
command: command.into(),
},
CliBase::Base2 { level, command } => Self::Base2 {
level,
verbose,
command: command.into(),
},
CliBase::Base2(command) => Self::Base2(command.into()),
}
}
}
Expand Down Expand Up @@ -252,9 +246,10 @@ impl<'a> From<CliBase2Sub<'a>> for Base2Sub {
"Usage: base1 [OPTIONS] <COMMAND>",
"",
"Options:",
" -n, --name [NAME] Optional argument",
" -v Make things verbose",
" -h, --help Print help",
" -n, --name [NAME] Optional argument",
" -l, --level <LEVEL> Some level",
" -v Make things verbose",
" -h, --help Print help",
"",
"Commands:",
" get Get something",
Expand Down Expand Up @@ -340,11 +335,10 @@ impl<'a> From<CliBase2Sub<'a>> for Base2Sub {
#[case("base2 --help", &[
"Another base command",
"",
"Usage: base2 [OPTIONS] <COMMAND>",
"Usage: base2 <COMMAND>",
"",
"Options:",
" -l, --level <LEVEL> Some level",
" -h, --help Print help",
" -h, --help Print help",
"",
"Commands:",
" get Get something but differently",
Expand Down
Loading

0 comments on commit 81fb436

Please sign in to comment.