From 84ae2ddbceda34b5cbda98a6959edaa52fde2e1a Mon Sep 17 00:00:00 2001 From: Kevin K Date: Thu, 13 Aug 2015 23:22:48 -0400 Subject: [PATCH] feat(Args): allows for custom argument value validations to be defined Closes #170 --- src/app.rs | 47 +++++++++++++++++++++++++++++-- src/args/arg.rs | 45 ++++++++++++++++++++++++++--- src/args/argbuilder/option.rs | 3 ++ src/args/argbuilder/positional.rs | 7 +++-- 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index e16203e6..13bce7c4 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 5fb93a93..e76072a6 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}; @@ -90,7 +91,9 @@ pub struct Arg<'n, 'l, 'h, 'g, 'p, 'r> { #[doc(hidden)] pub empty_vals: bool, #[doc(hidden)] - pub global: bool + pub global: bool, + #[doc(hidden)] + pub validator: Option Result<(), String>>> } 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> { val_names: None, group: None, global: false, - empty_vals: true + empty_vals: true, + validator: None } } @@ -289,7 +293,8 @@ impl<'n, 'l, 'h, 'g, 'p, 'r> Arg<'n, 'l, 'h, 'g, 'p, 'r> { min_vals: None, group: None, global: false, - empty_vals: true + empty_vals: true, + validator: None, } } @@ -663,6 +668,37 @@ 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 + /// + /// ```no_run + /// # use clap::{App, Arg}; + /// # let matches = App::new("myprog") + /// # .arg( + /// # Arg::with_name("debug").index(1) + /// .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: Fn(String) -> Result<(), String> + 'static { + self.validator = Some(Rc::new(f)); + self + } + /// Specifies the *maximum* number of values are for this argument. For example, if you had a /// `-f ` argument where you wanted up to 3 'files' you would set /// `.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<' val_names: a.val_names.clone(), group: a.group, global: a.global, - empty_vals: a.empty_vals + 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 8733534a..420b676a 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 d7b63594..8f9b62c1 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));