Skip to content

Commit

Permalink
feat: ValueEnum fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
ModProg committed Jan 20, 2025
1 parent aa01c51 commit c567f0c
Show file tree
Hide file tree
Showing 12 changed files with 282 additions and 5 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ default = [
"usage",
"error-context",
"suggestions",
"unstable-derive-ui-tests"
]
debug = ["clap_builder/debug", "clap_derive?/debug"] # Enables debug messages
unstable-doc = ["clap_builder/unstable-doc", "derive"] # for docs.rs
Expand Down
2 changes: 1 addition & 1 deletion clap_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ proc-macro = true
bench = false

[dependencies]
syn = { version = "2.0.8", features = ["full"] }
syn = { version = "2.0.73", features = ["full"] }
quote = "1.0.9"
proc-macro2 = "1.0.69"
heck = "0.5.0"
Expand Down
2 changes: 2 additions & 0 deletions clap_derive/src/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ impl Parse for ClapAttr {
"long_help" => Some(MagicAttrName::LongHelp),
"author" => Some(MagicAttrName::Author),
"version" => Some(MagicAttrName::Version),
"fallback" => Some(MagicAttrName::Fallback),
_ => None,
};

Expand Down Expand Up @@ -168,6 +169,7 @@ pub(crate) enum MagicAttrName {
DefaultValuesOsT,
NextDisplayOrder,
NextHelpHeading,
Fallback,
}

#[derive(Clone)]
Expand Down
64 changes: 64 additions & 0 deletions clap_derive/src/derives/value_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub(crate) fn gen_for_enum(
let lits = lits(variants)?;
let value_variants = gen_value_variants(&lits);
let to_possible_value = gen_to_possible_value(item, &lits);
let from_str_for_fallback = gen_from_str_for_fallback(variants)?;

Ok(quote! {
#[allow(
Expand All @@ -75,6 +76,7 @@ pub(crate) fn gen_for_enum(
impl clap::ValueEnum for #item_name {
#value_variants
#to_possible_value
#from_str_for_fallback
}
})
}
Expand All @@ -85,6 +87,9 @@ fn lits(variants: &[(&Variant, Item)]) -> Result<Vec<(TokenStream, Ident)>, syn:
if let Kind::Skip(_, _) = &*item.kind() {
continue;
}
if item.is_fallback() {
continue;
}
if !matches!(variant.fields, Fields::Unit) {
abort!(variant.span(), "`#[derive(ValueEnum)]` only supports unit variants. Non-unit variants must be skipped");
}
Expand Down Expand Up @@ -128,3 +133,62 @@ fn gen_to_possible_value(item: &Item, lits: &[(TokenStream, Ident)]) -> TokenStr
}
}
}

fn gen_from_str_for_fallback(variants: &[(&Variant, Item)]) -> syn::Result<TokenStream> {
let fallbacks: Vec<_> = variants
.iter()
.filter(|(_, item)| item.is_fallback())
.collect();

match fallbacks.as_slice() {
[] => Ok(quote!()),
[(variant, _)] => {
let ident = &variant.ident;
let variant_initialization = match variant.fields.len() {
_ if matches!(variant.fields, Fields::Unit) => quote! {#ident},
0 => quote! {#ident{}},
1 => {
let member = variant
.fields
.members()
.next()
.expect("there should be exactly one field");
quote! {#ident{
#member: {
use std::convert::Into;
__input.into()
},
}}
}
_ => abort!(
variant,
"`fallback` only supports Unit variants, or variants with a single field"
),
};
Ok(quote! {
fn from_str(__input: &::std::primitive::str, __ignore_case: ::std::primitive::bool) -> ::std::result::Result<Self, ::std::string::String> {
Ok(Self::value_variants()
.iter()
.find(|v| {
v.to_possible_value()
.expect("ValueEnum::value_variants contains only values with a corresponding ValueEnum::to_possible_value")
.matches(__input, __ignore_case)
})
.cloned()
.unwrap_or_else(|| Self::#variant_initialization))
}
})
}
[first, second, ..] => {
let mut error = syn::Error::new_spanned(
first.0,
"`#[derive(ValueEnum)]` only supports one `fallback`.",
);
error.combine(syn::Error::new_spanned(
second.0,
"second fallback defined here",
));
Err(error)
}
}
}
13 changes: 13 additions & 0 deletions clap_derive/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub(crate) struct Item {
skip_group: bool,
group_id: Name,
group_methods: Vec<Method>,
/// Used as fallback value `ValueEnum`.
is_fallback: bool,
kind: Sp<Kind>,
}

Expand Down Expand Up @@ -279,6 +281,7 @@ impl Item {
group_id,
group_methods: vec![],
kind,
is_fallback: false,
}
}

Expand Down Expand Up @@ -835,6 +838,12 @@ impl Item {
self.skip_group = true;
}

Some(MagicAttrName::Fallback) => {
assert_attr_kind(attr, &[AttrKind::Value])?;

self.is_fallback = true;
}

None
// Magic only for the default, otherwise just forward to the builder
| Some(MagicAttrName::Short)
Expand Down Expand Up @@ -1077,6 +1086,10 @@ impl Item {
pub(crate) fn skip_group(&self) -> bool {
self.skip_group
}

pub(crate) fn is_fallback(&self) -> bool {
self.is_fallback
}
}

#[derive(Clone)]
Expand Down
149 changes: 149 additions & 0 deletions tests/derive/value_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,3 +626,152 @@ fn vec_type_default_value() {
Opt::try_parse_from(["", "-a", "foo,baz"]).unwrap()
);
}

