mirror of
https://github.com/Drop-OSS/interactive-clap.git
synced 2026-01-30 20:55:25 +01:00
feat: propagate doc comments on flags and arguments to --help/-h + structs derive refactor (#26)
the crux of the pr is0c3ce3af6fand912744d436the 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 commits382cc33d61,3db17e1e90which 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:
20
.github/workflows/tests.yml
vendored
20
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
24
interactive-clap-derive/src/debug.rs
Normal file
24
interactive-clap-derive/src/debug.rs
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod with_skip_interactive_input;
|
||||
pub mod with_subargs;
|
||||
pub mod with_subcommand;
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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! {};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
/// This module describes [`crate::InteractiveClap`] derive logic
|
||||
pub mod interactive_clap;
|
||||
pub mod to_cli_args;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
61
interactive-clap-derive/src/tests/test_simple_enum.rs
Normal file
61
interactive-clap-derive/src/tests/test_simple_enum.rs
Normal 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));
|
||||
}
|
||||
@@ -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
6
rust-toolchain.toml
Normal 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"]
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user