Skip to content

Commit 79d3022

Browse files
authored
Implement choice enums (#196)
* Add derive macro for FromArgValue for choice enums * Add basic documentation on choice enums * Fix and add choice name override tests * Update github workflow for new example
1 parent 6e70ecc commit 79d3022

File tree

6 files changed

+295
-7
lines changed

6 files changed

+295
-7
lines changed

.github/workflows/rust.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
run: cargo test --workspace --verbose
1616

1717
- name: Run simple_example
18-
run: cargo run --package argh --example simple_example two --fooey
18+
run: cargo run --package argh --example simple_example two --fooey --woot quiet
1919

2020
rustfmt:
2121
name: rustfmt

argh/examples/simple_example.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ struct SubCommandTwo {
3434
#[argh(switch)]
3535
/// whether to fooey
3636
fooey: bool,
37+
38+
#[argh(option)]
39+
/// how to woot
40+
woot: Woot,
41+
}
42+
43+
#[derive(argh::FromArgValue, PartialEq, Debug)]
44+
enum Woot {
45+
Quiet,
46+
Loud,
3747
}
3848

3949
fn main() {

argh/src/lib.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@
111111
//! }
112112
//! ```
113113
//!
114+
//! `FromArgValue` can be automatically derived for `enum`s, with automatic
115+
//! error messages:
116+
//!
117+
//! ```
118+
//! use argh::{FromArgs, FromArgValue};
119+
//!
120+
//! #[derive(FromArgValue)]
121+
//! enum Mode {
122+
//! SoftCore,
123+
//! HardCore,
124+
//! }
125+
//!
126+
//! #[derive(FromArgs)]
127+
//! /// Do the thing.
128+
//! struct DoIt {
129+
//! #[argh(option)]
130+
//! /// how to do it
131+
//! how: Mode,
132+
//! }
133+
//!
134+
//! // ./some_bin --how whatever
135+
//! // > Error parsing option '--how' with value 'whatever': expected "soft_core" or "hard_core"
136+
//! ```
137+
//!
114138
//! Positional arguments can be declared using `#[argh(positional)]`.
115139
//! These arguments will be parsed in order of their declaration in
116140
//! the structure:
@@ -319,7 +343,7 @@
319343

320344
use std::str::FromStr;
321345

322-
pub use argh_derive::{ArgsInfo, FromArgs};
346+
pub use argh_derive::{ArgsInfo, FromArgValue, FromArgs};
323347

324348
/// Information about a particular command used for output.
325349
pub type CommandInfo = argh_shared::CommandInfo<'static>;
@@ -751,7 +775,7 @@ pub fn cargo_from_env<T: TopLevelCommand>() -> T {
751775
/// Any field type declared in a struct that derives `FromArgs` must implement
752776
/// this trait. A blanket implementation exists for types implementing
753777
/// `FromStr<Error: Display>`. Custom types can implement this trait
754-
/// directly.
778+
/// directly. It can also be derived on plain `enum`s without associated data.
755779
pub trait FromArgValue: Sized {
756780
/// Construct the type from a commandline value, returning an error string
757781
/// on failure.

argh/tests/lib.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
clippy::unwrap_in_result
1212
)]
1313

14-
use {argh::FromArgs, std::fmt::Debug};
14+
use {
15+
argh::{FromArgValue, FromArgs},
16+
std::fmt::Debug,
17+
};
1518

1619
#[test]
1720
fn basic_example() {
@@ -519,6 +522,87 @@ Woot
519522
Options:
520523
--option-name fooey
521524
--help, help display usage information
525+
"###,
526+
);
527+
}
528+
529+
/// Test choices
530+
#[derive(FromArgs, PartialEq, Debug)]
531+
struct WithChoices {
532+
/// first choice with a default
533+
#[argh(option, default = "TwoChoices::Chao")]
534+
choice1: TwoChoices,
535+
/// second choice.
536+
#[argh(option)]
537+
choice2: ThreeChoices,
538+
}
539+
540+
#[derive(FromArgValue, PartialEq, Debug)]
541+
enum TwoChoices {
542+
Hola,
543+
Chao,
544+
}
545+
546+
#[derive(FromArgValue, PartialEq, Debug)]
547+
enum ThreeChoices {
548+
FirstChoice,
549+
#[argh(name = "に")]
550+
Two,
551+
Three,
552+
}
553+
554+
#[test]
555+
fn with_choices() {
556+
assert_output(
557+
&["--choice2", "three"],
558+
WithChoices { choice1: TwoChoices::Chao, choice2: ThreeChoices::Three },
559+
);
560+
}
561+
562+
#[test]
563+
fn with_choices_snake_case() {
564+
assert_output(
565+
&["--choice2", "first_choice"],
566+
WithChoices { choice1: TwoChoices::Chao, choice2: ThreeChoices::FirstChoice },
567+
)
568+
}
569+
570+
#[test]
571+
fn override_default() {
572+
assert_output(
573+
&["--choice2", "first_choice", "--choice1", "hola"],
574+
WithChoices { choice1: TwoChoices::Hola, choice2: ThreeChoices::FirstChoice },
575+
)
576+
}
577+
578+
#[test]
579+
fn with_name_override() {
580+
assert_output(
581+
&["--choice2", "に", "--choice1", "hola"],
582+
WithChoices { choice1: TwoChoices::Hola, choice2: ThreeChoices::Two },
583+
)
584+
}
585+
586+
#[test]
587+
fn invalid_choice() {
588+
assert_error::<WithChoices>(
589+
&["--choice2", "something"],
590+
r###"Error parsing option '--choice2' with value 'something': expected "first_choice", "に" or "three"
591+
"###,
592+
)
593+
}
594+
595+
#[test]
596+
fn choice_help() {
597+
assert_help_string::<WithChoices>(
598+
r###"Usage: test_arg_0 [--choice1 <choice1>] --choice2 <choice2>
599+
600+
Test choices
601+
602+
Options:
603+
--choice1 first choice with a default
604+
--choice2 second choice.
605+
--help, help display usage information
522606
"###,
523607
);
524608
}

argh_derive/src/lib.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Use of this source code is governed by a BSD-style
44
// license that can be found in the LICENSE file.
55

6+
use parse_attrs::has_argh_attrs;
67
use syn::ext::IdentExt as _;
78

89
/// Implementation of the `FromArgs` and `argh(...)` derive attributes.
@@ -34,6 +35,14 @@ pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
3435
gen.into()
3536
}
3637

