feat: Implement Arg::required_if_eq_all

This commit is contained in:
Omar El Halabi 2021-02-27 20:10:02 +02:00
parent c9e875e036
commit 701a4610b3
5 changed files with 221 additions and 2 deletions

View file

@ -144,6 +144,15 @@ pub(crate) fn assert_app(app: &App) {
);
}
for req in &arg.r_ifs_all {
assert!(
app.id_exists(&req.0),
"Argument or group '{:?}' specified in 'required_if_eq_all' for '{}' does not exist",
req.0,
arg.name
);
}
for req in &arg.r_unless {
assert!(
app.id_exists(req),

View file

@ -91,6 +91,7 @@ pub struct Arg<'help> {
pub(crate) groups: Vec<Id>,
pub(crate) requires: Vec<(Option<&'help str>, Id)>,
pub(crate) r_ifs: Vec<(Id, &'help str)>,
pub(crate) r_ifs_all: Vec<(Id, &'help str)>,
pub(crate) r_unless: Vec<Id>,
pub(crate) short: Option<char>,
pub(crate) long: Option<&'help str>,
@ -1436,7 +1437,7 @@ impl<'help> Arg<'help> {
/// Allows specifying that this argument is [required] based on multiple conditions. The
/// conditions are set up in a `(arg, val)` style tuple. The requirement will only become valid
/// if one of the specified `arg`'s value equals it's corresponding `val`.
/// if one of the specified `arg`'s value equals its corresponding `val`.
///
/// **NOTE:** If using YAML the values should be laid out as follows
///
@ -1520,6 +1521,90 @@ impl<'help> Arg<'help> {
self
}
/// Allows specifying that this argument is [required] based on multiple conditions. The
/// conditions are set up in a `(arg, val)` style tuple. The requirement will only become valid
/// if every one of the specified `arg`'s value equals its corresponding `val`.
///
/// **NOTE:** If using YAML the values should be laid out as follows
///
/// ```yaml
/// required_if_eq_all:
/// - [arg, val]
/// - [arg2, val2]
/// ```
///
/// # Examples
///
/// ```rust
/// # use clap::Arg;
/// Arg::new("config")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// # ;
/// ```
///
/// Setting `Arg::required_if_eq_all(&[(arg, val)])` makes this arg required if all of the `arg`s
/// are used at runtime and every value is equal to its corresponding `val`. If the `arg`'s value is
/// anything other than `val`, this argument isn't required.
///
/// ```rust
/// # use clap::{App, Arg};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// .takes_value(true)
/// .long("config"))
/// .arg(Arg::new("extra")
/// .takes_value(true)
/// .long("extra"))
/// .arg(Arg::new("option")
/// .takes_value(true)
/// .long("option"))
/// .try_get_matches_from(vec![
/// "prog", "--option", "spec"
/// ]);
///
/// assert!(res.is_ok()); // We didn't use --option=spec --extra=val so "cfg" isn't required
/// ```
///
/// Setting `Arg::required_if_eq_all(&[(arg, val)])` and having all of the `arg`s used with its
/// value of `val` but *not* using this arg is an error.
///
/// ```rust
/// # use clap::{App, Arg, ErrorKind};
/// let res = App::new("prog")
/// .arg(Arg::new("cfg")
/// .required_if_eq_all(&[
/// ("extra", "val"),
/// ("option", "spec")
/// ])
/// .takes_value(true)
/// .long("config"))
/// .arg(Arg::new("extra")
/// .takes_value(true)
/// .long("extra"))
/// .arg(Arg::new("option")
/// .takes_value(true)
/// .long("option"))
/// .try_get_matches_from(vec![
/// "prog", "--extra", "val", "--option", "spec"
/// ]);
///
/// assert!(res.is_err());
/// assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
/// ```
/// [required]: ./struct.Arg.html#method.required
pub fn required_if_eq_all<T: Key>(mut self, ifs: &[(T, &'help str)]) -> Self {
self.r_ifs_all
.extend(ifs.iter().map(|(id, val)| (Id::from_ref(id), *val)));
self
}
/// Sets multiple arguments by names that are required when this one is present I.e. when
/// using this argument, the following arguments *must* be present.
///
@ -4536,7 +4621,8 @@ impl<'help> From<&'help Yaml> for Arg<'help> {
"long_help" => yaml_to_str!(a, v, long_about),
"required" => yaml_to_bool!(a, v, required),
"required_if_eq" => yaml_tuple2!(a, v, required_if_eq),
"required_if_eq_any" => yaml_tuple2!(a, v, required_if_eq),
"required_if_eq_any" => yaml_array_tuple2!(a, v, required_if_eq_any),
"required_if_eq_all" => yaml_array_tuple2!(a, v, required_if_eq_all),
"takes_value" => yaml_to_bool!(a, v, takes_value),
"index" => yaml_to_usize!(a, v, index),
"global" => yaml_to_bool!(a, v, global),

View file

@ -17,6 +17,25 @@ macro_rules! yaml_tuple2 {
}};
}
#[cfg(feature = "yaml")]
macro_rules! yaml_array_tuple2 {
($a:ident, $v:ident, $c:ident) => {{
if let Some(vec) = $v.as_vec() {
for ys in vec {
if let Some(tup) = ys.as_vec() {
debug_assert_eq!(2, tup.len());
$a = $a.$c(&[(yaml_str!(tup[0]), yaml_str!(tup[1]))]);
} else {
panic!("Failed to convert YAML value to vec");
}
}
} else {
panic!("Failed to convert YAML value to vec");
}
$a
}};
}
#[cfg(feature = "yaml")]
macro_rules! yaml_tuple3 {
($a:ident, $v:ident, $c:ident) => {{

View file

@ -568,6 +568,23 @@ impl<'help, 'app, 'parser> Validator<'help, 'app, 'parser> {
}
}
}
let mut match_all = true;
for (other, val) in &a.r_ifs_all {
if let Some(ma) = matcher.get(other) {
if !ma.contains_val(val) {
match_all = false;
break;
}
} else {
match_all = false;
break;
}
}
if match_all && !a.r_ifs_all.is_empty() && !matcher.contains(&a.id) {
return self.missing_required_error(matcher, vec![a.id.clone()]);
}
}
Ok(())
}

View file

@ -533,6 +533,94 @@ fn required_if_val_present_fail() {
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}
#[test]
fn required_if_all_values_present_pass() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec![
"ri", "--extra", "val", "--option", "spec", "--config", "my.cfg",
]);
assert!(res.is_ok());
}
#[test]
fn required_if_some_values_present_pass() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec!["ri", "--extra", "val"]);
assert!(res.is_ok());
}
#[test]
fn required_if_all_values_present_fail() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec!["ri", "--extra", "val", "--option", "spec"]);
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}
#[test]
fn required_if_any_all_values_present_pass() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.required_if_eq_any(&[("extra", "val2"), ("option", "spec2")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec![
"ri", "--extra", "val", "--option", "spec", "--config", "my.cfg",
]);
assert!(res.is_ok());
}
#[test]
fn required_if_any_all_values_present_fail() {
let res = App::new("ri")
.arg(
Arg::new("cfg")
.required_if_eq_all(&[("extra", "val"), ("option", "spec")])
.required_if_eq_any(&[("extra", "val2"), ("option", "spec2")])
.takes_value(true)
.long("config"),
)
.arg(Arg::new("extra").takes_value(true).long("extra"))
.arg(Arg::new("option").takes_value(true).long("option"))
.try_get_matches_from(vec!["ri", "--extra", "val", "--option", "spec"]);
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind, ErrorKind::MissingRequiredArgument);
}
#[test]
fn list_correct_required_args() {
let app = App::new("Test app")