Skip to content

Commit 993bc8f

Browse files
committed
feat: ValueEnum fallback
1 parent aa01c51 commit 993bc8f

File tree

11 files changed

+272
-3
lines changed

11 files changed

+272
-3
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ default = [
144144
"usage",
145145
"error-context",
146146
"suggestions",
147+
"unstable-derive-ui-tests"
147148
]
148149
debug = ["clap_builder/debug", "clap_derive?/debug"] # Enables debug messages
149150
unstable-doc = ["clap_builder/unstable-doc", "derive"] # for docs.rs

clap_derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ proc-macro = true
2929
bench = false
3030

3131
[dependencies]
32-
syn = { version = "2.0.8", features = ["full"] }
32+
syn = { version = "2.0.73", features = ["full"] }
3333
quote = "1.0.9"
3434
proc-macro2 = "1.0.69"
3535
heck = "0.5.0"

clap_derive/src/attr.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ impl Parse for ClapAttr {
102102
"long_help" => Some(MagicAttrName::LongHelp),
103103
"author" => Some(MagicAttrName::Author),
104104
"version" => Some(MagicAttrName::Version),
105+
"fallback" => Some(MagicAttrName::Fallback),
105106
_ => None,
106107
};
107108

@@ -168,6 +169,7 @@ pub(crate) enum MagicAttrName {
168169
DefaultValuesOsT,
169170
NextDisplayOrder,
170171
NextHelpHeading,
172+
Fallback,
171173
}
172174

173175
#[derive(Clone)]

