feat: Added support for "#[interactive_clap(flatten)]" (#15)

Co-authored-by: FroVolod <frol_off@meta.ua>
This commit is contained in:
FroVolod
2024-03-25 13:10:47 +02:00
committed by GitHub
parent 2f55008fc2
commit d07d192fc6
11 changed files with 303 additions and 198 deletions

View File

@@ -0,0 +1,61 @@
// This example shows additional functionality of the "interactive-clap" macro for parsing command-line data into a structure using the macro's flatten attributes.
// 1) build an example: cargo build --example struct_with_flatten
// 2) go to the `examples` folder: cd target/debug/examples
// 3) run an example: ./struct_with_flatten (without parameters) => entered interactive mode
// ./struct_with_flatten QWERTY 18 => account: CliAccount { social_db_folder: None, account: Some(CliSender { sender_account_id: Some("QWERTY"), age: Some(18) }) }
// To learn more about the parameters, use "help" flag: ./struct_with_flatten --help
use interactive_clap::{ResultFromCli, ToCliArgs};
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
struct Account {
/// Change SocialDb prefix
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
social_db_folder: Option<String>,
#[interactive_clap(flatten)]
account: Sender,
}
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
pub struct Sender {
/// What is the sender account ID?
pub sender_account_id: String,
/// How old is the sender?
pub age: u64,
}
fn main() -> color_eyre::Result<()> {
let mut cli_account = Account::parse();
let context = (); // default: input_context = ()
loop {
let account = <Account as interactive_clap::FromCli>::from_cli(Some(cli_account), context);
match account {
ResultFromCli::Ok(cli_account) | ResultFromCli::Cancel(Some(cli_account)) => {
println!("account: {cli_account:?}");
println!(
"Your console command: {}",
shell_words::join(&cli_account.to_cli_args())
);
return Ok(());
}
ResultFromCli::Cancel(None) => {
println!("Goodbye!");
return Ok(());
}
ResultFromCli::Back => {
cli_account = Default::default();
}
ResultFromCli::Err(cli_account, err) => {
if let Some(cli_account) = cli_account {
println!(
"Your console command: {}",
shell_words::join(&cli_account.to_cli_args())
);
}
return Err(err);
}
}
}
}

View File

@@ -0,0 +1,14 @@
extern crate proc_macro;
use syn;
pub fn is_field_with_flatten(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}
field
.attrs
.iter()
.flat_map(|attr| attr.tokens.clone())
.any(|attr_token| attr_token.to_string().contains("flatten"))
}

View File

@@ -0,0 +1,18 @@
extern crate proc_macro;
use syn;
pub fn is_field_with_skip_default_input_arg(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}
field
.attrs
.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.any(|attr_token| {
attr_token.to_string().contains("skip_default_input_arg")
|| attr_token.to_string().contains("flatten")
})
}

View File

@@ -0,0 +1,17 @@
extern crate proc_macro;
use syn;
pub fn is_field_with_subcommand(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}
field
.attrs
.iter()
.flat_map(|attr| attr.tokens.clone())
.any(|attr_token| {
attr_token.to_string().contains("named_arg")
|| attr_token.to_string().contains("subcommand")
})
}

View File

@@ -1,24 +0,0 @@
extern crate proc_macro;
use syn;
pub fn is_field_without_skip_default_input_arg(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return true;
}
match field
.attrs
.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.find(|attr_token| match attr_token {
proc_macro2::TokenTree::Group(group) => group
.stream()
.to_string()
.contains("skip_default_input_arg"),
_ => false, // abort_call_site!("Only option `TokenTree::Group` is needed")
}) {
Some(_token_stream) => false,
None => true,
}
}

View File

@@ -1,25 +0,0 @@
extern crate proc_macro;
use syn;
pub fn is_field_without_subcommand(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return true;
}
match field
.attrs
.iter()
.flat_map(|attr| attr.tokens.clone())
.find(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => {
group.stream().to_string().contains("named_arg")
|| group.stream().to_string().contains("subcommand")
}
_ => false, // abort_call_site!("Only option `TokenTree::Group` is needed")
}
}) {
Some(_token_stream) => false,
None => true,
}
}

