mirror of
https://github.com/Drop-OSS/interactive-clap.git
synced 2026-01-30 20:55:25 +01:00
feat: Add support for boolean flags (e.g. --offline) (#6)
This commit is contained in:
3
.github/workflows/release-plz.yml
vendored
3
.github/workflows/release-plz.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
1
interactive-clap-derive/src/tests/mod.rs
Normal file
1
interactive-clap-derive/src/tests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod test_simple_struct;
|
||||
@@ -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<String> {
|
||||
let mut args = std::collections::VecDeque::new();
|
||||
if self.offline {
|
||||
args.push_front(std::concat!("--", "offline").to_string());
|
||||
}
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<<Self as interactive_clap::ToCli>::CliVariant>,
|
||||
context: Self::FromCliContext,
|
||||
) -> interactive_clap::ResultFromCli<
|
||||
<Self as interactive_clap::ToCli>::CliVariant,
|
||||
Self::FromCliError,
|
||||
>
|
||||
where
|
||||
Self: Sized + interactive_clap::ToCli,
|
||||
{
|
||||
let mut clap_variant = optional_clap_variant.unwrap_or_default();
|
||||
let 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<Option<bool>> {
|
||||
match inquire::CustomType::new(concat!(r" Offline mode",).trim()).prompt() {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(
|
||||
inquire::error::InquireError::OperationCanceled
|
||||
| inquire::error::InquireError::OperationInterrupted,
|
||||
) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
fn try_parse() -> Result<CliArgs, clap::Error> {
|
||||
<CliArgs as clap::Parser>::try_parse()
|
||||
}
|
||||
fn parse() -> CliArgs {
|
||||
<CliArgs as clap::Parser>::parse()
|
||||
}
|
||||
}
|
||||
impl From<Args> for CliArgs {
|
||||
fn from(args: Args) -> Self {
|
||||
Self {
|
||||
offline: args.offline.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<<u64 as interactive_clap::ToCli>::CliVariant>,
|
||||
pub first_name: Option<<String as interactive_clap::ToCli>::CliVariant>,
|
||||
pub second_name: Option<<String as interactive_clap::ToCli>::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<<Self as interactive_clap::ToCli>::CliVariant>,
|
||||
context: Self::FromCliContext,
|
||||
) -> interactive_clap::ResultFromCli<
|
||||
<Self as interactive_clap::ToCli>::CliVariant,
|
||||
Self::FromCliError,
|
||||
>
|
||||
where
|
||||
Self: Sized + interactive_clap::ToCli,
|
||||
{
|
||||
let mut clap_variant = optional_clap_variant.unwrap_or_default();
|
||||
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<Option<u64>> {
|
||||
match inquire::CustomType::new("age").prompt() {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(
|
||||
inquire::error::InquireError::OperationCanceled
|
||||
| inquire::error::InquireError::OperationInterrupted,
|
||||
) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
fn input_first_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
|
||||
match inquire::CustomType::new("first_name").prompt() {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(
|
||||
inquire::error::InquireError::OperationCanceled
|
||||
| inquire::error::InquireError::OperationInterrupted,
|
||||
) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
fn input_second_name(_context: &()) -> color_eyre::eyre::Result<Option<String>> {
|
||||
match inquire::CustomType::new("second_name").prompt() {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(
|
||||
inquire::error::InquireError::OperationCanceled
|
||||
| inquire::error::InquireError::OperationInterrupted,
|
||||
) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
fn try_parse() -> Result<CliArgs, clap::Error> {
|
||||
<CliArgs as clap::Parser>::try_parse()
|
||||
}
|
||||
fn parse() -> CliArgs {
|
||||
<CliArgs as clap::Parser>::parse()
|
||||
}
|
||||
}
|
||||
impl From<Args> for CliArgs {
|
||||
fn from(args: Args) -> Self {
|
||||
Self {
|
||||
age: Some(args.age.into()),
|
||||
first_name: Some(args.first_name.into()),
|
||||
second_name: Some(args.second_name.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
interactive-clap-derive/src/tests/test_simple_struct.rs
Normal file
46
interactive-clap-derive/src/tests/test_simple_struct.rs
Normal file
@@ -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));
|
||||
}
|
||||
@@ -22,6 +22,10 @@ impl ToCli for u64 {
|
||||
type CliVariant = u64;
|
||||
}
|
||||
|
||||
impl ToCli for bool {
|
||||
type CliVariant = bool;
|
||||
}
|
||||
|
||||
pub trait ToInteractiveClapContextScope {
|
||||
type InteractiveClapContextScope;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user