clap_derive/src/derives/value_enum.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) fn gen_for_enum(
4949
let lits = lits(variants)?;
5050
let value_variants = gen_value_variants(&lits);
5151
let to_possible_value = gen_to_possible_value(item, &lits);
52+
let from_str_for_fallback = gen_from_str_for_fallback(variants)?;
5253

5354
Ok(quote! {
5455
#[allow(
@@ -75,6 +76,7 @@ pub(crate) fn gen_for_enum(
7576
impl clap::ValueEnum for #item_name {
7677
#value_variants
7778
#to_possible_value
79+
#from_str_for_fallback
7880
}
7981
})
8082
}
@@ -85,6 +87,9 @@ fn lits(variants: &[(&Variant, Item)]) -> Result<Vec<(TokenStream, Ident)>, syn:
8587
if let Kind::Skip(_, _) = &*item.kind() {
8688
continue;
8789
}
90+
if item.is_fallback() {
91+
continue;
92+
}
8893
if !matches!(variant.fields, Fields::Unit) {
8994
abort!(variant.span(), "`#[derive(ValueEnum)]` only supports unit variants. Non-unit variants must be skipped");
9095
}
@@ -128,3 +133,62 @@ fn gen_to_possible_value(item: &Item, lits: &[(TokenStream, Ident)]) -> TokenStr
128133
}
129134
}
130135
}
136+
137+
fn gen_from_str_for_fallback(variants: &[(&Variant, Item)]) -> syn::Result<TokenStream> {
138+
let fallbacks: Vec<_> = variants
139+
.iter()
140+
.filter(|(_, item)| item.is_fallback())
141+
.collect();
142+
143+
match fallbacks.as_slice() {
144+
[] => Ok(quote!()),
145+
[(variant, _)] => {
146+
let ident = &variant.ident;
147+
let variant_initialization = match variant.fields.len() {
148+
_ if matches!(variant.fields, Fields::Unit) => quote! {#ident},
149+
0 => quote! {#ident{}},
150+
2.. => abort!(
151+
variant,
152+
"`fallback` only supports Unit variants, or variants with a single field"
153+
),
154+
1 => {
155+
let member = variant
156+
.fields
157+
.members()
158+
.next()
159+
.expect("there should be exactly one field");
160+
quote! {#ident{
161+
#member: {
162+
use std::convert::Into;
163+
__input.into()
164+
},
165+
}}
166+
}
167+
};
168+
Ok(quote! {
169+
fn from_str(__input: &::std::primitive::str, __ignore_case: ::std::primitive::bool) -> ::std::result::Result<Self, ::std::string::String> {
170+
Ok(Self::value_variants()
171+
.iter()
172+
.find(|v| {
173+
v.to_possible_value()
174+
.expect("ValueEnum::value_variants contains only values with a corresponding ValueEnum::to_possible_value")
175+
.matches(__input, __ignore_case)
176+
})
177+
.cloned()
178+
.unwrap_or_else(|| Self::#variant_initialization))
179+
}
180+
})
181+
}
182+
[first, second, ..] => {
183+
let mut error = syn::Error::new_spanned(
184+
first.0,
185+
"`#[derive(ValueEnum)]` only supports one `fallback`.",
186+
);
187+
error.combine(syn::Error::new_spanned(
188+
second.0,
189+
"second fallback defined here",
190+
));
191+
Err(error)
192+
}
193+
}
194+
}

clap_derive/src/item.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub(crate) struct Item {
4949
skip_group: bool,
5050
group_id: Name,
5151
group_methods: Vec<Method>,
52+
/// Used as fallback value `ValueEnum`.
53+
is_fallback: bool,
5254
kind: Sp<Kind>,
5355
}
5456

@@ -279,6 +281,7 @@ impl Item {
279281
group_id,
280282
group_methods: vec![],
281283
kind,
284+
is_fallback: false,
282285
}
283286
}
284287

@@ -835,6 +838,12 @@ impl Item {
835838
self.skip_group = true;
836839
}
837840

841+
Some(MagicAttrName::Fallback) => {
842+
assert_attr_kind(attr, &[AttrKind::Value])?;
843+
844+
self.is_fallback = true;
845+
}
846+
838847
None
839848
// Magic only for the default, otherwise just forward to the builder
840849
| Some(MagicAttrName::Short)
@@ -1077,6 +1086,10 @@ impl Item {
10771086
pub(crate) fn skip_group(&self) -> bool {
10781087
self.skip_group
10791088
}
1089+
1090+
pub(crate) fn is_fallback(&self) -> bool {
1091+
self.is_fallback
1092+
}
10801093
}
10811094

10821095
#[derive(Clone)]

tests/derive/value_enum.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,152 @@ fn vec_type_default_value() {
626626
Opt::try_parse_from(["", "-a", "foo,baz"]).unwrap()
627627
);
628628
}
629+
630+
#[test]
631+
fn unit_fallback() {
632+
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
633+
enum ArgChoice {
634+
Foo,
635+
#[clap(fallback)]
636+
Fallback,
637+
}
638+
639+
#[derive(Parser, PartialEq, Debug)]
640+
struct Opt {
641+
#[arg(value_enum)]
642+
arg: ArgChoice,
643+
}
644+
645+
assert_eq!(
646+
Opt {
647+
arg: ArgChoice::Foo
648+
},
649+
Opt::try_parse_from(["", "foo"]).unwrap()
650+
);
651+
assert_eq!(
652+
Opt {
653+
arg: ArgChoice::Fallback
654+
},
655+
Opt::try_parse_from(["", "not-foo"]).unwrap()
656+
);
657+
}
658+
659+
#[test]
660+
fn empty_tuple_fallback() {
661+
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
662+
enum ArgChoice {
663+
Foo,
664+
#[clap(fallback)]
665+
Fallback(),
666+
}
667+
668+
#[derive(Parser, PartialEq, Debug)]
669+
struct Opt {
670+
#[arg(value_enum)]
671+
arg: ArgChoice,
672+
}
673+
674+
assert_eq!(
675+
Opt {
676+
arg: ArgChoice::Foo
677+
},
678+
Opt::try_parse_from(["", "foo"]).unwrap()
679+
);
680+
assert_eq!(
681+
Opt {
682+
arg: ArgChoice::Fallback()
683+
},
684+
Opt::try_parse_from(["", "not-foo"]).unwrap()
685+
);
686+
}
687+
688+
#[test]
689+
fn empty_struct_fallback() {
690+
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
691+
enum ArgChoice {
692+
Foo,
693+
#[clap(fallback)]
694+
Fallback {},
695+
}
696+
697+
#[derive(Parser, PartialEq, Debug)]
698+
struct Opt {
699+
#[arg(value_enum)]
700+
arg: ArgChoice,
701+
}
702+
703+
assert_eq!(
704+
Opt {
705+
arg: ArgChoice::Foo
706+
},
707+
Opt::try_parse_from(["", "foo"]).unwrap()
708+
);
709+
assert_eq!(
710+
Opt {
711+
arg: ArgChoice::Fallback {}
712+
},
713+
Opt::try_parse_from(["", "not-foo"]).unwrap()
714+
);
715+
}
716+
717+
#[test]
718+
fn non_empty_struct_fallback() {
719+
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
720+
enum ArgChoice {
721+
Foo,
722+
#[clap(fallback)]
723+
Fallback {
724+
value: String,
725+
},
726+
}
727+
728+
#[derive(Parser, PartialEq, Debug)]
729+
struct Opt {
730+
#[arg(value_enum)]
731+
arg: ArgChoice,
732+
}
733+
734+
assert_eq!(
735+
Opt {
736+
arg: ArgChoice::Foo
737+
},
738+
Opt::try_parse_from(["", "foo"]).unwrap()
739+
);
740+
assert_eq!(
741+
Opt {
742+
arg: ArgChoice::Fallback {
743+
value: String::from("not-foo")
744+
}
745+
},
746+
Opt::try_parse_from(["", "not-foo"]).unwrap()
747+
);
748+
}
749+
750+
#[test]
751+
fn non_empty_tuple_fallback() {
752+
#[derive(clap::ValueEnum, PartialEq, Debug, Clone)]
753+
enum ArgChoice {
754+
Foo,
755+
#[clap(fallback)]
756+
Fallback(String),
757+
}
758+
759+
#[derive(Parser, PartialEq, Debug)]
760+
struct Opt {
761+
#[arg(value_enum)]
762+
arg: ArgChoice,
763+
}
764+
765+
assert_eq!(
766+
Opt {
767+
arg: ArgChoice::Foo
768+
},
769+
Opt::try_parse_from(["", "foo"]).unwrap()
770+
);
771+
assert_eq!(
772+
Opt {
773+
arg: ArgChoice::Fallback(String::from("not-foo"))
774+
},
775+
Opt::try_parse_from(["", "not-foo"]).unwrap()
776+
);
777+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use clap::ValueEnum;
2+
3+
#[derive(ValueEnum, Clone, Debug)]
4+
enum Opt {
5+
#[clap(fallback)]
6+
First(String, String),
7+
}
8+
9+
fn main() {}
10+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: `fallback` only supports Unit variants, or variants with a single field
2+
--> tests/derive_ui/value_enum_fallback_two_fields.rs:5:5
3+
|
4+
5 | / #[clap(fallback)]
5+
6 | | First(String, String),
6+
| |_________________________^
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use clap::ValueEnum;
2+
3+
#[derive(ValueEnum, Clone, Debug)]
4+
enum Opt {
5+
#[clap(fallback)]
6+
First(String),
7+
#[clap(fallback)]
8+
Second(String),
9+
}
10+
11+
fn main() {}

0 commit comments

Comments
 (0)