feat: Add support for "subargs" (#17)

This commit is contained in:
FroVolod
2024-04-21 14:35:25 +03:00
committed by GitHub
parent da7ce56b59
commit 62f08fd6ad
11 changed files with 360 additions and 51 deletions
+2 -1
View File
@@ -25,5 +25,6 @@ shell-words = "1.0.0"
clap = { version = "4.0.18", features = ["derive"] }
inquire = "0.6"
atty = "0.2.14"
colored = "2.0"
color-eyre = "0.6"
+3 -3
View File
@@ -3,8 +3,8 @@
// 1) build an example: cargo build --example advanced_enum
// 2) go to the `examples` folder: cd target/debug/examples
// 3) run an example: ./advanced_enum (without parameters) => entered interactive mode
// ./advanced_enum network => mode: Ok(Network)
// ./advanced_enum offline => mode: Ok(Offline)
// ./advanced_enum network 23 QWE ASDFG => mode: Network(CliArgs { age: Some(23), first_name: Some("QWE"), second_name: Some("ASDFG") })
// ./advanced_enum offline => mode: Offline
// To learn more about the parameters, use "help" flag: ./advanced_enum --help
use interactive_clap::{ResultFromCli, ToCliArgs};
@@ -62,7 +62,7 @@ fn main() -> color_eyre::Result<()> {
}
}
};
println!("cli_mode: {:?}", mode);
println!("mode: {:?}", mode);
println!(
"Your console command: {}",
shell_words::join(&mode.to_cli_args())
+261 -23
View File
@@ -3,40 +3,278 @@
// 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) }) }
// ./struct_with_flatten --no-docker --no-abi --out-dir /Users/Documents/Rust --color never test.testnet offline => contract: CliContract { build_command_args: Some(CliBuildCommand { no_docker: true, no_release: false, no_abi: true, no_embed_abi: false, no_doc: false, out_dir: Some("/Users/Documents/Rust"), manifest_path: None, color: Some(Never) }), contract_account_id: Some("test.testnet"), mode: Some(Offline) }
// To learn more about the parameters, use "help" flag: ./struct_with_flatten --help
// Note: currently there is no automatic generation of "interactive clap::From Cli"
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(input_context = ())]
#[interactive_clap(output_context = ContractContext)]
#[interactive_clap(skip_default_from_cli)]
pub struct Contract {
#[interactive_clap(flatten)]
account: Sender,
/// Specify a build command args:
build_command_args: BuildCommand,
/// What is the contract account ID?
contract_account_id: String,
#[interactive_clap(subcommand)]
pub mode: Mode,
}
#[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,
#[derive(Debug, Clone)]
pub struct ContractContext;
impl ContractContext {
pub fn from_previous_context(
previous_context: (),
scope: &<Contract as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
) -> color_eyre::eyre::Result<Self> {
// Your commands
Ok(Self)
}
}
impl interactive_clap::FromCli for Contract {
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.unwrap_or_default();
let build_command_args =
if let Some(cli_build_command_args) = &clap_variant.build_command_args {
BuildCommand {
no_docker: cli_build_command_args.no_docker,
no_release: cli_build_command_args.no_release,
no_abi: cli_build_command_args.no_abi,
no_embed_abi: cli_build_command_args.no_embed_abi,
no_doc: cli_build_command_args.no_doc,
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(),
}
} else {
BuildCommand::default()
};
if clap_variant.contract_account_id.is_none() {
clap_variant.contract_account_id = match Self::input_contract_account_id(&context) {
Ok(Some(contract_account_id)) => Some(contract_account_id),
Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)),
Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err),
};
}
let contract_account_id = clap_variant
.contract_account_id
.clone()
.expect("Unexpected error");
let new_context_scope = InteractiveClapContextScopeForContract {
build_command_args,
contract_account_id,
};
let output_context =
match ContractContext::from_previous_context(context, &new_context_scope) {
Ok(new_context) => new_context,
Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err),
};
match <Mode as interactive_clap::FromCli>::from_cli(
clap_variant.mode.take(),
context.into(),
) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant.mode = Some(cli_field);
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.mode = option_cli_field;
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
interactive_clap::ResultFromCli::Cancel(option_cli_field) => {
clap_variant.mode = 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.mode = option_cli_field;
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
#[derive(Debug, Default, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(input_context = ())]
#[interactive_clap(output_context = BuildCommandlContext)]
pub struct BuildCommand {
/// Build contract without SourceScan verification
#[interactive_clap(long)]
pub no_docker: bool,
/// Build contract in debug mode, without optimizations and bigger is size
#[interactive_clap(long)]
pub no_release: bool,
/// Do not generate ABI for the contract
#[interactive_clap(long)]
pub no_abi: bool,
/// Do not embed the ABI in the contract binary
#[interactive_clap(long)]
pub no_embed_abi: bool,
/// Do not include rustdocs in the embedded ABI
#[interactive_clap(long)]
pub no_doc: bool,
/// Copy final artifacts to this directory
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
pub out_dir: Option<String>,
/// Path to the `Cargo.toml` of the contract to build
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
pub manifest_path: Option<String>,
/// Coloring: auto, always, never
#[interactive_clap(long)]
#[interactive_clap(value_enum)]
#[interactive_clap(skip_interactive_input)]
pub color: Option<ColorPreference>,
}
#[derive(Debug, Clone)]
pub struct BuildCommandlContext {
build_command_args: BuildCommand,
}
impl BuildCommandlContext {
pub fn from_previous_context(
_previous_context: (),
scope: &<BuildCommand as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
) -> color_eyre::eyre::Result<Self> {
let build_command_args = BuildCommand {
no_docker: scope.no_docker,
no_release: scope.no_release,
no_abi: scope.no_abi,
no_embed_abi: scope.no_embed_abi,
no_doc: scope.no_doc,
out_dir: scope.out_dir.clone(),
manifest_path: scope.manifest_path.clone(),
color: scope.color.clone(),
};
Ok(Self { build_command_args })
}
}
use strum::{EnumDiscriminants, EnumIter, EnumMessage};
#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)]
#[strum_discriminants(derive(EnumMessage, EnumIter))]
///To construct a transaction you will need to provide information about sender (signer) and receiver accounts, and actions that needs to be performed.
///Do you want to derive some information required for transaction construction automatically querying it online?
pub enum Mode {
/// Prepare and, optionally, submit a new transaction with online mode
#[strum_discriminants(strum(message = "Yes, I keep it simple"))]
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
#[strum_discriminants(strum(
message = "No, I want to work in no-network (air-gapped) environment"
))]
Offline,
}
use std::{env, str::FromStr};
#[derive(Debug, EnumDiscriminants, Clone, clap::ValueEnum)]
#[strum_discriminants(derive(EnumMessage, EnumIter))]
pub enum ColorPreference {
Auto,
Always,
Never,
}
impl interactive_clap::ToCli for ColorPreference {
type CliVariant = ColorPreference;
}
impl std::fmt::Display for ColorPreference {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Auto => write!(f, "auto"),
Self::Always => write!(f, "always"),
Self::Never => write!(f, "never"),
}
}
}
impl FromStr for ColorPreference {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(default_mode()),
"always" => Ok(ColorPreference::Always),
"never" => Ok(ColorPreference::Never),
_ => Err(format!("invalid color preference: {}", s)),
}
}
}
fn default_mode() -> ColorPreference {
match env::var("NO_COLOR") {
Ok(v) if v != "0" => ColorPreference::Never,
_ => {
if atty::is(atty::Stream::Stderr) {
ColorPreference::Always
} else {
ColorPreference::Never
}
}
}
}
impl ColorPreference {
pub fn as_str(&self) -> &str {
match self {
ColorPreference::Auto => "auto",
ColorPreference::Always => "always",
ColorPreference::Never => "never",
}
}
pub fn apply(&self) {
match self {
ColorPreference::Auto => {
default_mode().apply();
}
ColorPreference::Always => colored::control::set_override(true),
ColorPreference::Never => colored::control::set_override(false),
}
}
}
fn main() -> color_eyre::Result<()> {
let mut cli_account = Account::parse();
let mut cli_contract = Contract::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:?}");
let contract =
<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!(
"Your console command: {}",
shell_words::join(&cli_account.to_cli_args())
shell_words::join(&cli_contract.to_cli_args())
);
return Ok(());
}
@@ -45,13 +283,13 @@ fn main() -> color_eyre::Result<()> {
return Ok(());
}
ResultFromCli::Back => {
cli_account = Default::default();
cli_contract = Default::default();
}
ResultFromCli::Err(cli_account, err) => {
if let Some(cli_account) = cli_account {
ResultFromCli::Err(cli_contract, err) => {
if let Some(cli_contract) = cli_contract {
println!(
"Your console command: {}",
shell_words::join(&cli_account.to_cli_args())
shell_words::join(&cli_contract.to_cli_args())
);
}
return Err(err);
+61
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 subargs attributes.
// 1) build an example: cargo build --example struct_with_subargs
// 2) go to the `examples` folder: cd target/debug/examples
// 3) run an example: ./struct_with_subargs (without parameters) => entered interactive mode
// ./struct_with_subargs 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_subargs --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(subargs)]
account: Sender,
}
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
pub struct Sender {
/// What is the sender account ID?
sender_account_id: String,
/// How old is the sender?
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);
}
}
}
}
@@ -14,5 +14,6 @@ pub fn is_field_with_skip_default_input_arg(field: &syn::Field) -> bool {
.any(|attr_token| {
attr_token.to_string().contains("skip_default_input_arg")
|| attr_token.to_string().contains("flatten")
|| attr_token.to_string().contains("subargs")
})
}
@@ -2,7 +2,7 @@ extern crate proc_macro;
use syn;
pub fn is_field_with_flatten(field: &syn::Field) -> bool {
pub fn is_field_with_subargs(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}
@@ -10,5 +10,5 @@ pub fn is_field_with_flatten(field: &syn::Field) -> bool {
.attrs
.iter()
.flat_map(|attr| attr.tokens.clone())
.any(|attr_token| attr_token.to_string().contains("flatten"))
.any(|attr_token| attr_token.to_string().contains("subargs"))
}
@@ -2,6 +2,7 @@ extern crate proc_macro;
use syn;
/// This function selects fields with: subcommand, named_arg
pub fn is_field_with_subcommand(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
@@ -17,11 +17,11 @@ pub fn from_cli_for_struct(
return quote!();
};
let fields_without_subcommand_and_flatten = fields
let fields_without_subcommand_and_subargs = fields
.iter()
.filter(|field| {
!super::fields_with_subcommand::is_field_with_subcommand(field)
&& !super::fields_with_flatten::is_field_with_flatten(field)
&& !super::fields_with_subargs::is_field_with_subargs(field)
})
.map(|field| {
let ident_field = &field.clone().ident.expect("this field does not exist");
@@ -46,9 +46,9 @@ pub fn from_cli_for_struct(
.find(|token_stream| !token_stream.is_empty())
.unwrap_or(quote!());
let field_value_flatten = fields
let field_value_subargs = fields
.iter()
.map(field_value_flatten)
.map(field_value_subargs)
.find(|token_stream| !token_stream.is_empty())
.unwrap_or(quote!());
@@ -61,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_and_flatten,)* };
let new_context_scope = #interactive_clap_context_scope_for_struct { #(#fields_without_subcommand_and_subargs,)* };
};
let output_context = match &interactive_clap_attrs_context.output_context_dir {
@@ -89,7 +89,7 @@ pub fn from_cli_for_struct(
#(#fields_value)*
#new_context_scope
#output_context
#field_value_flatten
#field_value_subargs
#field_value_named_arg
#field_value_subcommand;
interactive_clap::ResultFromCli::Ok(clap_variant)
@@ -107,8 +107,6 @@ 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()
@@ -125,7 +123,9 @@ fn fields_value(field: &syn::Field) -> proc_macro2::TokenStream {
};
let #ident_field = clap_variant.#ident_field.clone();
}
} else if !super::fields_with_subcommand::is_field_with_subcommand(field) {
} else if !super::fields_with_subcommand::is_field_with_subcommand(field)
&& !super::fields_with_subargs::is_field_with_subargs(field)
{
quote! {
if clap_variant.#ident_field.is_none() {
clap_variant
@@ -153,7 +153,7 @@ fn field_value_named_arg(name: &syn::Ident, field: &syn::Field) -> proc_macro2::
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
proc_macro2::TokenTree::Group(group) => group.stream().to_string() == *"named_arg",
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
@@ -241,7 +241,7 @@ fn field_value_subcommand(field: &syn::Field) -> proc_macro2::TokenStream {
}
}
fn field_value_flatten(field: &syn::Field) -> proc_macro2::TokenStream {
fn field_value_subargs(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() {
@@ -252,7 +252,7 @@ fn field_value_flatten(field: &syn::Field) -> proc_macro2::TokenStream {
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("flatten"),
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("subargs"),
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
@@ -1,7 +1,7 @@
pub mod choose_variant;
pub mod cli_field_type;
pub mod fields_with_flatten;
pub mod fields_with_skip_default_input_arg;
pub mod fields_with_subargs;
pub mod fields_with_subcommand;
pub mod from_cli_for_enum;
pub mod from_cli_for_struct;
@@ -31,18 +31,18 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
let mut clap_attr_vec: Vec<proc_macro2::TokenStream> = Vec::new();
let mut cfg_attr_vec: Vec<proc_macro2::TokenStream> = Vec::new();
for attr in &field.attrs {
if attr.path.is_ident("interactive_clap") | attr.path.is_ident("cfg") {
if attr.path.is_ident("interactive_clap") || attr.path.is_ident("cfg") {
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")
|| group.stream().to_string().contains("value_enum")
|| group.stream().to_string().contains("long")
|| (group.stream().to_string() == *"skip")
|| (group.stream().to_string() == *"flatten")
{
clap_attr_vec.push(group.stream())
} else if group.stream().to_string().contains("named_arg") {
} else if group.stream().to_string() == *"named_arg" {
let ident_subcommand =
syn::Ident::new("subcommand", Span::call_site());
clap_attr_vec.push(quote! {#ident_subcommand});
@@ -71,6 +71,11 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
if group.stream().to_string().contains("feature") {
cfg_attr_vec.push(attr.into_token_stream())
};
if group.stream().to_string().contains("subargs") {
let ident_subargs =
syn::Ident::new("flatten", Span::call_site());
clap_attr_vec.push(quote! {#ident_subargs});
};
if group.stream().to_string() == *"skip" {
ident_skip_field_vec.push(ident_field.clone());
cli_field = quote!()
@@ -134,7 +139,7 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
.flat_map(|attr| attr.tokens.clone())
.filter(|attr_token| {
match attr_token {
proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"),
proc_macro2::TokenTree::Group(group) => group.stream().to_string() == *"named_arg",
_ => abort_call_site!("Only option `TokenTree::Group` is needed")
}
})
@@ -364,7 +369,7 @@ fn context_scope_for_struct_field(field: &syn::Field) -> proc_macro2::TokenStrea
let ident_field = &field.ident.clone().expect("this field does not exist");
let ty = &field.ty;
if !self::methods::fields_with_subcommand::is_field_with_subcommand(field)
&& !self::methods::fields_with_flatten::is_field_with_flatten(field)
&& !self::methods::fields_with_subargs::is_field_with_subargs(field)
{
quote! {
pub #ident_field: #ty
@@ -45,7 +45,9 @@ impl InteractiveClapAttrsCliField {
if ident == "flatten" {
args_without_attrs = quote! {
if let Some(arg) = &self.#ident_field {
args.append(&mut arg.to_cli_args())
let mut to_cli_args = arg.to_cli_args();
to_cli_args.append(&mut args);
std::mem::swap(&mut args, &mut to_cli_args);
}
};
}