From 31be35b241c5abf02de4868003325babca3eb65f Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 28 Dec 2020 07:58:57 -0800 Subject: [PATCH] feat: add a validator that checks for specific types conversion (#526) --- README.md | 3 +- include/CLI/Validators.hpp | 89 +++++++++++++------------------------- tests/AppTest.cpp | 18 ++++++++ 3 files changed, 51 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 2ec669b0..664c9573 100644 --- a/README.md +++ b/README.md @@ -409,6 +409,7 @@ CLI11 has several Validators built-in that perform some common checks - `CLI::NonNegativeNumber`: Requires the number be greater or equal to 0 - `CLI::Number`: Requires the input be a number. - `CLI::ValidIPV4`: Requires that the option be a valid IPv4 string e.g. `'255.255.255.255'`, `'10.1.1.7'`. +- `CLI::TypeValidator`:🚧 Requires that the option be convertible to the specified type e.g. `CLI::TypeValidator()` would require that the input be convertible to an `unsigned int` regardless of the end conversion. These Validators can be used by simply passing the name into the `check` or `transform` methods on an option @@ -715,7 +716,7 @@ app.set_config(option_name="", If this is called with no arguments, it will remove the configuration file option (like `set_help_flag`). Setting a configuration option is special. If it is present, it will be read along with the normal command line arguments. The file will be read if it exists, and does not throw an error unless `required` is `true`. Configuration files are in [TOML] format by default 🚧, though the default reader can also accept files in INI format as well 🆕. It should be noted that CLI11 does not contain a full TOML parser but can read strings from most TOML file and run them through the CLI11 parser. Other formats can be added by an adept user, some variations are available through customization points in the default formatter. An example of a TOML file 🆕: -```ini +```toml # Comments are supported, using a # # The default section is [default], case insensitive diff --git a/include/CLI/Validators.hpp b/include/CLI/Validators.hpp index 8992f75d..2c998384 100644 --- a/include/CLI/Validators.hpp +++ b/include/CLI/Validators.hpp @@ -419,53 +419,6 @@ class IPV4Validator : public Validator { } }; -/// Validate the argument is a number and greater than 0 -class PositiveNumber : public Validator { - public: - PositiveNumber() : Validator("POSITIVE") { - func_ = [](std::string &number_str) { - double number; - if(!detail::lexical_cast(number_str, number)) { - return std::string("Failed parsing number: (") + number_str + ')'; - } - if(number <= 0) { - return std::string("Number less or equal to 0: (") + number_str + ')'; - } - return std::string(); - }; - } -}; -/// Validate the argument is a number and greater than or equal to 0 -class NonNegativeNumber : public Validator { - public: - NonNegativeNumber() : Validator("NONNEGATIVE") { - func_ = [](std::string &number_str) { - double number; - if(!detail::lexical_cast(number_str, number)) { - return std::string("Failed parsing number: (") + number_str + ')'; - } - if(number < 0) { - return std::string("Number less than 0: (") + number_str + ')'; - } - return std::string(); - }; - } -}; - -/// Validate the argument is a number -class Number : public Validator { - public: - Number() : Validator("NUMBER") { - func_ = [](std::string &number_str) { - double number; - if(!detail::lexical_cast(number_str, number)) { - return std::string("Failed parsing as a number (") + number_str + ')'; - } - return std::string(); - }; - } -}; - } // namespace detail // Static is not needed here, because global const implies static. @@ -485,14 +438,23 @@ const detail::NonexistentPathValidator NonexistentPath; /// Check for an IP4 address const detail::IPV4Validator ValidIPV4; -/// Check for a positive number -const detail::PositiveNumber PositiveNumber; - -/// Check for a non-negative number -const detail::NonNegativeNumber NonNegativeNumber; +/// Validate the input as a particular type +template class TypeValidator : public Validator { + public: + explicit TypeValidator(const std::string &validator_name) : Validator(validator_name) { + func_ = [](std::string &input_string) { + auto val = DesiredType(); + if(!detail::lexical_cast(input_string, val)) { + return std::string("Failed parsing ") + input_string + " as a " + detail::type_name(); + } + return std::string(); + }; + } + TypeValidator() : TypeValidator(detail::type_name()) {} +}; /// Check for a number -const detail::Number Number; +const TypeValidator Number("NUMBER"); /// Produce a range (factory). Min and max are inclusive. class Range : public Validator { @@ -501,10 +463,13 @@ class Range : public Validator { /// /// Note that the constructor is templated, but the struct is not, so C++17 is not /// needed to provide nice syntax for Range(a,b). - template Range(T min, T max) { - std::stringstream out; - out << detail::type_name() << " in [" << min << " - " << max << "]"; - description(out.str()); + template + Range(T min, T max, const std::string &validator_name = std::string{}) : Validator(validator_name) { + if(validator_name.empty()) { + std::stringstream out; + out << detail::type_name() << " in [" << min << " - " << max << "]"; + description(out.str()); + } func_ = [min, max](std::string &input) { T val; @@ -518,9 +483,17 @@ class Range : public Validator { } /// Range of one value is 0 to value - template explicit Range(T max) : Range(static_cast(0), max) {} + template + explicit Range(T max, const std::string &validator_name = std::string{}) + : Range(static_cast(0), max, validator_name) {} }; +/// Check for a non negative number +const Range NonNegativeNumber(std::numeric_limits::max(), "NONNEGATIVE"); + +/// Check for a positive valued number (val>0.0), min() her is the smallest positive number +const Range PositiveNumber(std::numeric_limits::min(), std::numeric_limits::max(), "POSITIVE"); + /// Produce a bounded range (factory). Min and max are inclusive. class Bound : public Validator { public: diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 6742a505..0716c542 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -1815,6 +1815,24 @@ TEST_F(TApp, RangeDouble) { run(); } +TEST_F(TApp, typeCheck) { + + /// Note that this must be a double in Range, too + app.add_option("--one")->check(CLI::TypeValidator()); + + args = {"--one=1"}; + EXPECT_NO_THROW(run()); + + args = {"--one=-7"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--one=error"}; + EXPECT_THROW(run(), CLI::ValidationError); + + args = {"--one=4.568"}; + EXPECT_THROW(run(), CLI::ValidationError); +} + // Check to make sure programmatic access to left over is available TEST_F(TApp, AllowExtras) {