Skip to content

Commit 84ae2dd

Browse files
committed
feat(Args): allows for custom argument value validations to be defined
Closes #170
1 parent 23aca97 commit 84ae2dd

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

src/app.rs

+45-2
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,8 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
663663
max_vals: a.max_vals,
664664
help: a.help,
665665
global: a.global,
666-
empty_vals: a.empty_vals
666+
empty_vals: a.empty_vals,
667+
validator: None
667668
};
668669
if pb.min_vals.is_some() && !pb.multiple {
669670
panic!("Argument \"{}\" does not allow multiple values, yet it is expecting {} \
@@ -700,6 +701,9 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
700701
for n in p { phs.insert(*n); }
701702
pb.possible_vals = Some(phs);
702703
}
704+
if let Some(ref p) = a.validator {
705+
pb.validator = Some(p.clone());
706+
}
703707
self.positionals_idx.insert(i, pb);
704708
} else if a.takes_value {
705709
if a.short.is_none() && a.long.is_none() {
@@ -722,7 +726,8 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
722726
val_names: a.val_names.clone(),
723727
requires: None,
724728
required: a.required,
725-
empty_vals: a.empty_vals
729+
empty_vals: a.empty_vals,
730+
validator: None
726731
};
727732
if let Some(ref vec) = ob.val_names {
728733
ob.num_vals = Some(vec.len() as u8);
@@ -743,6 +748,9 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
743748
for n in bl { bhs.insert(*n); }
744749
ob.blacklist = Some(bhs);
745750
}
751+
if let Some(ref p) = a.validator {
752+
ob.validator = Some(p.clone());
753+
}
746754
// Check if there is anything in the requires list and add any values
747755
if let Some(ref r) = a.requires {
748756
let mut rhs = HashSet::new();
@@ -764,6 +772,10 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
764772
}
765773
self.opts.insert(a.name, ob);
766774
} else {
775+
if a.validator.is_some() {
776+
panic!("The argument '{}' has a validator set, yet was parsed as a flag. Ensure \
777+
.takes_value(true) or .index(u8) is set.")
778+
}
767779
if !a.empty_vals {
768780
// Empty vals defaults to true, so if it's false it was manually set
769781
panic!("The argument '{}' cannot have empty_values() set because it is a flag. \
@@ -1758,6 +1770,13 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
17581770
if let Some(ref mut o) = matches.args.get_mut(opt.name) {
17591771
// Options have values, so we can unwrap()
17601772
if let Some(ref mut vals) = o.values {
1773+
if let Some(ref vtor) = opt.validator {
1774+
if let Err(e) = vtor(arg_slice.to_owned()) {
1775+
self.report_error(e,
1776+
true,
1777+
Some(vec![opt.name]));
1778+
}
1779+
}
17611780
let len = vals.len() as u8 + 1;
17621781
vals.insert(len, arg_slice.to_owned());
17631782
}
@@ -1928,6 +1947,14 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
19281947
Some(matches.args.keys()
19291948
.map(|k| *k).collect()));
19301949
}
1950+
if let Some(ref vtor) = p.validator {
1951+
let f = &*vtor;
1952+
if let Err(ref e) = f(arg_slice.to_owned()) {
1953+
self.report_error(e.clone(),
1954+
true,
1955+
Some(matches.args.keys().map(|k| *k).collect()));
1956+
}
1957+
}
19311958
bm.insert(1, arg_slice.to_owned());
19321959
matches.args.insert(p.name, MatchedArg{
19331960
occurrences: 1,
@@ -2234,6 +2261,13 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
22342261
Some(matches.args.keys()
22352262
.map(|k| *k).collect()));
22362263
}
2264+
if let Some(ref vtor) = v.validator {
2265+
if let Err(e) = vtor(arg_val.clone().unwrap()) {
2266+
self.report_error(e,
2267+
true,
2268+
Some(matches.args.keys().map(|k| *k).collect()));
2269+
}
2270+
}
22372271
if let Some(ref mut o) = matches.args.get_mut(v.name) {
22382272
o.occurrences += 1;
22392273
if let Some(ref mut vals) = o.values {
@@ -2250,6 +2284,15 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
22502284
Some(matches.args.keys()
22512285
.map(|k| *k).collect()));
22522286
}
2287+
if let Some(ref val) = arg_val {
2288+
if let Some(ref vtor) = v.validator {
2289+
if let Err(e) = vtor(val.clone()) {
2290+
self.report_error(e,
2291+
true,
2292+
Some(matches.args.keys().map(|k| *k).collect()));
2293+
}
2294+
}
2295+
}
22532296
matches.args.insert(v.name, MatchedArg{
22542297
occurrences: if arg_val.is_some() { 1 } else { 0 },
22552298
values: if arg_val.is_some() {

src/args/arg.rs

+41-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::iter::IntoIterator;
22
use std::collections::HashSet;
3+
use std::rc::Rc;
34

45
use usageparser::{UsageParser, UsageToken};
56

@@ -90,7 +91,9 @@ pub struct Arg<'n, 'l, 'h, 'g, 'p, 'r> {
9091
#[doc(hidden)]
9192
pub empty_vals: bool,
9293
#[doc(hidden)]
93-
pub global: bool
94+
pub global: bool,
95+
#[doc(hidden)]
96+
pub validator: Option<Rc<Fn(String) -> Result<(), String>>>
9497
}
9598

9699
impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
@@ -131,7 +134,8 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
131134
val_names: None,
132135
group: None,
133136
global: false,
134-
empty_vals: true
137+
empty_vals: true,
138+
validator: None
135139
}
136140
}
137141

@@ -289,7 +293,8 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
289293
min_vals: None,
290294
group: None,
291295
global: false,
292-
empty_vals: true
296+
empty_vals: true,
297+
validator: None,
293298
}
294299
}
295300

@@ -663,6 +668,37 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
663668
self
664669
}
665670

671+
/// Allows one to perform a validation on the argument value. You provide a closure which
672+
/// accepts a `String` value, a `Result` where the `Err(String)` is a message displayed to the
673+
/// user.
674+
///
675+
/// **NOTE:** The error message does *not* need to contain the `error:` portion, only the
676+
/// message.
677+
///
678+
/// **NOTE:** There is a small performance hit for using validators, as they are implemented
679+
/// with `Rc` pointers. And the value to be checked will be allocated an extra time in order to
680+
/// to be passed to the closure.
681+
///
682+
/// # Example
683+
///
684+
/// ```no_run
685+
/// # use clap::{App, Arg};
686+
/// # let matches = App::new("myprog")
687+
/// # .arg(
688+
/// # Arg::with_name("debug").index(1)
689+
/// .validator(|val| {
690+
/// if val.contains("@") {
691+
/// Ok(())
692+
/// } else {
693+
/// Err(String::from("the value must contain at lesat one '@' character"))
694+
/// }
695+
/// })
696+
/// # ).get_matches();
697+
pub fn validator<F>(mut self, f: F) -> Self where F: Fn(String) -> Result<(), String> + 'static {
698+
self.validator = Some(Rc::new(f));
699+
self
700+
}
701+
666702
/// Specifies the *maximum* number of values are for this argument. For example, if you had a
667703
/// `-f <file>` argument where you wanted up to 3 'files' you would set
668704
/// `.max_values(3)`, and this argument would be satisfied if the user provided, 1, 2, or 3
@@ -780,7 +816,8 @@ impl<'n, 'l, 'h, 'g, 'p, 'r, 'z> From<&'z Arg<'n, 'l, 'h, 'g, 'p, 'r>> for Arg<'
780816
val_names: a.val_names.clone(),
781817
group: a.group,
782818
global: a.global,
783-
empty_vals: a.empty_vals
819+
empty_vals: a.empty_vals,
820+
validator: a.validator.clone()
784821
}
785822
}
786823
}

