diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f60c048..e6b25f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/examples/struct_with_subargs.rs b/examples/struct_with_subargs.rs index f91f35a..3f33740 100644 --- a/examples/struct_with_subargs.rs +++ b/examples/struct_with_subargs.rs @@ -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, + /// Sender account #[interactive_clap(subargs)] account: Sender, } diff --git a/interactive-clap-derive/Cargo.toml b/interactive-clap-derive/Cargo.toml index 9ceb11d..4e66d81 100644 --- a/interactive-clap-derive/Cargo.toml +++ b/interactive-clap-derive/Cargo.toml @@ -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 = [] diff --git a/interactive-clap-derive/src/debug.rs b/interactive-clap-derive/src/debug.rs new file mode 100644 index 0000000..2d1777d --- /dev/null +++ b/interactive-clap-derive/src/debug.rs @@ -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 + }; +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/choose_variant.rs b/interactive-clap-derive/src/derives/interactive_clap/common_methods/choose_variant.rs similarity index 87% rename from interactive-clap-derive/src/derives/interactive_clap/methods/choose_variant.rs rename to interactive-clap-derive/src/derives/interactive_clap/common_methods/choose_variant.rs index 4c9635f..d508ed3 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/choose_variant.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/common_methods/choose_variant.rs @@ -9,6 +9,7 @@ pub fn fn_choose_variant( ast: &syn::DeriveInput, variants: &syn::punctuated::Punctuated, ) -> 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::>() + .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 diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_skip_default_input_arg.rs b/interactive-clap-derive/src/derives/interactive_clap/common_methods/fields_with_skip_default_input_arg.rs similarity index 100% rename from interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_skip_default_input_arg.rs rename to interactive-clap-derive/src/derives/interactive_clap/common_methods/fields_with_skip_default_input_arg.rs diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_enum.rs b/interactive-clap-derive/src/derives/interactive_clap/common_methods/from_cli_for_enum.rs similarity index 100% rename from interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_enum.rs rename to interactive-clap-derive/src/derives/interactive_clap/common_methods/from_cli_for_enum.rs diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/interactive_clap_attrs_context.rs b/interactive-clap-derive/src/derives/interactive_clap/common_methods/interactive_clap_attrs_context.rs similarity index 100% rename from interactive-clap-derive/src/derives/interactive_clap/methods/interactive_clap_attrs_context.rs rename to interactive-clap-derive/src/derives/interactive_clap/common_methods/interactive_clap_attrs_context.rs diff --git a/interactive-clap-derive/src/derives/interactive_clap/common_methods/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/common_methods/mod.rs new file mode 100644 index 0000000..f25832e --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/common_methods/mod.rs @@ -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; diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/methods/mod.rs deleted file mode 100644 index a1d5463..0000000 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/mod.rs +++ /dev/null @@ -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; diff --git a/interactive-clap-derive/src/derives/interactive_clap/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/mod.rs index c95c70e..95d2b4a 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/mod.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/mod.rs @@ -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 = 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 = Vec::new(); - let mut cfg_attr_vec: Vec = 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::>(); - - 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::>(); - 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(itr: I) -> Result<#cli_name, clap::Error> - where - I: ::std::iter::IntoIterator, - 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 { - 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 { + 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) + } } diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/clap_for_named_arg_enum.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/clap_for_named_arg_enum.rs new file mode 100644 index 0000000..7dfc68d --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/clap_for_named_arg_enum.rs @@ -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(::CliVariant), +} +impl From for ClapNamedArgSenderFor#name { + fn from(item: Sender) -> Self { + Self::FieldName(::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 { + 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() +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/mod.rs new file mode 100644 index 0000000..7fad14e --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/mod.rs @@ -0,0 +1,3 @@ +pub mod with_skip_interactive_input; +pub mod with_subargs; +pub mod with_subcommand; diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_skip_interactive_input.rs similarity index 89% rename from interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_skip_interactive_input.rs index dc91e49..e68c5ae 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/skip_interactive_input.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_skip_interactive_input.rs @@ -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() diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subargs.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subargs.rs similarity index 81% rename from interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subargs.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subargs.rs index 4e52f60..21eed65 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subargs.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subargs.rs @@ -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; } diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subcommand.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subcommand.rs similarity index 86% rename from interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subcommand.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subcommand.rs index b1acab7..6b44b71 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/fields_with_subcommand.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/common_field_methods/with_subcommand.rs @@ -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; } diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/from_cli_trait.rs similarity index 79% rename from interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/from_cli_trait.rs index 706be3c..17d1c35 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/from_cli_trait.rs @@ -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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); + 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() { diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/input_arg.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/input_args_impl.rs similarity index 53% rename from interactive-clap-derive/src/derives/interactive_clap/methods/input_arg.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/input_args_impl.rs index f887304..b1bc233 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/input_arg.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/input_args_impl.rs @@ -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> { + 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> { + 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 { +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 { 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! {}; } diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/clap_parser_trait_adapter.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/clap_parser_trait_adapter.rs new file mode 100644 index 0000000..53a6b46 --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/clap_parser_trait_adapter.rs @@ -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(itr: I) -> Result<#cli_name, clap::Error> + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + <#cli_name as clap::Parser>::try_parse_from(itr) + } + } + } +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/field.rs similarity index 78% rename from interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs rename to interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/field.rs index debe791..27cf470 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/field.rs @@ -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() } diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/mod.rs new file mode 100644 index 0000000..db12858 --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/cli_variant_struct/mod.rs @@ -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) { + 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, Vec) { + let mut ident_skip_field_vec: Vec = 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 = Vec::new(); + let mut cfg_attr_vec: Vec = Vec::new(); + let mut doc_attr_vec: Vec = Vec::new(); + for attr in &field.attrs { + dbg_cond!(attr.path.to_token_stream().into_iter().collect::>()); + 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::>(); + (fields, ident_skip_field_vec) +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/from_trait.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/from_trait.rs new file mode 100644 index 0000000..18babcb --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/from_trait.rs @@ -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"), + } + } +} diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/mod.rs new file mode 100644 index 0000000..0731b8e --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_cli_trait/mod.rs @@ -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(itr: I) -> Result<#cli_name, clap::Error> + where + I: ::std::iter::IntoIterator, + 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; diff --git a/interactive-clap-derive/src/derives/interactive_clap/structs/to_interactive_clap_context_scope_trait.rs b/interactive-clap-derive/src/derives/interactive_clap/structs/to_interactive_clap_context_scope_trait.rs new file mode 100644 index 0000000..93f980a --- /dev/null +++ b/interactive-clap-derive/src/derives/interactive_clap/structs/to_interactive_clap_context_scope_trait.rs @@ -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::>(); + 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!() + } +} diff --git a/interactive-clap-derive/src/derives/mod.rs b/interactive-clap-derive/src/derives/mod.rs index ad346a0..f427573 100644 --- a/interactive-clap-derive/src/derives/mod.rs +++ b/interactive-clap-derive/src/derives/mod.rs @@ -1,2 +1,3 @@ +/// This module describes [`crate::InteractiveClap`] derive logic pub mod interactive_clap; pub mod to_cli_args; diff --git a/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs b/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs index 4a00cbb..053ae77 100755 --- a/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs +++ b/interactive-clap-derive/src/derives/to_cli_args/methods/interactive_clap_attrs_cli_field.rs @@ -5,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()); diff --git a/interactive-clap-derive/src/helpers/mod.rs b/interactive-clap-derive/src/helpers/mod.rs index 970a12f..201cdcb 100644 --- a/interactive-clap-derive/src/helpers/mod.rs +++ b/interactive-clap-derive/src/helpers/mod.rs @@ -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 +} diff --git a/interactive-clap-derive/src/lib.rs b/interactive-clap-derive/src/lib.rs index daa4fcb..80936d1 100644 --- a/interactive-clap-derive/src/lib.rs +++ b/interactive-clap-derive/src/lib.rs @@ -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 +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 { diff --git a/interactive-clap-derive/src/tests/mod.rs b/interactive-clap-derive/src/tests/mod.rs index d489ea3..6d3e6c2 100644 --- a/interactive-clap-derive/src/tests/mod.rs +++ b/interactive-clap-derive/src/tests/mod.rs @@ -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) +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum-2.snap new file mode 100644 index 0000000..eb9f69c --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum-2.snap @@ -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 { + 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 + } + } + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum.snap new file mode 100644 index 0000000..c95244a --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum.snap @@ -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 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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::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< + ::CliVariant, + ::FromCliError, + > {} + pub fn try_parse() -> Result { + ::try_parse() + } + pub fn parse() -> CliMode { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants-2.snap new file mode 100644 index 0000000..eb9f69c --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants-2.snap @@ -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 { + 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 + } + } + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants.snap new file mode 100644 index 0000000..330fca2 --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_enum__simple_enum_with_strum_discriminants.snap @@ -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 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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::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< + ::CliVariant, + ::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 { + ::try_parse() + } + pub fn parse() -> CliMode { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive-2.snap new file mode 100644 index 0000000..5bc3547 --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive-2.snap @@ -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 { + let mut args = std::collections::VecDeque::new(); + if let Some(arg) = &self.account_id { + args.push_front(arg.to_string()) + } + args + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive.snap new file mode 100644 index 0000000..fe5efde --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__bug_fix_of_to_cli_args_derive.snap @@ -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< + ::CliVariant, + >, +} +impl interactive_clap::ToCli for ViewAccountSummary { + type CliVariant = CliViewAccountSummary; +} +impl ViewAccountSummary { + pub fn try_parse() -> Result { + ::try_parse() + } + pub fn parse() -> CliViewAccountSummary { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for 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> { + 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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); + 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) + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate-2.snap new file mode 100644 index 0000000..d4dc0ba --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate-2.snap @@ -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 { + 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 + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate.snap new file mode 100644 index 0000000..2a6c7a6 --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__doc_comments_propagate.snap @@ -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<::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<::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 { + ::try_parse() + } + pub fn parse() -> CliArgs { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for CliArgs { + fn from(args: Args) -> Self { + Self { + 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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); + let 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) + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap index 5dcb81d..67cdbaa 100644 --- a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap @@ -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 { + ::try_parse() + } + pub fn parse() -> CliArgs { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for CliArgs { + fn from(args: Args) -> Self { + Self { + offline: args.offline.into(), + } + } +} +impl Args { + fn input_offline(_context: &()) -> color_eyre::eyre::Result> { + 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> { - 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 { - ::try_parse() - } - pub fn parse() -> CliArgs { - ::parse() - } - pub fn try_parse_from(itr: I) -> Result - where - I: ::std::iter::IntoIterator, - T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, - { - ::try_parse_from(itr) - } -} -impl From for CliArgs { - fn from(args: Args) -> Self { - Self { - offline: args.offline.into(), - } - } -} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap index 70c84fd..fbaae2f 100644 --- a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap @@ -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 { 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 } } - diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap index 8b00ce9..4d83e4e 100644 --- a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap @@ -12,6 +12,62 @@ pub struct CliArgs { impl interactive_clap::ToCli for Args { type CliVariant = CliArgs; } +impl Args { + pub fn try_parse() -> Result { + ::try_parse() + } + pub fn parse() -> CliArgs { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for CliArgs { + fn from(args: Args) -> Self { + Self { + 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> { + 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> { + 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> { + 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> { - 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> { - 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> { - 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 { - ::try_parse() - } - pub fn parse() -> CliArgs { - ::parse() - } - pub fn try_parse_from(itr: I) -> Result - where - I: ::std::iter::IntoIterator, - T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, - { - ::try_parse_from(itr) - } -} -impl From for CliArgs { - fn from(args: Args) -> Self { - Self { - age: Some(args.age.into()), - first_name: Some(args.first_name.into()), - second_name: Some(args.second_name.into()), - } - } -} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg-2.snap new file mode 100644 index 0000000..18034f4 --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg-2.snap @@ -0,0 +1,14 @@ +--- +source: interactive-clap-derive/src/tests/test_simple_struct.rs +expression: pretty_codegen(&to_cli_args_codegen) +--- +impl interactive_clap::ToCliArgs for CliAccount { + fn to_cli_args(&self) -> std::collections::VecDeque { + let mut args = self + .field_name + .as_ref() + .map(|subcommand| subcommand.to_cli_args()) + .unwrap_or_default(); + args + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg.snap new file mode 100644 index 0000000..247315c --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct_with_named_arg.snap @@ -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, +} +impl interactive_clap::ToCli for Account { + type CliVariant = CliAccount; +} +impl Account { + pub fn try_parse() -> Result { + ::try_parse() + } + pub fn parse() -> CliAccount { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for 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<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.clone().unwrap_or_default(); + let new_context_scope = InteractiveClapContextScopeForAccount { + }; + let optional_field = match clap_variant.field_name.take() { + Some(ClapNamedArgSenderForAccount::FieldName(cli_arg)) => Some(cli_arg), + None => None, + }; + match ::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(::CliVariant), +} +impl From for ClapNamedArgSenderForAccount { + fn from(item: Sender) -> Self { + Self::FieldName(::CliVariant::from(item)) + } +} diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt_to_cli_args.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt-2.snap similarity index 100% rename from interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt_to_cli_args.snap rename to interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt-2.snap diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap index 16a176f..fbbda1b 100644 --- a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__vec_multiple_opt.snap @@ -11,6 +11,27 @@ pub struct CliArgs { impl interactive_clap::ToCli for Args { type CliVariant = CliArgs; } +impl Args { + pub fn try_parse() -> Result { + ::try_parse() + } + pub fn parse() -> CliArgs { + ::parse() + } + pub fn try_parse_from(itr: I) -> Result + where + I: ::std::iter::IntoIterator, + T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, + { + ::try_parse_from(itr) + } +} +impl From for CliArgs { + fn from(args: Args) -> Self { + Self { env: args.env.into() } + } +} +impl Args {} pub struct InteractiveClapContextScopeForArgs { pub env: Vec, } @@ -38,23 +59,3 @@ impl interactive_clap::FromCli for Args { interactive_clap::ResultFromCli::Ok(clap_variant) } } -impl Args { - pub fn try_parse() -> Result { - ::try_parse() - } - pub fn parse() -> CliArgs { - ::parse() - } - pub fn try_parse_from(itr: I) -> Result - where - I: ::std::iter::IntoIterator, - T: ::std::convert::Into<::std::ffi::OsString> + ::std::clone::Clone, - { - ::try_parse_from(itr) - } -} -impl From for CliArgs { - fn from(args: Args) -> Self { - Self { env: args.env.into() } - } -} diff --git a/interactive-clap-derive/src/tests/test_simple_enum.rs b/interactive-clap-derive/src/tests/test_simple_enum.rs new file mode 100644 index 0000000..e9c5173 --- /dev/null +++ b/interactive-clap-derive/src/tests/test_simple_enum.rs @@ -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)); +} diff --git a/interactive-clap-derive/src/tests/test_simple_struct.rs b/interactive-clap-derive/src/tests/test_simple_struct.rs index 2d9a306..2c9fd86 100644 --- a/interactive-clap-derive/src/tests/test_simple_struct.rs +++ b/interactive-clap-derive/src/tests/test_simple_struct.rs @@ -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, - } - }; + 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)); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..e5c329d --- /dev/null +++ b/rust-toolchain.toml @@ -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"] diff --git a/src/lib.rs b/src/lib.rs index e1b1999..da88e2d 100644 --- a/src/lib.rs +++ b/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 { Err(Option, 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;