feat: add long_vec_multiple_opt attribute (#22)

screenshot from `cargo-near` with
[patched](06fd2569ec)
`interactive-clap` version from pr's fork:


![image](https://github.com/user-attachments/assets/a624c011-2a0c-4830-95da-58a0bf553648)

---------

Co-authored-by: dj8yf0μl <noreply@nowhere.org>
This commit is contained in:
dj8yf0μl
2024-09-18 18:27:10 +03:00
committed by GitHub
parent a7031fcabf
commit 94442d867e
10 changed files with 206 additions and 13 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target
.DS_Store
Cargo.lock
*.snap.new

View File

@@ -63,6 +63,7 @@ impl interactive_clap::FromCli for Contract {
out_dir: cli_build_command_args.out_dir.clone(),
manifest_path: cli_build_command_args.manifest_path.clone(),
color: cli_build_command_args.color.clone(),
env: cli_build_command_args.env.clone(),
}
} else {
BuildCommand::default()
@@ -150,6 +151,11 @@ pub struct BuildCommand {
#[interactive_clap(value_enum)]
#[interactive_clap(skip_interactive_input)]
pub color: Option<ColorPreference>,
// `long_vec_multiple_opt` implies `skip_interactive_input`
// `long_vec_multiple_opt` implies `long`
#[interactive_clap(long_vec_multiple_opt)]
pub env: Vec<String>,
}
#[derive(Debug, Clone)]
@@ -171,6 +177,7 @@ impl BuildCommandlContext {
out_dir: scope.out_dir.clone(),
manifest_path: scope.manifest_path.clone(),
color: scope.color.clone(),
env: scope.env.clone(),
};
Ok(Self { build_command_args })
}
@@ -193,7 +200,7 @@ pub enum Mode {
Offline,
}
use std::{env, str::FromStr};
use std::str::FromStr;
#[derive(Debug, EnumDiscriminants, Clone, clap::ValueEnum)]
#[strum_discriminants(derive(EnumMessage, EnumIter))]
@@ -252,7 +259,7 @@ fn main() -> color_eyre::Result<()> {
<Contract as interactive_clap::FromCli>::from_cli(Some(cli_contract), context);
match contract {
ResultFromCli::Ok(cli_contract) | ResultFromCli::Cancel(Some(cli_contract)) => {
println!("contract: {cli_contract:?}");
println!("contract: {cli_contract:#?}");
println!(
"Your console command: {}",
shell_words::join(&cli_contract.to_cli_args())

View File

@@ -37,3 +37,14 @@ pub fn cli_field_type(ty: &syn::Type) -> proc_macro2::TokenStream {
_ => abort_call_site!("Only option `Type::Path` is needed"),
}
}
pub fn starts_with_vec(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty {
if let Some(path_segment) = type_path.path.segments.first() {
if path_segment.ident == "Vec" {
return true;
}
}
}
false
}

View File

@@ -2,6 +2,9 @@ extern crate proc_macro;
use syn;
use crate::LONG_VEC_MUTLIPLE_OPT;
pub fn is_skip_interactive_input(field: &syn::Field) -> bool {
field
.attrs
@@ -9,10 +12,11 @@ pub fn is_skip_interactive_input(field: &syn::Field) -> bool {
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.any(|attr_token| match attr_token {
proc_macro2::TokenTree::Group(group) => group
.stream()
.to_string()
.contains("skip_interactive_input"),
proc_macro2::TokenTree::Group(group) => {
let group_string = group.stream().to_string();
group_string.contains("skip_interactive_input")
|| group_string.contains(LONG_VEC_MUTLIPLE_OPT)
}
_ => false,
})
}

View File

@@ -1,11 +1,14 @@
extern crate proc_macro;
use methods::cli_field_type;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use syn;
mod methods;
use crate::LONG_VEC_MUTLIPLE_OPT;
pub(crate) mod methods;
pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
@@ -35,13 +38,16 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
for attr_token in attr.tokens.clone() {
match attr_token {
proc_macro2::TokenTree::Group(group) => {
if group.stream().to_string().contains("subcommand")
|| group.stream().to_string().contains("value_enum")
|| group.stream().to_string().contains("long")
|| (group.stream().to_string() == *"skip")
|| (group.stream().to_string() == *"flatten")
let group_string = group.stream().to_string();
if group_string.contains("subcommand")
|| group_string.contains("value_enum")
|| group_string.contains("long")
|| (group_string == *"skip")
|| (group_string == *"flatten")
{
clap_attr_vec.push(group.stream())
if group_string != LONG_VEC_MUTLIPLE_OPT {
clap_attr_vec.push(group.stream())
}
} else if group.stream().to_string() == *"named_arg" {
let ident_subcommand =
syn::Ident::new("subcommand", Span::call_site());
@@ -80,6 +86,18 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
ident_skip_field_vec.push(ident_field.clone());
cli_field = quote!()
};
if group.stream().to_string() == LONG_VEC_MUTLIPLE_OPT {
if !cli_field_type::starts_with_vec(ty) {
abort_call_site!("`{}` attribute is only supposed to be used with `Vec` types", LONG_VEC_MUTLIPLE_OPT)
}
// implies `#[interactive_clap(long)]`
clap_attr_vec.push(quote! { long });
// type goes into output unchanged, otherwise it
// prevents clap deriving correctly its `remove_many` thing
cli_field = quote! {
pub #ident_field: #ty
};
}
}
_ => {
abort_call_site!("Only option `TokenTree::Group` is needed")
@@ -410,6 +428,21 @@ fn for_cli_field(
quote!()
} else {
let ty = &field.ty;
if field.attrs.iter().any(|attr|
attr.path.is_ident("interactive_clap") &&
attr.tokens.clone().into_iter().any(
|attr_token|
matches!(
attr_token,
proc_macro2::TokenTree::Group(group) if group.stream().to_string() == LONG_VEC_MUTLIPLE_OPT
)
)
) {
return quote! {
#ident_field: args.#ident_field.into()
};
}
match &ty {
syn::Type::Path(type_path) => match type_path.path.segments.first() {
Some(path_segment) => {

View File

@@ -5,6 +5,8 @@ use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use syn;
use crate::derives::interactive_clap::methods::cli_field_type;
#[derive(Debug, Clone)]
pub enum InteractiveClapAttrsCliField {
RegularField(proc_macro2::TokenStream),
@@ -66,6 +68,7 @@ impl InteractiveClapAttrsCliField {
&ident_field_to_kebab_case_string,
Span::call_site(),
);
if field.ty.to_token_stream().to_string() == "bool"
{
unnamed_args = quote! {
@@ -80,6 +83,14 @@ impl InteractiveClapAttrsCliField {
args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string());
}
};
if cli_field_type::starts_with_vec(&field.ty) {
unnamed_args = quote! {
for arg in self.#ident_field.iter().rev() {
args.push_front(arg.to_string());
args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string());
}
};
}
}
}
}

View File

@@ -8,6 +8,14 @@ mod helpers;
#[cfg(test)]
mod tests;
/// `#[interactive_clap(...)]` attribute used for specifying multiple values with `Vec<..>` type,
/// by repeating corresponding flag `--field-name` (kebab case) for each value
///
/// implies `#[interactive_clap(long)]`
///
/// implies `#[interactive_clap(skip_interactive_input)]`, as it's not intended for interactive input
pub(crate) const LONG_VEC_MUTLIPLE_OPT: &str = "long_vec_multiple_opt";
#[proc_macro_derive(InteractiveClap, attributes(interactive_clap))]
#[proc_macro_error]
pub fn interactive_clap(input: TokenStream) -> TokenStream {

View File

@@ -0,0 +1,60 @@
---
source: interactive-clap-derive/src/tests/test_simple_struct.rs
expression: pretty_codegen(&interactive_clap_codegen)
---
#[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)]
#[clap(author, version, about, long_about = None)]
pub struct CliArgs {
#[clap(long)]
pub env: Vec<String>,
}
impl interactive_clap::ToCli for Args {
type CliVariant = CliArgs;
}
pub struct InteractiveClapContextScopeForArgs {
pub env: Vec<String>,
}
impl interactive_clap::ToInteractiveClapContextScope for Args {
type InteractiveClapContextScope = InteractiveClapContextScopeForArgs;
}
impl interactive_clap::FromCli for Args {
type FromCliContext = ();
type FromCliError = color_eyre::eyre::Error;
fn from_cli(
optional_clap_variant: Option<<Self as interactive_clap::ToCli>::CliVariant>,
context: Self::FromCliContext,
) -> interactive_clap::ResultFromCli<
<Self as interactive_clap::ToCli>::CliVariant,
Self::FromCliError,
>
where
Self: Sized + interactive_clap::ToCli,
{
let mut clap_variant = optional_clap_variant.clone().unwrap_or_default();
let env = clap_variant.env.clone();
let new_context_scope = InteractiveClapContextScopeForArgs {
env: env.into(),
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
impl Args {
pub fn try_parse() -> Result<CliArgs, clap::Error> {
<CliArgs as clap::Parser>::try_parse()
}
pub fn parse() -> CliArgs {
<CliArgs as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<CliArgs, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<CliArgs as clap::Parser>::try_parse_from(itr)
}
}
impl From<Args> for CliArgs {
fn from(args: Args) -> Self {
Self { env: args.env.into() }
}
}

View File

@@ -0,0 +1,14 @@
---
source: interactive-clap-derive/src/tests/test_simple_struct.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for CliArgs {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
let mut args = std::collections::VecDeque::new();
for arg in self.env.iter().rev() {
args.push_front(arg.to_string());
args.push_front(std::concat!("--", "env").to_string());
}
args
}
}

View File

@@ -44,3 +44,47 @@ fn test_flag() {
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
#[test]
fn test_vec_multiple_opt() {
let input = syn::parse_quote! {
struct Args {
#[interactive_clap(long_vec_multiple_opt)]
pub env: Vec<String>,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
}
#[test]
fn test_vec_multiple_opt_to_cli_args() {
let input = syn::parse_quote! {
pub struct CliArgs {
#[clap(long)]
pub env: Vec<String>,
}
};
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
#[test]
// testing correct panic msg isn't really very compatible with
// `proc-macro-error` crate
#[should_panic]
fn test_vec_multiple_opt_err() {
let input = syn::parse_quote! {
struct Args {
#[interactive_clap(long_vec_multiple_opt)]
pub env: String,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
}