src/args/argbuilder/option.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::rc::Rc;
12
use std::collections::HashSet;
23
use std::collections::BTreeSet;
34
use std::fmt::{ Display, Formatter, Result };
5+
use std::result::Result as StdResult;
46

57
pub struct OptBuilder<'n> {
68
pub name: &'n str,
@@ -31,6 +33,7 @@ pub struct OptBuilder<'n> {
3133
pub val_names: Option<Vec<&'n str>>,
3234
pub empty_vals: bool,
3335
pub global: bool,
36+
pub validator: Option<Rc<Fn(String) -> StdResult<(), String>>>
3437
}
3538

3639
impl<'n> Display for OptBuilder<'n> {

src/args/argbuilder/positional.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::collections::HashSet;
22
use std::collections::BTreeSet;
33
use std::fmt::{ Display, Formatter, Result };
4+
use std::result::Result as StdResult;
5+
use std::rc::Rc;
46

57
pub struct PosBuilder<'n> {
68
pub name: &'n str,
@@ -27,12 +29,13 @@ pub struct PosBuilder<'n> {
2729
pub max_vals: Option<u8>,
2830
pub min_vals: Option<u8>,
2931
pub empty_vals: bool,
30-
pub global: bool
32+
pub global: bool,
33+
pub validator: Option<Rc<Fn(String) -> StdResult<(), String>>>
3134
}
3235

3336
impl<'n> Display for PosBuilder<'n> {
3437
fn fmt(&self, f: &mut Formatter) -> Result {
35-
if self.required {
38+
if self.required {
3639
try!(write!(f, "<{}>", self.name));
3740
} else {
3841
try!(write!(f, "[{}]", self.name));

0 commit comments

Comments
 (0)