38+
/// Entrypoint for `#[derive(FromArgValue)]`.
39+
#[proc_macro_derive(FromArgValue, attributes(argh))]
40+
pub fn argh_value_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
41+
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
42+
let gen = impl_from_arg_value(&ast);
43+
gen.into()
44+
}
45+
3746
/// Entrypoint for `#[derive(ArgsInfo)]`.
3847
#[proc_macro_derive(ArgsInfo, attributes(argh))]
3948
pub fn args_info_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@@ -63,6 +72,25 @@ fn impl_from_args(input: &syn::DeriveInput) -> TokenStream {
6372
output_tokens
6473
}
6574

75+
fn impl_from_arg_value(input: &syn::DeriveInput) -> TokenStream {
76+
let errors = &Errors::default();
77+
let mut output_tokens = match &input.data {
78+
syn::Data::Enum(de) => impl_from_arg_value_enum(errors, &input.ident, &input.generics, de),
79+
_ => {
80+
errors.err(input, "`#[derive(FromArgValue)]` can only be applied to `enum`s");
81+
TokenStream::new()
82+
}
83+
};
84+
if has_argh_attrs(&input.attrs) {
85+
errors.err(
86+
&input.ident,
87+
"`#[derive(FromArgValue)]` `enum`s do not support `#[argh(...)]` attributes",
88+
);
89+
}
90+
errors.to_tokens(&mut output_tokens);
91+
output_tokens
92+
}
93+
6694
/// The kind of optionality a parameter has.
6795
enum Optionality {
6896
None,
@@ -1178,3 +1206,101 @@ fn enum_only_single_field_unnamed_variants<'a>(
11781206
}
11791207
}
11801208
}
1209+
1210+
/// Implements `FromArgValue` for a `#![derive(FromArgValue)]` enum (a choice enum).
1211+
fn impl_from_arg_value_enum(
1212+
errors: &Errors,
1213+
name: &syn::Ident,
1214+
generic_args: &syn::Generics,
1215+
de: &syn::DataEnum,
1216+
) -> TokenStream {
1217+
// An enum variant like `<name>`
1218+
struct ChoiceVariant<'a> {
1219+
ident: &'a syn::Ident,
1220+
name: syn::LitStr,
1221+
}
1222+
1223+
let variants: Vec<ChoiceVariant<'_>> = de
1224+
.variants
1225+
.iter()
1226+
.map(|variant| {
1227+
let ident = &variant.ident;
1228+
choice_enum_only_fieldless_variant(errors, &variant.fields);
1229+
let attrs = parse_attrs::ChoiceVariantAttrs::parse(errors, variant);
1230+
let name = match attrs.name_override {
1231+
Some(lit) => lit,
1232+
None => {
1233+
let name_str = pascal_to_snake_case(&format!("{}", ident));
1234+
syn::LitStr::new(&name_str, ident.span())
1235+
}
1236+
};
1237+
ChoiceVariant { ident, name }
1238+
})
1239+
.collect();
1240+
1241+
if variants.is_empty() {
1242+
errors.err(&de.variants, "Choice enums must have at least one variant");
1243+
}
1244+
1245+
let name_repeating = std::iter::repeat(name.clone());
1246+
let variant_idents = variants.iter().map(|x| x.ident);
1247+
let variant_names = variants.iter().map(|x| &x.name).collect::<Vec<_>>();
1248+
let err_literal = {
1249+
let mut err = "expected ".to_string();
1250+
for (i, name) in variant_names.iter().enumerate() {
1251+
if i == 0 {
1252+
} else if i == variant_names.len() - 1 {
1253+
err.push_str(" or ");
1254+
} else {
1255+
err.push_str(", ");
1256+
}
1257+
err.push_str(&format!("{:?}", name.value()));
1258+
}
1259+
LitStr::new(&err, name.span())
1260+
};
1261+
let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
1262+
quote! {
1263+
impl #impl_generics argh::FromArgValue for #name #ty_generics #where_clause {
1264+
fn from_arg_value(value: &str)
1265+
-> std::result::Result<Self, String>
1266+
{
1267+
Ok(match value {
1268+
#(
1269+
#variant_names => #name_repeating::#variant_idents,
1270+
)*
1271+
_ => {
1272+
return Err(#err_literal.to_owned())
1273+
}
1274+
})
1275+
}
1276+
}
1277+
}
1278+
}
1279+
1280+
/// Generates an error if the variant is not a field-less variant like `Foo`.
1281+
fn choice_enum_only_fieldless_variant(errors: &Errors, variant_fields: &syn::Fields) {
1282+
match variant_fields {
1283+
syn::Fields::Unit => {}
1284+
_ => {
1285+
errors.err(
1286+
variant_fields,
1287+
"Choice `enum`s tagged with `#![derive(FromArgValue)]` do not support variants with associated data.",
1288+
);
1289+
}
1290+
}
1291+
}
1292+
1293+
fn pascal_to_snake_case(camel: &str) -> String {
1294+
let mut out = String::with_capacity(camel.len() + 8);
1295+
for (i, c) in camel.chars().enumerate() {
1296+
if c.is_uppercase() {
1297+
if i != 0 {
1298+
out.push('_');
1299+
}
1300+
out.extend(c.to_lowercase());
1301+
} else {
1302+
out.push(c);
1303+
}
1304+
}
1305+
out
1306+
}

0 commit comments

Comments
 (0)