From b9fca8528266189a0f1ef25cdf03ef38067ccb71 Mon Sep 17 00:00:00 2001 From: Kevin K Date: Fri, 14 Aug 2015 18:42:36 -0400 Subject: [PATCH] feat(Args): allows custom validations Custom validations can now be performed on the argument values that are fatal when the tests fail. Closes #170 --- src/app.rs | 47 ++++++++++++++++++- src/args/arg.rs | 76 +++++++++++++++++++------------ src/args/argbuilder/option.rs | 3 ++ src/args/argbuilder/positional.rs | 7 ++- 4 files changed, 99 insertions(+), 34 deletions(-) diff --git a/src/app.rs b/src/app.rs index e16203e6de54..13bce7c41327 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 {} \ @@ -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() { @@ -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); @@ -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(); @@ -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. \ @@ -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()); } @@ -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, @@ -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 { @@ -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() { diff --git a/src/args/arg.rs b/src/args/arg.rs index 2ff214e5d353..e76072a66732 100644 --- a/src/args/arg.rs +++ b/src/args/arg.rs @@ -1,5 +1,6 @@ use std::iter::IntoIterator; use std::collections::HashSet; +use std::rc::Rc; use usageparser::{UsageParser, UsageToken}; @@ -92,7 +93,7 @@ pub struct Arg<'n, 'l, 'h, 'g, 'p, 'r> { #[doc(hidden)] pub global: bool, #[doc(hidden)] - pub validator: Option Result<(), String>>> + pub validator: Option Result<(), String>>> } impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> { @@ -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 /// @@ -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(mut self, f: F) -> Self where F: FnOnce(String) -> Result<(), String> + 'static { - self.validator = Some(Box::new(f)); + pub fn validator(mut self, f: F) -> Self where F: Fn(String) -> Result<(), String> + 'static { + self.validator = Some(Rc::new(f)); self } @@ -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 -// } -// } -// } \ No newline at end of file +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() + } + } +} \ No newline at end of file diff --git a/src/args/argbuilder/option.rs b/src/args/argbuilder/option.rs index 8733534a1b51..420b676a4902 100644 --- a/src/args/argbuilder/option.rs +++ b/src/args/argbuilder/option.rs @@ -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, @@ -31,6 +33,7 @@ pub struct OptBuilder<'n> { pub val_names: Option>, pub empty_vals: bool, pub global: bool, + pub validator: Option StdResult<(), String>>> } impl<'n> Display for OptBuilder<'n> { diff --git a/src/args/argbuilder/positional.rs b/src/args/argbuilder/positional.rs index d7b635943471..8f9b62c118bb 100644 --- a/src/args/argbuilder/positional.rs +++ b/src/args/argbuilder/positional.rs @@ -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, @@ -27,12 +29,13 @@ pub struct PosBuilder<'n> { pub max_vals: Option, pub min_vals: Option, pub empty_vals: bool, - pub global: bool + pub global: bool, + pub validator: Option 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));