From 2122e2b2dd0fe8f5b8a5a454e0b2be4303863c79 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 10 Oct 2022 10:53:29 -0500 Subject: [PATCH] feat(parser): Add TypedValueParser::try_map This is a major building block to avoid needing to implement `TypedValueParser` Inspired by #4362 --- src/builder/value_parser.rs | 110 +++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/src/builder/value_parser.rs b/src/builder/value_parser.rs index 6aece7a78a3..1b2b51dba13 100644 --- a/src/builder/value_parser.rs +++ b/src/builder/value_parser.rs @@ -602,9 +602,9 @@ where /// /// As alternatives to implementing `TypedValueParser`, /// - Use `Fn(&str) -> Result` which implements `TypedValueParser` -/// - [`TypedValueParser::map`] to adapt an existing `TypedValueParser` +/// - [`TypedValueParser::map`] or [`TypedValueParser::try_map`] to adapt an existing `TypedValueParser` /// -/// See `ValueParserFactory` for register `TypedValueParser::Value` with +/// See `ValueParserFactory` to register `TypedValueParser::Value` with /// [`value_parser!`][crate::value_parser]. /// /// # Example @@ -726,6 +726,56 @@ pub trait TypedValueParser: Clone + Send + Sync + 'static { { MapValueParser::new(self, func) } + + /// Adapt a `TypedValueParser` from one value to another + /// + /// # Example + /// + /// ```rust + /// # use std::ffi::OsString; + /// # use std::ffi::OsStr; + /// # use std::path::PathBuf; + /// # use std::path::Path; + /// # use clap::Command; + /// # use clap::Arg; + /// # use clap::builder::TypedValueParser as _; + /// # use clap::builder::OsStringValueParser; + /// let cmd = Command::new("mycmd") + /// .arg( + /// Arg::new("flag") + /// .long("flag") + /// .value_parser( + /// OsStringValueParser::new() + /// .try_map(verify_ext) + /// ) + /// ); + /// + /// fn verify_ext(os: OsString) -> Result { + /// let path = PathBuf::from(os); + /// if path.extension() != Some(OsStr::new("rs")) { + /// return Err("only Rust files are supported"); + /// } + /// Ok(path) + /// } + /// + /// let error = cmd.clone().try_get_matches_from(["mycmd", "--flag", "foo.txt"]).unwrap_err(); + /// error.print(); + /// + /// let matches = cmd.try_get_matches_from(["mycmd", "--flag", "foo.rs"]).unwrap(); + /// assert!(matches.contains_id("flag")); + /// assert_eq!( + /// matches.get_one::("flag").map(|s| s.as_path()), + /// Some(Path::new("foo.rs")) + /// ); + /// ``` + fn try_map(self, func: F) -> TryMapValueParser + where + F: Fn(Self::Value) -> Result + Clone + Send + Sync + 'static, + T: Send + Sync + Clone, + E: Into>, + { + TryMapValueParser::new(self, func) + } } impl TypedValueParser for F @@ -1931,6 +1981,62 @@ where } } +/// Adapt a `TypedValueParser` from one value to another +/// +/// See [`TypedValueParser::try_map`] +#[derive(Clone, Debug)] +pub struct TryMapValueParser { + parser: P, + func: F, +} + +impl TryMapValueParser +where + P: TypedValueParser, + P::Value: Send + Sync + Clone, + F: Fn(P::Value) -> Result + Clone + Send + Sync + 'static, + T: Send + Sync + Clone, + E: Into>, +{ + fn new(parser: P, func: F) -> Self { + Self { parser, func } + } +} + +impl TypedValueParser for TryMapValueParser +where + P: TypedValueParser, + P::Value: Send + Sync + Clone, + F: Fn(P::Value) -> Result + Clone + Send + Sync + 'static, + T: Send + Sync + Clone, + E: Into>, +{ + type Value = T; + + fn parse_ref( + &self, + cmd: &crate::Command, + arg: Option<&crate::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let mid_value = ok!(self.parser.parse_ref(cmd, arg, value)); + let value = ok!((self.func)(mid_value).map_err(|e| { + let arg = arg + .map(|a| a.to_string()) + .unwrap_or_else(|| "...".to_owned()); + crate::Error::value_validation(arg, value.to_string_lossy().into_owned(), e.into()) + .with_cmd(cmd) + })); + Ok(value) + } + + fn possible_values( + &self, + ) -> Option + '_>> { + self.parser.possible_values() + } +} + /// Register a type with [value_parser!][crate::value_parser!] /// /// # Example