feat: propagate doc comments on flags and arguments to --help/-h + structs derive refactor (#26)

the crux of the pr is
0c3ce3af6f
and
912744d436

the rest is some refactoring and splitting big chunks of code into
smaller and nested modules/functions

---

the changes to the logic of derives aren't many and are well summarised
by
the following list of commits with snapshot differences
from the same tests as added to `master` branch in  

https://github.com/dj8yfo/interactive-clap/commits/backporting_tests_into_master/
branch:


*
[test_simple_struct](985a46571b/interactive-clap-derive/src/tests/test_simple_struct.rs (L4-L26))
=>
eb4b1243f1
(fragments reorder)
*
[test_simple_struct_with_named_arg](985a46571b/interactive-clap-derive/src/tests/test_simple_struct.rs (L29-L49))
=>
ddb38910a9
(fragments reorder)
*
[test_doc_comments_propagate](985a46571b/interactive-clap-derive/src/tests/test_simple_struct.rs (L151-L208))
=>
6461299b8e
(doc comments propagated, `clap(verbatim_doc_comment)` propagated,
fragments reordered)
*
[test_simple_enum](985a46571b/interactive-clap-derive/src/tests/test_simple_enum.rs (L4-L28))
=> no change
*
[test_simple_enum_with_strum_discriminants](985a46571b/interactive-clap-derive/src/tests/test_simple_enum.rs (L31-L61))
=> no change
* also a bug was found when integrating new functionality:
* [new
test](985a46571b/interactive-clap-derive/src/tests/test_simple_struct.rs (L51-L75))
* [snapshot change in `InteractiveClap`
derive](19b20993c1),
test passing
* [input change for 2nd stage derive of
`ToCliArgs`](29c9aeaf7f)
resulted in [test
failing](https://github.com/near-cli-rs/interactive-clap/actions/runs/13120732036/job/36605779439)


---

second (or 3rd) sub-summary mentions 2 commits
382cc33d61,
3db17e1e90
which made
a testing step automatic and not requiring to manually copy-paste a
fragment
from snapshot generated with first test assertion.
No snapshots changed as result of these latter 2 commits.

---------

Co-authored-by: dj8yf0μl <noreply@nowhere.org>
Co-authored-by: Artur Yurii Korchynskyi <42449190+akorchyn@users.noreply.github.com>
This commit is contained in:
dj8yf0μl
2025-02-11 12:50:41 +02:00
committed by GitHub
parent e917d5584f
commit 962ddb9561
48 changed files with 1704 additions and 480 deletions

View File

@@ -20,3 +20,23 @@ jobs:
profile: minimal
- name: Tests
run: cargo test --workspace
# there're sometimes warnings, which signal, that the generated doc
# won't look as expected, when rendered, and sometimes errors, which will prevent doc from being
# generated at release time altogether.
cargo-doc:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
default: true
- name: run cargo doc
env:
RUSTDOCFLAGS: -D warnings
run: |
cargo doc -p interactive-clap
cargo doc -p interactive-clap-derive --document-private-items

View File

@@ -11,9 +11,14 @@ use interactive_clap::{ResultFromCli, ToCliArgs};
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
struct Account {
/// Change SocialDb prefix
///
/// It's a paraghraph, describing, this argument usage in more detail
/// than just the headline
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
#[interactive_clap(verbatim_doc_comment)]
social_db_folder: Option<String>,
/// Sender account
#[interactive_clap(subargs)]
account: Sender,
}

View File

@@ -21,4 +21,12 @@ syn = "1"
[dev-dependencies]
prettyplease = "0.1"
insta = "1"
syn = { version = "1", features = ["full"] }
syn = { version = "1", features = ["full", "extra-traits"] }
[package.metadata.docs.rs]
# Additional `RUSTDOCFLAGS` to set (default: [])
rustdoc-args = ["--document-private-items"]
[features]
default = []
introspect = []

View File

@@ -0,0 +1,24 @@
#[cfg(feature = "introspect")]
macro_rules! dbg_cond {
($val:expr) => {
dbg!($val)
};
}
/// this macro under `introspect` feature can be used to debug how derive proc macros
/// ([`crate::InteractiveClap`], [`crate::ToCliArgs`]) work
///
/// ```bash
/// # interactive-clap-derive folder
/// cargo test test_doc_comments_propagate --features introspect -- --nocapture
/// # from repo root
/// cargo run --example struct_with_subargs --features interactive-clap-derive/introspect
/// ```
#[cfg(not(feature = "introspect"))]
macro_rules! dbg_cond {
($val:expr) => {
#[allow(unused)]
#[allow(clippy::no_effect)]
$val
};
}

View File

@@ -9,6 +9,7 @@ pub fn fn_choose_variant(
ast: &syn::DeriveInput,
variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
) -> proc_macro2::TokenStream {
dbg_cond!("entered `fn_choose_variant`");
let name = &ast.ident;
let interactive_clap_attrs_context =
super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
@@ -19,7 +20,8 @@ pub fn fn_choose_variant(
let mut ast_attrs: Vec<&str> = std::vec::Vec::new();
if !ast.attrs.is_empty() {
for attr in ast.attrs.clone() {
for (_index, attr) in ast.attrs.clone().into_iter().enumerate() {
dbg_cond!((_index, &attr));
if attr.path.is_ident("interactive_clap") {
for attr_token in attr.tokens.clone() {
if let proc_macro2::TokenTree::Group(group) = attr_token {
@@ -29,16 +31,25 @@ pub fn fn_choose_variant(
}
}
};
dbg_cond!(attr.path.is_ident("strum_discriminants"));
if attr.path.is_ident("strum_discriminants") {
for attr_token in attr.tokens.clone() {
if let proc_macro2::TokenTree::Group(group) = attr_token {
if &group.stream().to_string() == "derive(EnumMessage, EnumIter)" {
let group_stream_no_whitespace = group
.stream()
.to_string()
.split_whitespace()
.collect::<Vec<_>>()
.join("");
dbg_cond!(&group_stream_no_whitespace);
if &group_stream_no_whitespace == "derive(EnumMessage,EnumIter)" {
ast_attrs.push("strum_discriminants");
};
}
}
};
}
dbg_cond!(&ast_attrs);
if ast_attrs.contains(&"strum_discriminants") {
let doc_attrs = ast
.attrs

View File

@@ -0,0 +1,4 @@
pub mod choose_variant;
pub mod fields_with_skip_default_input_arg;
pub mod from_cli_for_enum;
pub mod interactive_clap_attrs_context;

View File

@@ -1,10 +0,0 @@
pub mod choose_variant;
pub mod cli_field_type;
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;
pub mod input_arg;
pub mod interactive_clap_attrs_context;
pub mod skip_interactive_input;

View File

@@ -1,241 +1,27 @@
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;
use crate::LONG_VEC_MUTLIPLE_OPT;
/// these are common methods, reused for both the [structs] and `enums` derives
pub(super) mod common_methods;
pub(crate) mod methods;
fn get_names(ast: &syn::DeriveInput) -> (&syn::Ident, syn::Ident) {
let name = &ast.ident;
let cli_name = {
let cli_name_string = format!("Cli{}", name);
syn::Ident::new(&cli_name_string, Span::call_site())
};
(name, cli_name)
}
pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let cli_name_string = format!("Cli{}", &ast.ident);
let cli_name = &syn::Ident::new(&cli_name_string, Span::call_site());
let (name, cli_name) = get_names(ast);
match &ast.data {
syn::Data::Struct(data_struct) => {
let fields = data_struct.fields.clone();
let mut ident_skip_field_vec: Vec<syn::Ident> = Vec::new();
let cli_fields = fields
.iter()
.map(|field| {
let ident_field = field.ident.clone().expect("this field does not exist");
let ty = &field.ty;
let cli_ty = self::methods::cli_field_type::cli_field_type(ty);
let mut cli_field = quote! {
pub #ident_field: #cli_ty
};
if field.attrs.is_empty() {
return cli_field;
};
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") {
for attr_token in attr.tokens.clone() {
match attr_token {
proc_macro2::TokenTree::Group(group) => {
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")
{
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());
clap_attr_vec.push(quote! {#ident_subcommand});
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(),
);
cli_field = quote! {
pub #ident_field: Option<#enum_for_clap_named_arg>
}
};
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!()
};
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")
}
}
}
}
}
if cli_field.is_empty() {
return cli_field;
};
let cfg_attrs = cfg_attr_vec.iter();
if !clap_attr_vec.is_empty() {
let clap_attrs = clap_attr_vec.iter();
quote! {
#(#cfg_attrs)*
#[clap(#(#clap_attrs, )*)]
#cli_field
}
} else {
quote! {
#(#cfg_attrs)*
#cli_field
}
}
})
.filter(|token_stream| !token_stream.is_empty())
.collect::<Vec<_>>();
let for_cli_fields = fields
.iter()
.map(|field| for_cli_field(field, &ident_skip_field_vec))
.filter(|token_stream| !token_stream.is_empty());
let fn_from_cli_for_struct =
self::methods::from_cli_for_struct::from_cli_for_struct(ast, &fields);
let vec_fn_input_arg = self::methods::input_arg::vec_fn_input_arg(ast, &fields);
let context_scope_fields = fields
.iter()
.map(context_scope_for_struct_field)
.filter(|token_stream| !token_stream.is_empty())
.collect::<Vec<_>>();
let context_scope_for_struct = context_scope_for_struct(name, context_scope_fields);
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() == *"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))
}
}
}
})
.next()
})
.unwrap_or(quote!());
quote! {
#[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)]
#[clap(author, version, about, long_about = None)]
pub struct #cli_name {
#( #cli_fields, )*
}
impl interactive_clap::ToCli for #name {
type CliVariant = #cli_name;
}
#context_scope_for_struct
#fn_from_cli_for_struct
impl #name {
#(#vec_fn_input_arg)*
pub fn try_parse() -> Result<#cli_name, clap::Error> {
<#cli_name as clap::Parser>::try_parse()
}
pub fn parse() -> #cli_name {
<#cli_name as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<#cli_name, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<#cli_name as clap::Parser>::try_parse_from(itr)
}
}
impl From<#name> for #cli_name {
fn from(args: #name) -> Self {
Self {
#( #for_cli_fields, )*
}
}
}
#clap_enum_for_named_arg
}
self::structs::token_stream(name, &cli_name, ast, &data_struct.fields)
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
let enum_variants = variants.iter().map(|variant| {
@@ -321,10 +107,11 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
let scope_for_enum = context_scope_for_enum(name);
let fn_choose_variant = self::methods::choose_variant::fn_choose_variant(ast, variants);
let fn_choose_variant =
self::common_methods::choose_variant::fn_choose_variant(ast, variants);
let fn_from_cli_for_enum =
self::methods::from_cli_for_enum::from_cli_for_enum(ast, variants);
self::common_methods::from_cli_for_enum::from_cli_for_enum(ast, variants);
quote! {
#[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)]
@@ -373,35 +160,56 @@ pub fn impl_interactive_clap(ast: &syn::DeriveInput) -> TokenStream {
}
}
fn context_scope_for_struct(
name: &syn::Ident,
context_scope_fields: Vec<proc_macro2::TokenStream>,
) -> proc_macro2::TokenStream {
let interactive_clap_context_scope_for_struct = syn::Ident::new(
&format!("InteractiveClapContextScopeFor{}", &name),
Span::call_site(),
);
quote! {
pub struct #interactive_clap_context_scope_for_struct {
#(#context_scope_fields,)*
}
impl interactive_clap::ToInteractiveClapContextScope for #name {
type InteractiveClapContextScope = #interactive_clap_context_scope_for_struct;
}
}
}
/** This module describes [`crate::InteractiveClap`] derive logic in case when [`syn::DeriveInput`]
is a 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_with_subcommand::is_field_with_subcommand(field)
&& !self::methods::fields_with_subargs::is_field_with_subargs(field)
{
quote! {
pub #ident_field: #ty
The structure of produced derive output is as follows, where code blocks are generated by
submodules with corresponding names:
```rust,ignore
quote::quote! {
#to_cli_trait_block
#input_args_impl_block
#to_interactive_clap_context_scope_trait_block
#from_cli_trait_block
#clap_for_named_arg_enum_block
}
```
*/
pub(crate) mod structs {
pub(crate) mod to_cli_trait;
mod input_args_impl;
mod to_interactive_clap_context_scope_trait;
mod from_cli_trait;
mod clap_for_named_arg_enum;
/// these are common field methods, reused by other [structs](super::structs) submodules
pub(super) mod common_field_methods;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(
name: &syn::Ident,
cli_name: &syn::Ident,
ast: &syn::DeriveInput,
fields: &syn::Fields,
) -> proc_macro2::TokenStream {
let b1 = to_cli_trait::token_stream(name, cli_name, fields);
let b2 = input_args_impl::token_stream(ast, fields);
let b3 = to_interactive_clap_context_scope_trait::token_stream(ast, fields);
let b4 = from_cli_trait::token_stream(ast, fields);
let b5 = clap_for_named_arg_enum::token_stream(ast, fields);
quote::quote! {
#b1
#b2
#b3
#b4
#b5
}
} else {
quote!()
}
}
@@ -419,46 +227,37 @@ fn context_scope_for_enum(name: &syn::Ident) -> proc_macro2::TokenStream {
}
}
fn for_cli_field(
field: &syn::Field,
ident_skip_field_vec: &[syn::Ident],
) -> proc_macro2::TokenStream {
let ident_field = &field.clone().ident.expect("this field does not exist");
if ident_skip_field_vec.contains(ident_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) => {
if path_segment.ident == "Option" || path_segment.ident == "bool" {
quote! {
#ident_field: args.#ident_field.into()
}
} else {
quote! {
#ident_field: Some(args.#ident_field.into())
}
}
}
_ => abort_call_site!("Only option `PathSegment` is needed"),
},
_ => abort_call_site!("Only option `Type::Path` is needed"),
#[cfg(test)]
pub(crate) mod to_cli_args_structs_test_bridge {
struct Opts {
name: syn::Ident,
cli_name: syn::Ident,
input_fields: syn::Fields,
}
fn prepare(ast: &syn::DeriveInput) -> Opts {
let (name, cli_name) = super::get_names(ast);
let input_fields = match &ast.data {
syn::Data::Struct(data_struct) => data_struct.fields.clone(),
syn::Data::Enum(..) | syn::Data::Union(..) => {
unreachable!("stuct DeriveInput expected");
}
};
Opts {
name: name.clone(),
cli_name,
input_fields,
}
}
pub fn partial_output(ast: &syn::DeriveInput) -> syn::Result<syn::DeriveInput> {
let opts = prepare(ast);
let (token_stream, _unused_byproduct) =
super::structs::to_cli_trait::cli_variant_struct::token_stream(
&opts.name,
&opts.cli_name,
&opts.input_fields,
);
syn::parse2(token_stream)
}
}

