diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 766d8eb..0159803 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -21,9 +21,6 @@ jobs: token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Install packages (Linux) - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install --assume-yes libudev-dev - name: Run release-plz uses: MarcoIeni/release-plz-action@v0.5 env: diff --git a/interactive-clap-derive/Cargo.toml b/interactive-clap-derive/Cargo.toml index 878a992..0f18a0f 100644 --- a/interactive-clap-derive/Cargo.toml +++ b/interactive-clap-derive/Cargo.toml @@ -17,3 +17,8 @@ proc-macro2 = "1.0.24" proc-macro-error = "1" quote = "1.0" syn = "1" + +[dev-dependencies] +prettyplease = "0.1" +insta = "1" +syn = { version = "1", features = ["full"] } diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs b/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs index b0441e6..ee3b82b 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/methods/cli_field_type.rs @@ -8,7 +8,7 @@ pub fn cli_field_type(ty: &syn::Type) -> proc_macro2::TokenStream { match &ty { syn::Type::Path(type_path) => match type_path.path.segments.first() { Some(path_segment) => { - if path_segment.ident.eq("Option") { + if path_segment.ident == "Option" { match &path_segment.arguments { syn::PathArguments::AngleBracketed(gen_args) => { let ty_option = &gen_args.args; @@ -22,6 +22,10 @@ pub fn cli_field_type(ty: &syn::Type) -> proc_macro2::TokenStream { } } } + } else if path_segment.ident == "bool" { + quote! { + bool + } } else { quote! { Option<<#ty as interactive_clap::ToCli>::CliVariant> diff --git a/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs b/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs index 1d75873..c36104a 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/methods/from_cli_for_struct.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro2::Span; use proc_macro_error::abort_call_site; -use quote::quote; +use quote::{quote, ToTokens}; use syn; pub fn from_cli_for_struct( @@ -22,7 +22,7 @@ pub fn from_cli_for_struct( .filter(|field| super::fields_without_subcommand::is_field_without_subcommand(field)) .map(|field| { let ident_field = &field.clone().ident.expect("this field does not exist"); - quote! {#ident_field} + quote! {#ident_field: #ident_field.into()} }) .collect::>(); @@ -99,7 +99,11 @@ pub fn from_cli_for_struct( 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 super::fields_without_subcommand::is_field_without_subcommand(field) { + if field.ty.to_token_stream().to_string() == "bool" { + quote! { + let #ident_field = clap_variant.#ident_field.clone(); + } + } else if super::fields_without_subcommand::is_field_without_subcommand(field) { quote! { if clap_variant.#ident_field.is_none() { clap_variant @@ -127,7 +131,7 @@ fn field_value_named_arg(name: &syn::Ident, field: &syn::Field) -> proc_macro2:: .flat_map(|attr| attr.tokens.clone()) .filter(|attr_token| { match attr_token { - proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"), + proc_macro2::TokenTree::Group(group) => group.stream().to_string().contains("named_arg"), _ => abort_call_site!("Only option `TokenTree::Group` is needed") } }) diff --git a/interactive-clap-derive/src/derives/interactive_clap/mod.rs b/interactive-clap-derive/src/derives/interactive_clap/mod.rs index ce80728..ec053e6 100644 --- a/interactive-clap-derive/src/derives/interactive_clap/mod.rs +++ b/interactive-clap-derive/src/derives/interactive_clap/mod.rs @@ -1,7 +1,6 @@ extern crate proc_macro; -use proc_macro::TokenStream; -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort_call_site; use quote::{quote, ToTokens}; use syn; @@ -397,7 +396,7 @@ fn for_cli_field( match &ty { syn::Type::Path(type_path) => match type_path.path.segments.first() { Some(path_segment) => { - if path_segment.ident.eq("Option") { + if path_segment.ident == "Option" || path_segment.ident == "bool" { quote! { #ident_field: args.#ident_field.into() } 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 06569a2..7530fdd 100644 --- 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 @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro2::Span; use proc_macro_error::abort_call_site; -use quote::quote; +use quote::{quote, ToTokens}; use syn; #[derive(Debug, Clone)] @@ -39,7 +39,7 @@ impl InteractiveClapAttrsCliField { for item in group.stream() { match &item { proc_macro2::TokenTree::Ident(ident) => { - if *ident == *"subcommand" { + if ident == "subcommand" { subcommand_args = quote! { let mut args = self .#ident_field @@ -47,13 +47,13 @@ impl InteractiveClapAttrsCliField { .map(|subcommand| subcommand.to_cli_args()) .unwrap_or_default(); }; - } else if *ident == *"value_enum" { + } else if ident == "value_enum" { args_without_attrs = quote! { if let Some(arg) = &self.#ident_field { args.push_front(arg.to_string()) } }; - } else if *ident == *"long" { + } else if ident == "long" { let ident_field_to_kebab_case_string = crate::helpers::to_kebab_case::to_kebab_case( ident_field.to_string(), @@ -62,12 +62,21 @@ impl InteractiveClapAttrsCliField { &ident_field_to_kebab_case_string, Span::call_site(), ); - unnamed_args = quote! { - if let Some(arg) = &self.#ident_field { - args.push_front(arg.to_string()); - args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); - } - }; + if field.ty.to_token_stream().to_string() == "bool" + { + unnamed_args = quote! { + if self.#ident_field { + args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); + } + }; + } else { + unnamed_args = quote! { + if let Some(arg) = &self.#ident_field { + args.push_front(arg.to_string()); + args.push_front(std::concat!("--", #ident_field_to_kebab_case).to_string()); + } + }; + } } } proc_macro2::TokenTree::Literal(literal) => { diff --git a/interactive-clap-derive/src/derives/to_cli_args/mod.rs b/interactive-clap-derive/src/derives/to_cli_args/mod.rs index f7e388f..b960d7a 100644 --- a/interactive-clap-derive/src/derives/to_cli_args/mod.rs +++ b/interactive-clap-derive/src/derives/to_cli_args/mod.rs @@ -1,7 +1,6 @@ extern crate proc_macro; -use proc_macro::TokenStream; -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort_call_site; use quote::quote; use syn; diff --git a/interactive-clap-derive/src/lib.rs b/interactive-clap-derive/src/lib.rs index 961814e..2c170f7 100644 --- a/interactive-clap-derive/src/lib.rs +++ b/interactive-clap-derive/src/lib.rs @@ -5,17 +5,19 @@ use proc_macro_error::proc_macro_error; mod derives; mod helpers; +#[cfg(test)] +mod tests; #[proc_macro_derive(InteractiveClap, attributes(interactive_clap))] #[proc_macro_error] pub fn interactive_clap(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input); - derives::interactive_clap::impl_interactive_clap(&ast) + derives::interactive_clap::impl_interactive_clap(&ast).into() } #[proc_macro_derive(ToCliArgs, attributes(to_cli_args))] #[proc_macro_error] pub fn to_cli_args(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input); - derives::to_cli_args::impl_to_cli_args(&ast) + derives::to_cli_args::impl_to_cli_args(&ast).into() } diff --git a/interactive-clap-derive/src/tests/mod.rs b/interactive-clap-derive/src/tests/mod.rs new file mode 100644 index 0000000..d489ea3 --- /dev/null +++ b/interactive-clap-derive/src/tests/mod.rs @@ -0,0 +1 @@ +mod test_simple_struct; diff --git a/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag-2.snap b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag-2.snap new file mode 100644 index 0000000..fd25c4e --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag-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 CliArgs { + fn to_cli_args(&self) -> std::collections::VecDeque { + let mut args = std::collections::VecDeque::new(); + if self.offline { + args.push_front(std::concat!("--", "offline").to_string()); + } + args + } +} + 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 new file mode 100644 index 0000000..128cfcb --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__flag.snap @@ -0,0 +1,66 @@ +--- +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 { + #[clap(long)] + pub offline: bool, +} +impl interactive_clap::ToCli for Args { + type CliVariant = CliArgs; +} +pub struct InteractiveClapContextScopeForArgs { + pub offline: 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.unwrap_or_default(); + let offline = clap_variant.offline.clone(); + let new_context_scope = InteractiveClapContextScopeForArgs { + offline: offline.into(), + }; + 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()), + } + } + fn try_parse() -> Result { + ::try_parse() + } + fn parse() -> CliArgs { + ::parse() + } +} +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 new file mode 100644 index 0000000..70c84fd --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct-2.snap @@ -0,0 +1,20 @@ +--- +source: interactive-clap-derive/src/tests/test_simple_struct.rs +expression: pretty_codegen(&to_cli_args_codegen) +--- +impl interactive_clap::ToCliArgs for Args { + fn to_cli_args(&self) -> std::collections::VecDeque { + let mut args = std::collections::VecDeque::new(); + if let Some(arg) = &self.second_name { + args.push_front(arg.to_string()) + } + if let Some(arg) = &self.first_name { + args.push_front(arg.to_string()) + } + if let Some(arg) = &self.age { + args.push_front(arg.to_string()) + } + 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 new file mode 100644 index 0000000..f5a3daf --- /dev/null +++ b/interactive-clap-derive/src/tests/snapshots/interactive_clap_derive__tests__test_simple_struct__simple_struct.snap @@ -0,0 +1,131 @@ +--- +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 { + pub age: Option<::CliVariant>, + pub first_name: Option<::CliVariant>, + pub second_name: Option<::CliVariant>, +} +impl interactive_clap::ToCli for Args { + type CliVariant = CliArgs; +} +pub struct InteractiveClapContextScopeForArgs { + pub age: u64, + pub first_name: String, + pub second_name: String, +} +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.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"); + if clap_variant.second_name.is_none() { + clap_variant + .second_name = match Self::input_second_name(&context) { + Ok(Some(second_name)) => Some(second_name), + Ok(None) => { + return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)); + } + Err(err) => { + return interactive_clap::ResultFromCli::Err(Some(clap_variant), err); + } + }; + } + let second_name = clap_variant.second_name.clone().expect("Unexpected error"); + let new_context_scope = InteractiveClapContextScopeForArgs { + age: age.into(), + first_name: first_name.into(), + second_name: second_name.into(), + }; + 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()), + } + } + fn try_parse() -> Result { + ::try_parse() + } + fn parse() -> CliArgs { + ::parse() + } +} +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/test_simple_struct.rs b/interactive-clap-derive/src/tests/test_simple_struct.rs new file mode 100644 index 0000000..4b629fa --- /dev/null +++ b/interactive-clap-derive/src/tests/test_simple_struct.rs @@ -0,0 +1,46 @@ +fn pretty_codegen(ts: &proc_macro2::TokenStream) -> String { + let file = syn::parse_file(&ts.to_string()).unwrap(); + prettyplease::unparse(&file) +} + +#[test] +fn test_simple_struct() { + let input = syn::parse_quote! { + struct Args { + age: u64, + first_name: String, + second_name: String, + } + }; + + 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); + insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); +} + +#[test] +fn test_flag() { + let input = syn::parse_quote! { + struct Args { + /// Offline mode + #[interactive_clap(long)] + offline: bool + } + }; + + 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 to_cli_args_codegen = crate::derives::to_cli_args::impl_to_cli_args(&input); + insta::assert_snapshot!(pretty_codegen(&to_cli_args_codegen)); +} diff --git a/src/lib.rs b/src/lib.rs index 42b2d3a..e1b1999 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,10 @@ impl ToCli for u64 { type CliVariant = u64; } +impl ToCli for bool { + type CliVariant = bool; +} + pub trait ToInteractiveClapContextScope { type InteractiveClapContextScope; }