View File

@@ -17,9 +17,12 @@ pub fn from_cli_for_struct(
return quote!();
};
let fields_without_subcommand = fields
let fields_without_subcommand_and_flatten = fields
.iter()
.filter(|field| super::fields_without_subcommand::is_field_without_subcommand(field))
.filter(|field| {
!super::fields_with_subcommand::is_field_with_subcommand(field)
&& !super::fields_with_flatten::is_field_with_flatten(field)
})
.map(|field| {
let ident_field = &field.clone().ident.expect("this field does not exist");
quote! {#ident_field: #ident_field.into()}
@@ -31,25 +34,23 @@ pub fn from_cli_for_struct(
.map(fields_value)
.filter(|token_stream| !token_stream.is_empty());
let field_value_named_arg = if let Some(token_stream) = fields
let field_value_named_arg = fields
.iter()
.map(|field| field_value_named_arg(name, field))
.find(|token_stream| !token_stream.is_empty())
{
token_stream
} else {
quote!()
};
.unwrap_or(quote!());
let field_value_subcommand = if let Some(token_stream) = fields
let field_value_subcommand = fields
.iter()
.map(field_value_subcommand)
.find(|token_stream| !token_stream.is_empty())
{
token_stream
} else {
quote!()
};
.unwrap_or(quote!());
let field_value_flatten = fields
.iter()
.map(field_value_flatten)
.find(|token_stream| !token_stream.is_empty())
.unwrap_or(quote!());
let input_context_dir = interactive_clap_attrs_context
.clone()
@@ -60,7 +61,7 @@ pub fn from_cli_for_struct(
Span::call_site(),
);
let new_context_scope = quote! {
let new_context_scope = #interactive_clap_context_scope_for_struct { #(#fields_without_subcommand,)* };
let new_context_scope = #interactive_clap_context_scope_for_struct { #(#fields_without_subcommand_and_flatten,)* };
};
let output_context = match &interactive_clap_attrs_context.output_context_dir {
@@ -84,10 +85,11 @@ pub fn from_cli_for_struct(
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.unwrap_or_default();
let mut clap_variant = optional_clap_variant.clone().unwrap_or_default();
#(#fields_value)*
#new_context_scope
#output_context
#field_value_flatten
#field_value_named_arg
#field_value_subcommand;
interactive_clap::ResultFromCli::Ok(clap_variant)
@@ -105,6 +107,8 @@ fn fields_value(field: &syn::Field) -> proc_macro2::TokenStream {
quote! {
let #ident_field = clap_variant.#ident_field.clone();
}
} else if super::fields_with_flatten::is_field_with_flatten(field) {
quote!()
} else if field
.ty
.to_token_stream()
@@ -121,7 +125,7 @@ fn fields_value(field: &syn::Field) -> proc_macro2::TokenStream {
};
let #ident_field = clap_variant.#ident_field.clone();
}
} else if super::fields_without_subcommand::is_field_without_subcommand(field) {
} else if !super::fields_with_subcommand::is_field_with_subcommand(field) {
quote! {
if clap_variant.#ident_field.is_none() {
clap_variant
@@ -144,56 +148,54 @@ fn field_value_named_arg(name: &syn::Ident, field: &syn::Field) -> proc_macro2::
if field.attrs.is_empty() {
quote!()
} else {
match field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
let type_string = match ty {
syn::Type::Path(type_path) => {
match type_path.path.segments.last() {
Some(path_segment) => path_segment.ident.to_string(),
_ => String::new()
}
},
_ => String::new()
};
let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site());
let variant_name_string = crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string());
let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site());
quote! {
let optional_field = match clap_variant.#ident_field.take() {
Some(#enum_for_clap_named_arg::#variant_name(cli_arg)) => Some(cli_arg),
None => None,
field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
let type_string = match ty {
syn::Type::Path(type_path) => {
match type_path.path.segments.last() {
Some(path_segment) => path_segment.ident.to_string(),
_ => String::new()
}
},
_ => String::new()
};
match <#ty as interactive_clap::FromCli>::from_cli(
optional_field,
context.into(),
) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant.#ident_field = Some(#enum_for_clap_named_arg::#variant_name(cli_field));
}
interactive_clap::ResultFromCli::Cancel(optional_cli_field) => {
clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name);
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back,
interactive_clap::ResultFromCli::Err(optional_cli_field, err) => {
clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name);
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site());
let variant_name_string = crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string());
let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site());
quote! {
let optional_field = match clap_variant.#ident_field.take() {
Some(#enum_for_clap_named_arg::#variant_name(cli_arg)) => Some(cli_arg),
None => None,
};
match <#ty as interactive_clap::FromCli>::from_cli(
optional_field,
context.into(),
) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant.#ident_field = Some(#enum_for_clap_named_arg::#variant_name(cli_field));
}
interactive_clap::ResultFromCli::Cancel(optional_cli_field) => {
clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name);
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back,
interactive_clap::ResultFromCli::Err(optional_cli_field, err) => {
clap_variant.#ident_field = optional_cli_field.map(#enum_for_clap_named_arg::#variant_name);
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
}
}
}
})
.next() {
Some(token_stream) => token_stream,
None => quote! ()
}
})
.next()
.unwrap_or(quote!())
}
}
@@ -203,40 +205,77 @@ fn field_value_subcommand(field: &syn::Field) -> proc_macro2::TokenStream {
if field.attrs.is_empty() {
quote!()
} else {
match field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("subcommand"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
quote! {
match <#ty as interactive_clap::FromCli>::from_cli(clap_variant.#ident_field.take(), context.into()) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant.#ident_field = Some(cli_field);
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back,
interactive_clap::ResultFromCli::Err(option_cli_field, err) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("subcommand"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
quote! {
match <#ty as interactive_clap::FromCli>::from_cli(clap_variant.#ident_field.take(), context.into()) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant.#ident_field = Some(cli_field);
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back,
interactive_clap::ResultFromCli::Err(option_cli_field, err) => {
clap_variant.#ident_field = option_cli_field;
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
}
}
}
})
.next() {
Some(token_stream) => token_stream,
None => quote! ()
}
})
.next()
.unwrap_or(quote!())
}
}
fn field_value_flatten(field: &syn::Field) -> proc_macro2::TokenStream {
let ident_field = &field.clone().ident.expect("this field does not exist");
let ty = &field.ty;
if field.attrs.is_empty() {
quote!()
} else {
field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("flatten"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
quote! {
match #ty::from_cli(
optional_clap_variant.unwrap_or_default().#ident_field,
context.into(),
) {
interactive_clap::ResultFromCli::Ok(cli_field) => clap_variant.#ident_field = Some(cli_field),
interactive_clap::ResultFromCli::Cancel(optional_cli_field) => {
clap_variant.#ident_field = optional_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Back => return interactive_clap::ResultFromCli::Back,
interactive_clap::ResultFromCli::Err(optional_cli_field, err) => {
clap_variant.#ident_field = optional_cli_field;
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
}
})
.next()
.unwrap_or(quote!())
}
}

View File

@@ -12,9 +12,9 @@ pub fn vec_fn_input_arg(
super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
let vec_fn_input_arg = fields
.iter()
.filter(|field| super::fields_without_subcommand::is_field_without_subcommand(field))
.filter(|field| !super::fields_with_subcommand::is_field_with_subcommand(field))
.filter(|field| {
super::fields_without_skip_default_input_arg::is_field_without_skip_default_input_arg(
!super::fields_with_skip_default_input_arg::is_field_with_skip_default_input_arg(
field,
)
})

View File

@@ -1,7 +1,8 @@
pub mod choose_variant;
pub mod cli_field_type;
pub mod fields_without_skip_default_input_arg;
pub mod fields_without_subcommand;
pub mod fields_with_flatten;
pub mod fields_with_skip_default_input_arg;
pub mod fields_with_subcommand;
pub mod from_cli_for_enum;
pub mod from_cli_for_struct;
pub mod input_arg;

View File

@@ -121,56 +121,52 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
.collect::<Vec<_>>();
let context_scope_for_struct = context_scope_for_struct(name, context_scope_fields);
let clap_enum_for_named_arg =
if let Some(token_stream) = fields.iter().find_map(|field| {
let ident_field = &field.clone().ident.expect("this field does not exist");
let variant_name_string = crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string());
let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site());
let attr_doc_vec: Vec<_> = field.attrs.iter()
.filter(|attr| attr.path.is_ident("doc"))
.map(|attr| attr.into_token_stream())
.collect();
field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
let ty = &field.ty;
let type_string = match ty {
syn::Type::Path(type_path) => {
match type_path.path.segments.last() {
Some(path_segment) => path_segment.ident.to_string(),
_ => String::new()
}
},
_ => String::new()
};
let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site());
quote! {
#[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)]
pub enum #enum_for_clap_named_arg {
#(#attr_doc_vec)*
#variant_name(<#ty as interactive_clap::ToCli>::CliVariant)
let clap_enum_for_named_arg = fields.iter().find_map(|field| {
let ident_field = &field.clone().ident.expect("this field does not exist");
let variant_name_string = crate::helpers::snake_case_to_camel_case::snake_case_to_camel_case(ident_field.to_string());
let variant_name = &syn::Ident::new(&variant_name_string, Span::call_site());
let attr_doc_vec: Vec<_> = field.attrs.iter()
.filter(|attr| attr.path.is_ident("doc"))
.map(|attr| attr.into_token_stream())
.collect();
field.attrs.iter()
.filter(|attr| attr.path.is_ident("interactive_clap"))
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
.map(|_| {
let ty = &field.ty;
let type_string = match ty {
syn::Type::Path(type_path) => {
match type_path.path.segments.last() {
Some(path_segment) => path_segment.ident.to_string(),
_ => String::new()
}
},
_ => String::new()
};
let enum_for_clap_named_arg = syn::Ident::new(&format!("ClapNamedArg{}For{}", &type_string, &name), Span::call_site());
quote! {
#[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)]
pub enum #enum_for_clap_named_arg {
#(#attr_doc_vec)*
#variant_name(<#ty as interactive_clap::ToCli>::CliVariant)
}
impl From<#ty> for #enum_for_clap_named_arg {
fn from(item: #ty) -> Self {
Self::#variant_name(<#ty as interactive_clap::ToCli>::CliVariant::from(item))
}
impl From<#ty> for #enum_for_clap_named_arg {
fn from(item: #ty) -> Self {
Self::#variant_name(<#ty as interactive_clap::ToCli>::CliVariant::from(item))
}
}
})
.next()
}) {
token_stream
} else {
quote! ()
};
}
})
.next()
})
.unwrap_or(quote!());
quote! {
#[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)]
@@ -222,7 +218,6 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
for attr in &variant.attrs {
if attr.path.is_ident("doc") {
attrs.push(attr.into_token_stream());
// break;
};
if attr.path.is_ident("cfg") {
for attr_token in attr.tokens.clone() {
@@ -368,7 +363,9 @@ fn context_scope_for_struct(
fn context_scope_for_struct_field(field: &syn::Field) -> proc_macro2::TokenStream {
let ident_field = &field.ident.clone().expect("this field does not exist");
let ty = &field.ty;
if self::methods::fields_without_subcommand::is_field_without_subcommand(field) {
if !self::methods::fields_with_subcommand::is_field_with_subcommand(field)
&& !self::methods::fields_with_flatten::is_field_with_flatten(field)
{
quote! {
pub #ident_field: #ty
}

View File

@@ -42,6 +42,13 @@ impl InteractiveClapAttrsCliField {
.unwrap_or_default();
});
}
if ident == "flatten" {
args_without_attrs = quote! {
if let Some(arg) = &self.#ident_field {
args.append(&mut arg.to_cli_args())
}
};
}
if ident == "value_enum" {
args_without_attrs = quote! {
if let Some(arg) = &self.#ident_field {