feat: Add support for boolean flags (e.g. --offline) (#6)

This commit is contained in:
Vlad Frolov
2023-06-02 19:49:58 +02:00
committed by GitHub
parent 879efbd0d1
commit dea28e482a
15 changed files with 326 additions and 25 deletions

View File

@@ -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:

View File

@@ -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"] }

View File

@@ -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>

View File

@@ -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")
}
})

View File

@@ -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()
}

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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()
}

View File

@@ -0,0 +1 @@
mod test_simple_struct;

View File

@@ -0,0 +1,14 @@
---
source: interactive-clap-derive/src/tests/test_simple_struct.rs
expression: pretty_codegen(&to_cli_args_codegen)
---
impl interactive_clap::ToCliArgs for 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
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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
}
}

View File

@@ -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()),
}
}
}

View 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));
}

View File

@@ -22,6 +22,10 @@ impl ToCli for u64 {
type CliVariant = u64;
}
impl ToCli for bool {
type CliVariant = bool;
}
pub trait ToInteractiveClapContextScope {
type InteractiveClapContextScope;
}