#[test]
fn unit_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback,
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn empty_tuple_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback(),
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback()
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn empty_struct_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback {},
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback {}
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn non_empty_struct_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback {
value: String,
},
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback {
value: String::from("not-foo")
}
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}

#[test]
fn non_empty_tuple_fallback() {
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
enum ArgChoice {
Foo,
#[clap(fallback)]
Fallback(String),
}

#[derive(Parser, PartialEq, Debug)]
struct Opt {
#[arg(value_enum)]
arg: ArgChoice,
}

assert_eq!(
Opt {
arg: ArgChoice::Foo
},
Opt::try_parse_from(["", "foo"]).unwrap()
);
assert_eq!(
Opt {
arg: ArgChoice::Fallback(String::from("not-foo"))
},
Opt::try_parse_from(["", "not-foo"]).unwrap()
);
}
10 changes: 10 additions & 0 deletions tests/derive_ui/value_enum_fallback_two_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use clap::ValueEnum;

#[derive(ValueEnum, Clone, Debug)]
enum Opt {
#[clap(fallback)]
First(String, String),
}

fn main() {}

6 changes: 6 additions & 0 deletions tests/derive_ui/value_enum_fallback_two_fields.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: `fallback` only supports Unit variants, or variants with a single field
--> tests/derive_ui/value_enum_fallback_two_fields.rs:5:5
|
5 | / #[clap(fallback)]
6 | | First(String, String),
| |_________________________^
11 changes: 11 additions & 0 deletions tests/derive_ui/value_enum_two_fallback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use clap::ValueEnum;

#[derive(ValueEnum, Clone, Debug)]
enum Opt {
#[clap(fallback)]
First(String),
#[clap(fallback)]
Second(String),
}

fn main() {}
13 changes: 13 additions & 0 deletions tests/derive_ui/value_enum_two_fallback.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
error: `#[derive(ValueEnum)]` only supports one `fallback`.
--> tests/derive_ui/value_enum_two_fallback.rs:5:5
|
5 | / #[clap(fallback)]
6 | | First(String),
| |_________________^

error: second fallback defined here
--> tests/derive_ui/value_enum_two_fallback.rs:7:5
|
7 | / #[clap(fallback)]
8 | | Second(String),
| |__________________^
12 changes: 10 additions & 2 deletions tests/derive_ui/value_parser_unsupported.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ note: the traits `From`, `FromStr`, `ValueEnum`, and `ValueParserFactory` must
|
| pub trait ValueEnum: Sized + Clone {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--> $RUST/core/src/convert/mod.rs
--> $RUST/core/src/str/traits.rs
|
::: $RUST/core/src/convert/mod.rs
|
| pub trait From<T>: Sized {
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
::: $RUST/core/src/str/traits.rs
|
| pub trait FromStr: Sized {
| ^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the macro `clap::value_parser` (in Nightly builds, run with -Z macro-backtrace for more info)

0 comments on commit c567f0c

Please sign in to comment.