View File

@@ -0,0 +1,90 @@
/*!
derive of helper enum for structs with `#[interactive_clap(named_arg)]` on fields
```rust,ignore
struct #name {
#[interactive_clap(named_arg)]
///Specify a sender
field_name: Sender,
}
```
gets transformed
=>
```rust,ignore
#[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)]
pub enum ClapNamedArgSenderFor#name {
///Specify a sender
FieldName(<Sender as interactive_clap::ToCli>::CliVariant),
}
impl From<Sender> for ClapNamedArgSenderFor#name {
fn from(item: Sender) -> Self {
Self::FieldName(<Sender as interactive_clap::ToCli>::CliVariant::from(item))
}
}
```
*/
use proc_macro2::Span;
use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use syn;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream {
let name = &ast.ident;
fields
.iter()
.find_map(|field| field_transform(name, field))
.unwrap_or(quote!())
}
fn field_transform(name: &syn::Ident, field: &syn::Field) -> Option<proc_macro2::TokenStream> {
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() == *"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))
}
}
}
})
.next()
}

View File

@@ -0,0 +1,3 @@
pub mod with_skip_interactive_input;
pub mod with_subargs;
pub mod with_subcommand;

View File

@@ -4,7 +4,7 @@ use syn;
use crate::LONG_VEC_MUTLIPLE_OPT;
pub fn is_skip_interactive_input(field: &syn::Field) -> bool {
pub fn predicate(field: &syn::Field) -> bool {
field
.attrs
.iter()

View File

@@ -2,7 +2,7 @@ extern crate proc_macro;
use syn;
pub fn is_field_with_subargs(field: &syn::Field) -> bool {
pub fn predicate(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}

View File

@@ -3,7 +3,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 {
pub fn predicate(field: &syn::Field) -> bool {
if field.attrs.is_empty() {
return false;
}

View File

@@ -1,18 +1,89 @@
extern crate proc_macro;
/*!
`interactive_clap::FromCli` derive
This modules describes derive of `interactive_clap::FromCli` trait for `#name` struct,
which happens during derive of [`crate::InteractiveClap`] for `#name` struct:
The implementation combines usages of all of [super::to_cli_trait], [super::input_args_impl],
[super::to_interactive_clap_context_scope_trait]
derive input `#name`
```rust,ignore
struct #name {
age: u64,
first_name: String,
}
```
gets transformed
=>
```rust,ignore
impl interactive_clap::FromCli for #name {
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();
if clap_variant.age.is_none() {
clap_variant
.age = match Self::input_age(&context) {
Ok(Some(age)) => Some(age),
Ok(None) => {
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
Err(err) => {
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
}
let age = clap_variant.age.clone().expect("Unexpected error");
if clap_variant.first_name.is_none() {
clap_variant
.first_name = match Self::input_first_name(&context) {
Ok(Some(first_name)) => Some(first_name),
Ok(None) => {
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
Err(err) => {
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
}
let first_name = clap_variant.first_name.clone().expect("Unexpected error");
let new_context_scope = InteractiveClapContextScopeFor#name {
age: age.into(),
first_name: first_name.into(),
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
```
*/
use proc_macro2::Span;
use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use syn;
pub fn from_cli_for_struct(
ast: &syn::DeriveInput,
fields: &syn::Fields,
) -> proc_macro2::TokenStream {
use super::common_field_methods as field_methods;
use crate::derives::interactive_clap::common_methods;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream {
let name = &ast.ident;
let interactive_clap_attrs_context =
super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
common_methods::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
if interactive_clap_attrs_context.is_skip_default_from_cli {
return quote!();
};
@@ -20,8 +91,8 @@ pub fn from_cli_for_struct(
let fields_without_subcommand_and_subargs = fields
.iter()
.filter(|field| {
!super::fields_with_subcommand::is_field_with_subcommand(field)
&& !super::fields_with_subargs::is_field_with_subargs(field)
!field_methods::with_subcommand::predicate(field)
&& !field_methods::with_subargs::predicate(field)
})
.map(|field| {
let ident_field = &field.clone().ident.expect("this field does not exist");
@@ -102,7 +173,7 @@ fn fields_value(field: &syn::Field) -> proc_macro2::TokenStream {
let ident_field = &field.clone().ident.expect("this field does not exist");
let fn_input_arg = syn::Ident::new(&format!("input_{}", &ident_field), Span::call_site());
if field.ty.to_token_stream().to_string() == "bool"
|| super::skip_interactive_input::is_skip_interactive_input(field)
|| field_methods::with_skip_interactive_input::predicate(field)
{
quote! {
let #ident_field = clap_variant.#ident_field.clone();
@@ -123,8 +194,8 @@ 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)
&& !super::fields_with_subargs::is_field_with_subargs(field)
} else if !field_methods::with_subcommand::predicate(field)
&& !field_methods::with_subargs::predicate(field)
{
quote! {
if clap_variant.#ident_field.is_none() {

View File

@@ -1,20 +1,77 @@
/*!
per-field input with [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html) impl block
This modules describes derive of input args implementation block for `#name` struct,
which contains functions `input_#field_ident` per each field,
which prompt for value of each field via [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html)
, which happens during derive of [`crate::InteractiveClap`] for `#name` struct:
derive input `#name`
```rust,ignore
struct #name {
age: u64,
first_name: String,
}
```
gets transformed
=>
```rust,ignore
impl #name {
fn input_age(_context: &()) -> color_eyre::eyre::Result<Option<u64>> {
match inquire::CustomType::new("age").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn input_first_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
match inquire::CustomType::new("first_name").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
```
*/
extern crate proc_macro;
use proc_macro2::Span;
use quote::quote;
use syn;
pub fn vec_fn_input_arg(
ast: &syn::DeriveInput,
fields: &syn::Fields,
) -> Vec<proc_macro2::TokenStream> {
use super::common_field_methods as field_methods;
use crate::derives::interactive_clap::common_methods;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream {
let name = &ast.ident;
let vec_fn_input_arg = vec_fn_input_arg(ast, fields);
quote! {
impl #name {
#(#vec_fn_input_arg)*
}
}
}
fn vec_fn_input_arg(ast: &syn::DeriveInput, fields: &syn::Fields) -> Vec<proc_macro2::TokenStream> {
let interactive_clap_attrs_context =
super::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
common_methods::interactive_clap_attrs_context::InteractiveClapAttrsContext::new(ast);
let vec_fn_input_arg = fields
.iter()
.filter(|field| !super::fields_with_subcommand::is_field_with_subcommand(field))
.filter(|field| !field_methods::with_subcommand::predicate(field))
.filter(|field| {
!super::fields_with_skip_default_input_arg::is_field_with_skip_default_input_arg(
!common_methods::fields_with_skip_default_input_arg::is_field_with_skip_default_input_arg(
field,
)
})
@@ -44,7 +101,7 @@ pub fn vec_fn_input_arg(
};
}
if super::skip_interactive_input::is_skip_interactive_input(field) {
if field_methods::with_skip_interactive_input::predicate(field) {
return quote! {};
}

View File

@@ -0,0 +1,26 @@
use proc_macro2::TokenStream;
use quote::quote;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(name: &syn::Ident, cli_name: &syn::Ident) -> TokenStream {
quote! {
impl #name {
pub fn try_parse() -> Result<#cli_name, clap::Error> {
<#cli_name as clap::Parser>::try_parse()
}
pub fn parse() -> #cli_name {
<#cli_name as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<#cli_name, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<#cli_name as clap::Parser>::try_parse_from(itr)
}
}
}
}

View File

@@ -4,8 +4,8 @@ use proc_macro_error::abort_call_site;
use quote::quote;
use syn;
pub fn cli_field_type(ty: &syn::Type) -> proc_macro2::TokenStream {
match &ty {
pub fn field_type(ty: &syn::Type) -> syn::Type {
let token_stream = match &ty {
syn::Type::Path(type_path) => match type_path.path.segments.first() {
Some(path_segment) => {
if path_segment.ident == "Option" {
@@ -35,16 +35,6 @@ pub fn cli_field_type(ty: &syn::Type) -> proc_macro2::TokenStream {
_ => abort_call_site!("Only option `PathSegment` is needed"),
},
_ => 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
};
syn::parse2(token_stream).unwrap()
}

View File

@@ -0,0 +1,153 @@
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use crate::{LONG_VEC_MUTLIPLE_OPT, VERBATIM_DOC_COMMENT};
/// describes derive of individual field of `#cli_name` struct
/// based on transformation of input field from `#name` struct
mod field;
/// returns the whole result `TokenStream` of derive logic of containing module
/// and additional info as second returned tuple's element, needed for another derive
pub fn token_stream(
name: &syn::Ident,
cli_name: &syn::Ident,
input_fields: &syn::Fields,
) -> (TokenStream, Vec<syn::Ident>) {
let (cli_fields, ident_skip_field_vec) = fields(input_fields, name);
let token_stream = quote! {
#[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)]
#[clap(author, version, about, long_about = None)]
pub struct #cli_name {
#( #cli_fields, )*
}
};
(token_stream, ident_skip_field_vec)
}
/// describes derive of all fields of `#cli_name` struct
/// based on transformation of input fields from `#name` struct
fn fields(fields: &syn::Fields, name: &syn::Ident) -> (Vec<TokenStream>, Vec<syn::Ident>) {
let mut ident_skip_field_vec: Vec<syn::Ident> = Vec::new();
let fields = fields
.iter()
.map(|field| {
let ident_field = field.ident.clone().expect("this field does not exist");
let ty = &field.ty;
let cli_ty = self::field::field_type(ty);
let mut cli_field = quote! {
pub #ident_field: #cli_ty
};
if field.attrs.is_empty() {
return cli_field;
};
let mut clap_attr_vec: Vec<proc_macro2::TokenStream> = Vec::new();
let mut cfg_attr_vec: Vec<proc_macro2::TokenStream> = Vec::new();
let mut doc_attr_vec: Vec<proc_macro2::TokenStream> = Vec::new();
for attr in &field.attrs {
dbg_cond!(attr.path.to_token_stream().into_iter().collect::<Vec<_>>());
if attr.path.is_ident("interactive_clap") || attr.path.is_ident("cfg") {
for (_index, attr_token) in attr.tokens.clone().into_iter().enumerate() {
dbg_cond!((_index, &attr_token));
match attr_token {
proc_macro2::TokenTree::Group(group) => {
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")
|| (group_string == VERBATIM_DOC_COMMENT)
{
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());
clap_attr_vec.push(quote! {#ident_subcommand});
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(),
);
cli_field = quote! {
pub #ident_field: Option<#enum_for_clap_named_arg>
}
};
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!()
};
if group.stream().to_string() == LONG_VEC_MUTLIPLE_OPT {
if !crate::helpers::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")
}
}
}
}
if attr.path.is_ident("doc") {
doc_attr_vec.push(attr.into_token_stream())
}
}
if cli_field.is_empty() {
return cli_field;
};
let cfg_attrs = cfg_attr_vec.iter();
if !clap_attr_vec.is_empty() {
let clap_attrs = clap_attr_vec.iter();
quote! {
#(#cfg_attrs)*
#(#doc_attr_vec)*
#[clap(#(#clap_attrs, )*)]
#cli_field
}
} else {
quote! {
#(#cfg_attrs)*
#(#doc_attr_vec)*
#cli_field
}
}
})
.filter(|token_stream| !token_stream.is_empty())
.collect::<Vec<_>>();
(fields, ident_skip_field_vec)
}

View File

@@ -0,0 +1,69 @@
use crate::LONG_VEC_MUTLIPLE_OPT;
use proc_macro2::TokenStream;
use proc_macro_error::abort_call_site;
use quote::quote;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(
name: &syn::Ident,
cli_name: &syn::Ident,
input_fields: &syn::Fields,
ident_skip_field_vec: &[syn::Ident],
) -> TokenStream {
let fields_conversion = input_fields
.iter()
.map(|field| field_conversion(field, ident_skip_field_vec))
.filter(|token_stream| !token_stream.is_empty());
quote! {
impl From<#name> for #cli_name {
fn from(args: #name) -> Self {
Self {
#( #fields_conversion, )*
}
}
}
}
}
fn field_conversion(field: &syn::Field, ident_skip_field_vec: &[syn::Ident]) -> TokenStream {
let ident_field = &field.clone().ident.expect("this field does not exist");
if ident_skip_field_vec.contains(ident_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) => {
if path_segment.ident == "Option" || path_segment.ident == "bool" {
quote! {
#ident_field: args.#ident_field.into()
}
} else {
quote! {
#ident_field: Some(args.#ident_field.into())
}
}
}
_ => abort_call_site!("Only option `PathSegment` is needed"),
},
_ => abort_call_site!("Only option `Type::Path` is needed"),
}
}
}

View File

@@ -0,0 +1,93 @@
/*!
`interactive_clap::ToCli` derive
This module describes the derive logic of `#cli_name` struct used as `CliVariant` in
implementation of `interactive_clap::ToCli`, which happens during derive of [`crate::InteractiveClap`] for `#name` struct.
```rust,ignore
#[derive(Debug, Default, Clone, clap::Parser, interactive_clap::ToCliArgs)]
#[clap(author, version, about, long_about = None)]
pub struct #cli_name {
#( #cli_fields, )*
}
impl interactive_clap::ToCli for #name {
type CliVariant = #cli_name;
}
```
Where `interactive_clap::ToCli` is:
```rust,ignore
pub trait ToCli {
type CliVariant;
}
```
Additionally a [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html) adapter
for `#name` and `From<#name> for #cli_name` conversion are defined:
```rust,ignore
impl #name {
pub fn try_parse() -> Result<#cli_name, clap::Error> {
<#cli_name as clap::Parser>::try_parse()
}
pub fn parse() -> #cli_name {
<#cli_name as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<#cli_name, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<#cli_name as clap::Parser>::try_parse_from(itr)
}
}
impl From<#name> for #cli_name {
fn from(args: #name) -> Self {
Self {
#( #fields_conversion, )*
}
}
}
```
*/
use proc_macro2::TokenStream;
use quote::quote;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(
name: &syn::Ident,
cli_name: &syn::Ident,
input_fields: &syn::Fields,
) -> TokenStream {
let (cli_variant_struct, ident_skip_field_vec) =
cli_variant_struct::token_stream(name, cli_name, input_fields);
let clap_parser_adapter = clap_parser_trait_adapter::token_stream(name, cli_name);
let from_trait_impl =
from_trait::token_stream(name, cli_name, input_fields, &ident_skip_field_vec);
quote! {
#cli_variant_struct
impl interactive_clap::ToCli for #name {
type CliVariant = #cli_name;
}
#clap_parser_adapter
#from_trait_impl
}
}
/// describes derive of `#cli_name` struct based on input `#name` struct
pub(crate) mod cli_variant_struct;
/// describes logic of derive of [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html) adapter
/// for `#name` struct, which returns instances of `#cli_name` struct
mod clap_parser_trait_adapter;
/// describes the derive of `impl From<#name> for #cli_name`
mod from_trait;

View File

@@ -0,0 +1,68 @@
/*!
`interactive_clap::ToInteractiveClapContextScope` derive
This modules describes derive of `interactive_clap::ToInteractiveClapContextScope` trait for `#name` struct,
which happens during derive of [`crate::InteractiveClap`] for `#name` struct:
derive input `#name`
```rust,ignore
struct #name {
age: u64,
first_name: String,
}
```
gets transformed
=>
```rust,ignore
impl #name pub struct InteractiveClapContextScopeFor#name {
pub age: u64,
pub first_name: String,
}
impl interactive_clap::ToInteractiveClapContextScope for #name {
type InteractiveClapContextScope = InteractiveClapContextScopeFor#name;
}
```
*/
use proc_macro2::Span;
use quote::quote;
use super::common_field_methods as field_methods;
/// returns the whole result `TokenStream` of derive logic of containing module
pub fn token_stream(ast: &syn::DeriveInput, fields: &syn::Fields) -> proc_macro2::TokenStream {
let name = &ast.ident;
let context_scope_fields = fields
.iter()
.map(field_transform)
.filter(|token_stream| !token_stream.is_empty())
.collect::<Vec<_>>();
let interactive_clap_context_scope_for_struct = syn::Ident::new(
&format!("InteractiveClapContextScopeFor{}", &name),
Span::call_site(),
);
quote! {
pub struct #interactive_clap_context_scope_for_struct {
#(#context_scope_fields,)*
}
impl interactive_clap::ToInteractiveClapContextScope for #name {
type InteractiveClapContextScope = #interactive_clap_context_scope_for_struct;
}
}
}
fn field_transform(field: &syn::Field) -> proc_macro2::TokenStream {
let ident_field = &field.ident.clone().expect("this field does not exist");
let ty = &field.ty;
if !field_methods::with_subcommand::predicate(field)
&& !field_methods::with_subargs::predicate(field)
{
quote! {
pub #ident_field: #ty
}
} else {
quote!()
}
}

View File

@@ -1,2 +1,3 @@
/// This module describes [`crate::InteractiveClap`] derive logic
pub mod interactive_clap;
pub mod to_cli_args;

View File

@@ -5,8 +5,6 @@ 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),
@@ -20,7 +18,12 @@ impl InteractiveClapAttrsCliField {
let mut named_args = quote!();
let mut unnamed_args = quote!();
if field.attrs.is_empty() {
if !field.attrs.iter().any(|attr| attr.path.is_ident("clap")) {
// BUGFIX: changed when this branch is being taken
// from: field attributes are empty
// to: there're no field attributes with `clap` identificator
//
// in order to allow `doc` attributes
args_without_attrs = quote! {
if let Some(arg) = &self.#ident_field {
args.push_front(arg.to_string())
@@ -83,7 +86,9 @@ impl InteractiveClapAttrsCliField {
args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string());
}
};
if cli_field_type::starts_with_vec(&field.ty) {
if crate::helpers::type_starts_with_vec(
&field.ty,
) {
unnamed_args = quote! {
for arg in self.#ident_field.iter().rev() {
args.push_front(arg.to_string());

View File

@@ -1,2 +1,13 @@
pub mod snake_case_to_camel_case;
pub mod to_kebab_case;
pub fn type_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

@@ -3,6 +3,9 @@ extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
#[macro_use]
mod debug;
mod derives;
mod helpers;
#[cfg(test)]
@@ -16,6 +19,11 @@ mod tests;
/// 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";
/// `#[interactive_clap(...)]` attribute which translates 1-to-1 into
/// `#[clap(verbatim_doc_comment)]`
/// More info on <https://docs.rs/clap/4.5.23/clap/_derive/index.html#command-attributes>
pub(crate) const VERBATIM_DOC_COMMENT: &str = "verbatim_doc_comment";
#[proc_macro_derive(InteractiveClap, attributes(interactive_clap))]
#[proc_macro_error]
pub fn interactive_clap(input: TokenStream) -> TokenStream {

View File

@@ -1 +1,7 @@
mod test_simple_enum;
mod test_simple_struct;
fn pretty_codegen(ts: &proc_macro2::TokenStream) -> String {
let file = syn::parse_file(&ts.to_string()).unwrap();
prettyplease::unparse(&file)
}

View File

@@ -0,0 +1,20 @@
---
source: interactive-clap-derive/src/tests/test_simple_enum.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for CliMode {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
match self {
Self::Network => {
let mut args = std::collections::VecDeque::new();
args.push_front("network".to_owned());
args
}
Self::Offline => {
let mut args = std::collections::VecDeque::new();
args.push_front("offline".to_owned());
args
}
}
}
}

View File

@@ -0,0 +1,81 @@
---
source: interactive-clap-derive/src/tests/test_simple_enum.rs
expression: pretty_codegen(&interactive_clap_codegen)
---
#[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)]
pub enum CliMode {
/// Prepare and, optionally, submit a new transaction with online mode
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
Offline,
}
impl interactive_clap::ToCli for Mode {
type CliVariant = CliMode;
}
pub type InteractiveClapContextScopeForMode = ModeDiscriminants;
impl interactive_clap::ToInteractiveClapContextScope for Mode {
type InteractiveClapContextScope = InteractiveClapContextScopeForMode;
}
impl From<Mode> for CliMode {
fn from(command: Mode) -> Self {
match command {
Mode::Network => Self::Network,
Mode::Offline => Self::Offline,
}
}
}
impl interactive_clap::FromCli for Mode {
type FromCliContext = ();
type FromCliError = color_eyre::eyre::Error;
fn from_cli(
mut 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,
{
loop {
return match optional_clap_variant {
Some(CliMode::Network) => {
interactive_clap::ResultFromCli::Ok(CliMode::Network)
}
Some(CliMode::Offline) => {
interactive_clap::ResultFromCli::Ok(CliMode::Offline)
}
None => {
match Self::choose_variant(context.clone()) {
interactive_clap::ResultFromCli::Ok(cli_args) => {
optional_clap_variant = Some(cli_args);
continue;
}
result => return result,
}
}
};
}
}
}
impl Mode {
pub fn choose_variant(
context: (),
) -> interactive_clap::ResultFromCli<
<Self as interactive_clap::ToCli>::CliVariant,
<Self as interactive_clap::FromCli>::FromCliError,
> {}
pub fn try_parse() -> Result<CliMode, clap::Error> {
<CliMode as clap::Parser>::try_parse()
}
pub fn parse() -> CliMode {
<CliMode as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<CliMode, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<CliMode as clap::Parser>::try_parse_from(itr)
}
}

View File

@@ -0,0 +1,20 @@
---
source: interactive-clap-derive/src/tests/test_simple_enum.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for CliMode {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
match self {
Self::Network => {
let mut args = std::collections::VecDeque::new();
args.push_front("network".to_owned());
args
}
Self::Offline => {
let mut args = std::collections::VecDeque::new();
args.push_front("offline".to_owned());
args
}
}
}
}

View File

@@ -0,0 +1,108 @@
---
source: interactive-clap-derive/src/tests/test_simple_enum.rs
expression: pretty_codegen(&interactive_clap_codegen)
---
#[derive(Debug, Clone, clap::Parser, interactive_clap::ToCliArgs)]
pub enum CliMode {
/// Prepare and, optionally, submit a new transaction with online mode
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
Offline,
}
impl interactive_clap::ToCli for Mode {
type CliVariant = CliMode;
}
pub type InteractiveClapContextScopeForMode = ModeDiscriminants;
impl interactive_clap::ToInteractiveClapContextScope for Mode {
type InteractiveClapContextScope = InteractiveClapContextScopeForMode;
}
impl From<Mode> for CliMode {
fn from(command: Mode) -> Self {
match command {
Mode::Network => Self::Network,
Mode::Offline => Self::Offline,
}
}
}
impl interactive_clap::FromCli for Mode {
type FromCliContext = ();
type FromCliError = color_eyre::eyre::Error;
fn from_cli(
mut 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,
{
loop {
return match optional_clap_variant {
Some(CliMode::Network) => {
interactive_clap::ResultFromCli::Ok(CliMode::Network)
}
Some(CliMode::Offline) => {
interactive_clap::ResultFromCli::Ok(CliMode::Offline)
}
None => {
match Self::choose_variant(context.clone()) {
interactive_clap::ResultFromCli::Ok(cli_args) => {
optional_clap_variant = Some(cli_args);
continue;
}
result => return result,
}
}
};
}
}
}
impl Mode {
pub fn choose_variant(
context: (),
) -> interactive_clap::ResultFromCli<
<Self as interactive_clap::ToCli>::CliVariant,
<Self as interactive_clap::FromCli>::FromCliError,
> {
use interactive_clap::SelectVariantOrBack;
use inquire::Select;
use strum::{EnumMessage, IntoEnumIterator};
let selected_variant = Select::new(
concat!(r" A little beautiful comment about our choice",).trim(),
ModeDiscriminants::iter()
.map(SelectVariantOrBack::Variant)
.chain([SelectVariantOrBack::Back])
.collect(),
)
.prompt();
match selected_variant {
Ok(SelectVariantOrBack::Variant(variant)) => {
let cli_args = match variant {
ModeDiscriminants::Network => CliMode::Network,
ModeDiscriminants::Offline => CliMode::Offline,
};
return interactive_clap::ResultFromCli::Ok(cli_args);
}
Ok(SelectVariantOrBack::Back) => return interactive_clap::ResultFromCli::Back,
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => return interactive_clap::ResultFromCli::Cancel(None),
Err(err) => return interactive_clap::ResultFromCli::Err(None, err.into()),
}
}
pub fn try_parse() -> Result<CliMode, clap::Error> {
<CliMode as clap::Parser>::try_parse()
}
pub fn parse() -> CliMode {
<CliMode as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<CliMode, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<CliMode as clap::Parser>::try_parse_from(itr)
}
}

View File

@@ -0,0 +1,13 @@
---
source: interactive-clap-derive/src/tests/test_simple_struct.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for CliViewAccountSummary {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
let mut args = std::collections::VecDeque::new();
if let Some(arg) = &self.account_id {
args.push_front(arg.to_string())
}
args
}
}

View File

@@ -0,0 +1,94 @@
---
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 CliViewAccountSummary {
/// What Account ID do you need to view?
pub account_id: Option<
<crate::types::account_id::AccountId as interactive_clap::ToCli>::CliVariant,
>,
}
impl interactive_clap::ToCli for ViewAccountSummary {
type CliVariant = CliViewAccountSummary;
}
impl ViewAccountSummary {
pub fn try_parse() -> Result<CliViewAccountSummary, clap::Error> {
<CliViewAccountSummary as clap::Parser>::try_parse()
}
pub fn parse() -> CliViewAccountSummary {
<CliViewAccountSummary as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<CliViewAccountSummary, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<CliViewAccountSummary as clap::Parser>::try_parse_from(itr)
}
}
impl From<ViewAccountSummary> for CliViewAccountSummary {
fn from(args: ViewAccountSummary) -> Self {
Self {
account_id: Some(args.account_id.into()),
}
}
}
impl ViewAccountSummary {
fn input_account_id(
_context: &(),
) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
match inquire::CustomType::new(
concat!(r" What Account ID do you need to view?",).trim(),
)
.prompt()
{
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
pub struct InteractiveClapContextScopeForViewAccountSummary {
pub account_id: crate::types::account_id::AccountId,
}
impl interactive_clap::ToInteractiveClapContextScope for ViewAccountSummary {
type InteractiveClapContextScope = InteractiveClapContextScopeForViewAccountSummary;
}
impl interactive_clap::FromCli for ViewAccountSummary {
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();
if clap_variant.account_id.is_none() {
clap_variant
.account_id = match Self::input_account_id(&context) {
Ok(Some(account_id)) => Some(account_id),
Ok(None) => {
return interactive_clap::ResultFromCli::Cancel(Some(clap_variant));
}
Err(err) => {
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
}
let account_id = clap_variant.account_id.clone().expect("Unexpected error");
let new_context_scope = InteractiveClapContextScopeForViewAccountSummary {
account_id: account_id.into(),
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}

View File

@@ -0,0 +1,21 @@
---
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();
if self.third_field {
args.push_front(std::concat!("--", "third-field").to_string());
}
if let Some(arg) = &self.second_field {
args.push_front(arg.to_string());
args.push_front(std::concat!("--", "second-field").to_string());
}
if let Some(arg) = &self.first_field {
args.push_front(arg.to_string());
args.push_front(std::concat!("--", "first-field").to_string());
}
args
}
}

View File

@@ -0,0 +1,87 @@
---
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 {
/// short first field description
///
/// a longer paragraph, describing the usage and stuff with first field's
/// awarenes of its possible applications
#[clap(long)]
pub first_field: Option<<u64 as interactive_clap::ToCli>::CliVariant>,
/// short second field description
///
/// a longer paragraph, describing the usage and stuff with second field's
/// awareness of its possible applications
#[clap(long, verbatim_doc_comment)]
pub second_field: Option<<String as interactive_clap::ToCli>::CliVariant>,
/// short third field description
///
/// a longer paragraph, describing the usage and stuff with third field's
/// awareness of its possible applications
#[clap(long, verbatim_doc_comment)]
pub third_field: bool,
}
impl interactive_clap::ToCli for Args {
type CliVariant = CliArgs;
}
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 {
first_field: Some(args.first_field.into()),
second_field: Some(args.second_field.into()),
third_field: args.third_field.into(),
}
}
}
impl Args {}
pub struct InteractiveClapContextScopeForArgs {
pub first_field: u64,
pub second_field: String,
pub third_field: bool,
}
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 first_field = clap_variant.first_field.clone();
let second_field = clap_variant.second_field.clone();
let third_field = clap_variant.third_field.clone();
let new_context_scope = InteractiveClapContextScopeForArgs {
first_field: first_field.into(),
second_field: second_field.into(),
third_field: third_field.into(),
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}

View File

@@ -5,12 +5,47 @@ 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 {
/// Offline mode
#[clap(long)]
pub offline: bool,
}
impl interactive_clap::ToCli for Args {
type CliVariant = CliArgs;
}
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 {
offline: args.offline.into(),
}
}
}
impl Args {
fn input_offline(_context: &()) -> color_eyre::eyre::Result<Option<bool>> {
match inquire::CustomType::new(concat!(r" Offline mode",).trim()).prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
pub struct InteractiveClapContextScopeForArgs {
pub offline: bool,
}
@@ -38,35 +73,3 @@ impl interactive_clap::FromCli for Args {
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
impl Args {
fn input_offline(_context: &()) -> color_eyre::eyre::Result<Option<bool>> {
match inquire::CustomType::new(concat!(r" Offline mode",).trim()).prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
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 {
offline: args.offline.into(),
}
}
}

View File

@@ -2,7 +2,7 @@
source: interactive-clap-derive/src/tests/test_simple_struct.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for Args {
impl interactive_clap::ToCliArgs for CliArgs {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
let mut args = std::collections::VecDeque::new();
if let Some(arg) = &self.second_name {
@@ -17,4 +17,3 @@ impl interactive_clap::ToCliArgs for Args {
args
}
}

View File

@@ -12,6 +12,62 @@ pub struct CliArgs {
impl interactive_clap::ToCli for Args {
type CliVariant = CliArgs;
}
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 {
age: Some(args.age.into()),
first_name: Some(args.first_name.into()),
second_name: Some(args.second_name.into()),
}
}
}
impl Args {
fn input_age(_context: &()) -> color_eyre::eyre::Result<Option<u64>> {
match inquire::CustomType::new("age").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn input_first_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
match inquire::CustomType::new("first_name").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn input_second_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
match inquire::CustomType::new("second_name").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
pub struct InteractiveClapContextScopeForArgs {
pub age: u64,
pub first_name: String,
@@ -81,57 +137,3 @@ impl interactive_clap::FromCli for Args {
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
impl Args {
fn input_age(_context: &()) -> color_eyre::eyre::Result<Option<u64>> {
match inquire::CustomType::new("age").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn input_first_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
match inquire::CustomType::new("first_name").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
fn input_second_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
match inquire::CustomType::new("second_name").prompt() {
Ok(value) => Ok(Some(value)),
Err(
inquire::error::InquireError::OperationCanceled
| inquire::error::InquireError::OperationInterrupted,
) => Ok(None),
Err(err) => Err(err.into()),
}
}
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 {
age: Some(args.age.into()),
first_name: Some(args.first_name.into()),
second_name: Some(args.second_name.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 CliAccount {
fn to_cli_args(&self) -> std::collections::VecDeque<String> {
let mut args = self
.field_name
.as_ref()
.map(|subcommand| subcommand.to_cli_args())
.unwrap_or_default();
args
}
}

View File

@@ -0,0 +1,98 @@
---
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 CliAccount {
#[clap(subcommand)]
pub field_name: Option<ClapNamedArgSenderForAccount>,
}
impl interactive_clap::ToCli for Account {
type CliVariant = CliAccount;
}
impl Account {
pub fn try_parse() -> Result<CliAccount, clap::Error> {
<CliAccount as clap::Parser>::try_parse()
}
pub fn parse() -> CliAccount {
<CliAccount as clap::Parser>::parse()
}
pub fn try_parse_from<I, T>(itr: I) -> Result<CliAccount, clap::Error>
where
I: ::std::iter::IntoIterator<Item = T>,
T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone,
{
<CliAccount as clap::Parser>::try_parse_from(itr)
}
}
impl From<Account> for CliAccount {
fn from(args: Account) -> Self {
Self {
field_name: Some(args.field_name.into()),
}
}
}
impl Account {}
pub struct InteractiveClapContextScopeForAccount {}
impl interactive_clap::ToInteractiveClapContextScope for Account {
type InteractiveClapContextScope = InteractiveClapContextScopeForAccount;
}
impl interactive_clap::FromCli for Account {
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 new_context_scope = InteractiveClapContextScopeForAccount {
};
let optional_field = match clap_variant.field_name.take() {
Some(ClapNamedArgSenderForAccount::FieldName(cli_arg)) => Some(cli_arg),
None => None,
};
match <Sender as interactive_clap::FromCli>::from_cli(
optional_field,
context.into(),
) {
interactive_clap::ResultFromCli::Ok(cli_field) => {
clap_variant
.field_name = Some(
ClapNamedArgSenderForAccount::FieldName(cli_field),
);
}
interactive_clap::ResultFromCli::Cancel(optional_cli_field) => {
clap_variant
.field_name = optional_cli_field
.map(ClapNamedArgSenderForAccount::FieldName);
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
.field_name = optional_cli_field
.map(ClapNamedArgSenderForAccount::FieldName);
return interactive_clap::ResultFromCli::Err(Some(clap_variant), err);
}
};
interactive_clap::ResultFromCli::Ok(clap_variant)
}
}
#[derive(Debug, Clone, clap::Parser, interactive_clap_derive::ToCliArgs)]
pub enum ClapNamedArgSenderForAccount {
FieldName(<Sender as interactive_clap::ToCli>::CliVariant),
}
impl From<Sender> for ClapNamedArgSenderForAccount {
fn from(item: Sender) -> Self {
Self::FieldName(<Sender as interactive_clap::ToCli>::CliVariant::from(item))
}
}

View File

@@ -11,6 +11,27 @@ pub struct CliArgs {
impl interactive_clap::ToCli for Args {
type CliVariant = CliArgs;
}
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() }
}
}
impl Args {}
pub struct InteractiveClapContextScopeForArgs {
pub env: Vec<String>,
}
@@ -38,23 +59,3 @@ impl interactive_clap::FromCli for Args {
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,61 @@
use super::pretty_codegen;
#[test]
fn test_simple_enum() {
let input = syn::parse_quote! {
pub enum Mode {
/// Prepare and, optionally, submit a new transaction with online mode
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
Offline,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let step_one_output = syn::parse_quote! {
pub enum CliMode {
/// Prepare and, optionally, submit a new transaction with online mode
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
Offline,
}
};
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_one_output);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
#[test]
fn test_simple_enum_with_strum_discriminants() {
let input = syn::parse_quote! {
#[strum_discriminants(derive(EnumMessage, EnumIter))]
/// A little beautiful comment about our choice
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,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let step_one_output = syn::parse_quote! {
pub enum CliMode {
/// Prepare and, optionally, submit a new transaction with online mode
Network,
/// Prepare and, optionally, submit a new transaction with offline mode
Offline,
}
};
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_one_output);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}

View File

@@ -1,7 +1,5 @@
fn pretty_codegen(ts: &proc_macro2::TokenStream) -> String {
let file = syn::parse_file(&ts.to_string()).unwrap();
prettyplease::unparse(&file)
}
use super::pretty_codegen;
use crate::derives::interactive_clap::to_cli_args_structs_test_bridge;
#[test]
fn test_simple_struct() {
@@ -16,7 +14,49 @@ fn test_simple_struct() {
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input);
let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
#[test]
fn test_simple_struct_with_named_arg() {
let input = syn::parse_quote! {
struct Account {
#[interactive_clap(named_arg)]
field_name: Sender,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
/// this tested this problem https://github.com/near/near-cli-rs/pull/444#issuecomment-2631866217
#[test]
fn test_bug_fix_of_to_cli_args_derive() {
let input = syn::parse_quote! {
pub struct ViewAccountSummary {
/// What Account ID do you need to view?
account_id: crate::types::account_id::AccountId,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
@@ -33,15 +73,10 @@ fn test_flag() {
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let input = syn::parse_quote! {
struct CliArgs {
/// Offline mode
#[clap(long)]
offline: bool
}
};
let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input);
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
@@ -56,18 +91,11 @@ fn test_vec_multiple_opt() {
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 step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input);
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}
@@ -86,3 +114,49 @@ fn test_vec_multiple_opt_err() {
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
}
/// this test checks if doc comments are propagated up to `CliArgs` struct,
/// which has `clap::Parser` derive on it
///
/// also it checks that `#[interactive_clap(verbatim_doc_comment)]` attribute
/// gets transferred to `#[clap(verbatim_doc_comment)]` on `second_field` of
/// the same `CliArgs` struct
#[test]
fn test_doc_comments_propagate() {
let input = syn::parse_quote! {
struct Args {
/// short first field description
///
/// a longer paragraph, describing the usage and stuff with first field's
/// awarenes of its possible applications
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
first_field: u64,
/// short second field description
///
/// a longer paragraph, describing the usage and stuff with second field's
/// awareness of its possible applications
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
#[interactive_clap(verbatim_doc_comment)]
second_field: String,
/// short third field description
///
/// a longer paragraph, describing the usage and stuff with third field's
/// awareness of its possible applications
#[interactive_clap(long)]
#[interactive_clap(skip_interactive_input)]
#[interactive_clap(verbatim_doc_comment)]
third_field: bool,
}
};
let interactive_clap_codegen = crate::derives::interactive_clap::impl_interactive_clap(&input);
insta::assert_snapshot!(pretty_codegen(&interactive_clap_codegen));
let step_two_input = to_cli_args_structs_test_bridge::partial_output(&input)
.unwrap_or_else(|err| panic!("couldn't parse syn::DeriveInput: {:?}", err));
let to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&step_two_input);
insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen));
}

6
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,6 @@
[toolchain]
# This specifies the version of Rust we use to build.
# Individual crates in the workspace may support a lower version, as indicated by `rust-version` field in each crate's `Cargo.toml`.
# The version specified below, should be at least as high as the maximum `rust-version` within the workspace.
channel = "stable"
components = ["rustfmt", "clippy", "rust-analyzer"]

View File

@@ -1,11 +1,16 @@
//! The Interactive-clap library is an add-on for the Command Line Argument
//! Parser (https://crates.io/crates/clap). Interactive-clap allows you to parse
//! Parser ([`CLAP`](https://crates.io/crates/clap>)). Interactive-clap allows you to parse
//! command line options. The peculiarity of this macro is that in the absence
//! of command line parameters, the interactive mode of entering these data by
//! the user is activated.
pub use interactive_clap_derive::{InteractiveClap, ToCliArgs};
/// Associated type [`Self::CliVariant`] is defined during derive of
/// [`macro@crate::InteractiveClap`]
///
/// This type has derive of [`clap::Parser`](https://docs.rs/clap/4.5.24/clap/trait.Parser.html), which allows to parse
/// initial input on cli, which may be incomplete
pub trait ToCli {
type CliVariant;
}
@@ -26,6 +31,7 @@ impl ToCli for bool {
type CliVariant = bool;
}
// TODO: the trait can clearly be shortened/renamed to `ContextScope`
pub trait ToInteractiveClapContextScope {
type InteractiveClapContextScope;
}
@@ -41,6 +47,10 @@ pub enum ResultFromCli<T, E> {
Err(Option<T>, E),
}
/// This trait drives the state machine of `interactive_clap`
///
/// It selects next command variants with [inquire::Select](https://docs.rs/inquire/0.6.2/inquire/struct.Select.html)
/// and prompts for non-optional arguments with [inquire::CustomType](https://docs.rs/inquire/0.6.2/inquire/struct.CustomType.html)
pub trait FromCli {
type FromCliContext;
type FromCliError;