diff --git a/.gitignore b/.gitignore index 82b32a0..853c392 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .DS_Store Cargo.lock +*.snap.new diff --git a/examples/struct_with_flatten.rs b/examples/struct_with_flatten.rs index 77f3205..9c1ce2b 100644 --- a/examples/struct_with_flatten.rs +++ b/examples/struct_with_flatten.rs @@ -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, + + // `long_vec_multiple_opt` implies `skip_interactive_input` + // `long_vec_multiple_opt` implies `long` + #[interactive_clap(long_vec_multiple_opt)] + pub env: Vec, } #[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<()> { ::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()) diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs b/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs index ee3b82b..debe791 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs @@ -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 +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs b/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs index 627f9b3..a7cc6b1 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs @@ -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, }) } diff --git a/interactive-clap-derive/src/derives/interactive_clap/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/mod.rs index 8124376..db05a76 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/mod.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/mod.rs @@ -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) => { diff --git a/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs b/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs index 8dc0fd8..4a00cbb 100755 --- a/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs +++ b/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs @@ -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()); + } + }; + } } } } diff --git a/interactive-clap-derive/src/lib.rs b/interactive-clap-derive/src/lib.rs index 2c170f7..daa4fcb 100644 --- a/interactive-clap-derive/src/lib.rs +++ b/interactive-clap-derive/src/lib.rs @@ -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 { diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap new file mode 100644 index 0000000..16a176f --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap @@ -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, +} +impl interactive_clap::ToCli for Args { + type CliVariant = CliArgs; +} +pub struct InteractiveClapContextScopeForArgs { + pub env: Vec, +} +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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::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 { + ::try_parse() + } + pub fn parse() -> CliArgs { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for CliArgs { + fn from(args: Args) -> Self { + Self { env: args.env.into() } + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt_to_cli_args.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt_to_cli_args.snap new file mode 100644 index 0000000..9be608c --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt_to_cli_args.snap @@ -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 { + 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 + } +} diff --git a/interactive-clap-derive/src/tests/test_simple_struct.rs b/interactive-clap-derive/src/tests/test_simple_struct.rs index 4b629fa..5b26aad 100644 --- a/interactive-clap-derive/src/tests/test_simple_struct.rs +++ b/interactive-clap-derive/src/tests/test_simple_struct.rs @@ -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, + } + }; + + 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, + } + }; + + 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)); + +}