Skip to content

Commit

Permalink
feat(Args): allows custom validations
Browse files Browse the repository at this point in the history
Custom validations can now be performed on the argument values that are
fatal when the tests fail.

Closes #170
  • Loading branch information
kbknapp committed Aug 14, 2015
1 parent 88ae711 commit b9fca85
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 34 deletions.
47 changes: 45 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,8 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
max_vals: a.max_vals,
help: a.help,
global: a.global,
empty_vals: a.empty_vals
empty_vals: a.empty_vals,
validator: None
};
if pb.min_vals.is_some() && !pb.multiple {
panic!("Argument \"{}\" does not allow multiple values, yet it is expecting {} \
Expand Down Expand Up @@ -700,6 +701,9 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
for n in p { phs.insert(*n); }
pb.possible_vals = Some(phs);
}
if let Some(ref p) = a.validator {
pb.validator = Some(p.clone());
}
self.positionals_idx.insert(i, pb);
} else if a.takes_value {
if a.short.is_none() && a.long.is_none() {
Expand All @@ -722,7 +726,8 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
val_names: a.val_names.clone(),
requires: None,
required: a.required,
empty_vals: a.empty_vals
empty_vals: a.empty_vals,
validator: None
};
if let Some(ref vec) = ob.val_names {
ob.num_vals = Some(vec.len() as u8);
Expand All @@ -743,6 +748,9 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
for n in bl { bhs.insert(*n); }
ob.blacklist = Some(bhs);
}
if let Some(ref p) = a.validator {
ob.validator = Some(p.clone());
}
// Check if there is anything in the requires list and add any values
if let Some(ref r) = a.requires {
let mut rhs = HashSet::new();
Expand All @@ -764,6 +772,10 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
}
self.opts.insert(a.name, ob);
} else {
if a.validator.is_some() {
panic!("The argument '{}' has a validator set, yet was parsed as a flag. Ensure \
.takes_value(true) or .index(u8) is set.")
}
if !a.empty_vals {
// Empty vals defaults to true, so if it's false it was manually set
panic!("The argument '{}' cannot have empty_values() set because it is a flag. \
Expand Down Expand Up @@ -1758,6 +1770,13 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
if let Some(ref mut o) = matches.args.get_mut(opt.name) {
// Options have values, so we can unwrap()
if let Some(ref mut vals) = o.values {
if let Some(ref vtor) = opt.validator {
if let Err(e) = vtor(arg_slice.to_owned()) {
self.report_error(e,
true,
Some(vec![opt.name]));
}
}
let len = vals.len() as u8 + 1;
vals.insert(len, arg_slice.to_owned());
}
Expand Down Expand Up @@ -1928,6 +1947,14 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
Some(matches.args.keys()
.map(|k| *k).collect()));
}
if let Some(ref vtor) = p.validator {
let f = &*vtor;
if let Err(ref e) = f(arg_slice.to_owned()) {
self.report_error(e.clone(),
true,
Some(matches.args.keys().map(|k| *k).collect()));
}
}
bm.insert(1, arg_slice.to_owned());
matches.args.insert(p.name, MatchedArg{
occurrences: 1,
Expand Down Expand Up @@ -2234,6 +2261,13 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
Some(matches.args.keys()
.map(|k| *k).collect()));
}
if let Some(ref vtor) = v.validator {
if let Err(e) = vtor(arg_val.clone().unwrap()) {
self.report_error(e,
true,
Some(matches.args.keys().map(|k| *k).collect()));
}
}
if let Some(ref mut o) = matches.args.get_mut(v.name) {
o.occurrences += 1;
if let Some(ref mut vals) = o.values {
Expand All @@ -2250,6 +2284,15 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
Some(matches.args.keys()
.map(|k| *k).collect()));
}
if let Some(ref val) = arg_val {
if let Some(ref vtor) = v.validator {
if let Err(e) = vtor(val.clone()) {
self.report_error(e,
true,
Some(matches.args.keys().map(|k| *k).collect()));
}
}
}
matches.args.insert(v.name, MatchedArg{
occurrences: if arg_val.is_some() { 1 } else { 0 },
values: if arg_val.is_some() {
Expand Down
76 changes: 46 additions & 30 deletions src/args/arg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::iter::IntoIterator;
use std::collections::HashSet;
use std::rc::Rc;

use usageparser::{UsageParser, UsageToken};

Expand Down Expand Up @@ -92,7 +93,7 @@ pub struct Arg<'n, 'l, 'h, 'g, 'p, 'r> {
#[doc(hidden)]
pub global: bool,
#[doc(hidden)]
pub validator: Option<Box<FnOnce(String) -> Result<(), String>>>
pub validator: Option<Rc<Fn(String) -> Result<(), String>>>
}

impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
Expand Down Expand Up @@ -667,6 +668,16 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
self
}

/// Allows one to perform a validation on the argument value. You provide a closure which
/// accepts a `String` value, a `Result` where the `Err(String)` is a message displayed to the
/// user.
///
/// **NOTE:** The error message does *not* need to contain the `error:` portion, only the
/// message.
///
/// **NOTE:** There is a small performance hit for using validators, as they are implemented
/// with `Rc` pointers. And the value to be checked will be allocated an extra time in order to
/// to be passed to the closure.
///
/// # Example
///
Expand All @@ -675,10 +686,16 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
/// # let matches = App::new("myprog")
/// # .arg(
/// # Arg::with_name("debug").index(1)
/// .number_of_values(3)
/// .validator(|val| {
/// if val.contains("@") {
/// Ok(())
/// } else {
/// Err(String::from("the value must contain at lesat one '@' character"))
/// }
/// })
/// # ).get_matches();
pub fn validator<F>(mut self, f: F) -> Self where F: FnOnce(String) -> Result<(), String> + 'static {
self.validator = Some(Box::new(f));
pub fn validator<F>(mut self, f: F) -> Self where F: Fn(String) -> Result<(), String> + 'static {
self.validator = Some(Rc::new(f));
self
}

Expand Down Expand Up @@ -779,29 +796,28 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> {
}
}

// impl<'n, 'l, 'h, 'g, 'p, 'r, 'z> From<&'z Arg<'n, 'l, 'h, 'g, 'p, 'r>> for Arg<'n, 'l, 'h, 'g, 'p, 'r> {
// fn from(a: &'z Arg<'n, 'l, 'h, 'g, 'p, 'r>) -> Self {
// let f = mem::replace(dest, mut src)
// Arg {
// name: a.name,
// short: a.short,
// long: a.long,
// help: a.help,
// required: a.required,
// takes_value: a.takes_value,
// multiple: a.multiple,
// index: a.index,
// possible_vals: a.possible_vals.clone(),
// blacklist: a.blacklist.clone(),
// requires: a.requires.clone(),
// num_vals: a.num_vals,
// min_vals: a.min_vals,
// max_vals: a.max_vals,
// val_names: a.val_names.clone(),
// group: a.group,
// global: a.global,
// empty_vals: a.empty_vals,
// validator: a.validator
// }
// }
// }
impl<'n, 'l, 'h, 'g, 'p, 'r, 'z> From<&'z Arg<'n, 'l, 'h, 'g, 'p, 'r>> for Arg<'n, 'l, 'h, 'g, 'p, 'r> {
fn from(a: &'z Arg<'n, 'l, 'h, 'g, 'p, 'r>) -> Self {
Arg {
name: a.name,
short: a.short,
long: a.long,
help: a.help,
required: a.required,
takes_value: a.takes_value,
multiple: a.multiple,
index: a.index,
possible_vals: a.possible_vals.clone(),
blacklist: a.blacklist.clone(),
requires: a.requires.clone(),
num_vals: a.num_vals,
min_vals: a.min_vals,
max_vals: a.max_vals,
val_names: a.val_names.clone(),
group: a.group,
global: a.global,
empty_vals: a.empty_vals,
validator: a.validator.clone()
}
}
}
3 changes: 3 additions & 0 deletions src/args/argbuilder/option.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::rc::Rc;
use std::collections::HashSet;
use std::collections::BTreeSet;
use std::fmt::{ Display, Formatter, Result };
use std::result::Result as StdResult;

pub struct OptBuilder<'n> {
pub name: &'n str,
Expand Down Expand Up @@ -31,6 +33,7 @@ pub struct OptBuilder<'n> {
pub val_names: Option<Vec<&'n str>>,
pub empty_vals: bool,
pub global: bool,
pub validator: Option<Rc<Fn(String) -> StdResult<(), String>>>
}

impl<'n> Display for OptBuilder<'n> {
Expand Down
7 changes: 5 additions & 2 deletions src/args/argbuilder/positional.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::HashSet;
use std::collections::BTreeSet;
use std::fmt::{ Display, Formatter, Result };
use std::result::Result as StdResult;
use std::rc::Rc;

pub struct PosBuilder<'n> {
pub name: &'n str,
Expand All @@ -27,12 +29,13 @@ pub struct PosBuilder<'n> {
pub max_vals: Option<u8>,
pub min_vals: Option<u8>,
pub empty_vals: bool,
pub global: bool
pub global: bool,
pub validator: Option<Rc<Fn(String) -> StdResult<(), String>>>
}

impl<'n> Display for PosBuilder<'n> {
fn fmt(&self, f: &mut Formatter) -> Result {
if self.required {
if self.required {
try!(write!(f, "<{}>", self.name));
} else {
try!(write!(f, "[{}]", self.name));
Expand Down

0 comments on commit b9fca85

Please sign in to comment.