Commit 31be35b241c5abf02de4868003325babca3eb65f

Authored by Philip Top
Committed by GitHub
1 parent 28b35af5

feat: add a validator that checks for specific types conversion (#526)

README.md
... ... @@ -409,6 +409,7 @@ CLI11 has several Validators built-in that perform some common checks
409 409 - `CLI::NonNegativeNumber`: Requires the number be greater or equal to 0
410 410 - `CLI::Number`: Requires the input be a number.
411 411 - `CLI::ValidIPV4`: Requires that the option be a valid IPv4 string e.g. `'255.255.255.255'`, `'10.1.1.7'`.
  412 +- `CLI::TypeValidator<TYPE>`:๐Ÿšง Requires that the option be convertible to the specified type e.g. `CLI::TypeValidator<unsigned int>()` would require that the input be convertible to an `unsigned int` regardless of the end conversion.
412 413  
413 414 These Validators can be used by simply passing the name into the `check` or `transform` methods on an option
414 415  
... ... @@ -715,7 +716,7 @@ app.set_config(option_name=&quot;&quot;,
715 716  
716 717 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 ๐Ÿ†•:
717 718  
718   -```ini
  719 +```toml
719 720 # Comments are supported, using a #
720 721 # The default section is [default], case insensitive
721 722  
... ...
include/CLI/Validators.hpp
... ... @@ -419,53 +419,6 @@ class IPV4Validator : public Validator {
419 419 }
420 420 };
421 421  
422   -/// Validate the argument is a number and greater than 0
423   -class PositiveNumber : public Validator {
424   - public:
425   - PositiveNumber() : Validator("POSITIVE") {
426   - func_ = [](std::string &number_str) {
427   - double number;
428   - if(!detail::lexical_cast(number_str, number)) {
429   - return std::string("Failed parsing number: (") + number_str + ')';
430   - }
431   - if(number <= 0) {
432   - return std::string("Number less or equal to 0: (") + number_str + ')';
433   - }
434   - return std::string();
435   - };
436   - }
437   -};
438   -/// Validate the argument is a number and greater than or equal to 0
439   -class NonNegativeNumber : public Validator {
440   - public:
441   - NonNegativeNumber() : Validator("NONNEGATIVE") {
442   - func_ = [](std::string &number_str) {
443   - double number;
444   - if(!detail::lexical_cast(number_str, number)) {
445   - return std::string("Failed parsing number: (") + number_str + ')';
446   - }
447   - if(number < 0) {
448   - return std::string("Number less than 0: (") + number_str + ')';
449   - }
450   - return std::string();
451   - };
452   - }
453   -};
454   -
455   -/// Validate the argument is a number
456   -class Number : public Validator {
457   - public:
458   - Number() : Validator("NUMBER") {
459   - func_ = [](std::string &number_str) {
460   - double number;
461   - if(!detail::lexical_cast(number_str, number)) {
462   - return std::string("Failed parsing as a number (") + number_str + ')';
463   - }
464   - return std::string();
465   - };
466   - }
467   -};
468   -
469 422 } // namespace detail
470 423  
471 424 // Static is not needed here, because global const implies static.
... ... @@ -485,14 +438,23 @@ const detail::NonexistentPathValidator NonexistentPath;
485 438 /// Check for an IP4 address
486 439 const detail::IPV4Validator ValidIPV4;
487 440  
488   -/// Check for a positive number
489   -const detail::PositiveNumber PositiveNumber;
490   -
491   -/// Check for a non-negative number
492   -const detail::NonNegativeNumber NonNegativeNumber;
  441 +/// Validate the input as a particular type
  442 +template <typename DesiredType> class TypeValidator : public Validator {
  443 + public:
  444 + explicit TypeValidator(const std::string &validator_name) : Validator(validator_name) {
  445 + func_ = [](std::string &input_string) {
  446 + auto val = DesiredType();
  447 + if(!detail::lexical_cast(input_string, val)) {
  448 + return std::string("Failed parsing ") + input_string + " as a " + detail::type_name<DesiredType>();
  449 + }
  450 + return std::string();
  451 + };
  452 + }
  453 + TypeValidator() : TypeValidator(detail::type_name<DesiredType>()) {}
  454 +};
493 455  
494 456 /// Check for a number
495   -const detail::Number Number;
  457 +const TypeValidator<double> Number("NUMBER");
496 458  
497 459 /// Produce a range (factory). Min and max are inclusive.
498 460 class Range : public Validator {
... ... @@ -501,10 +463,13 @@ class Range : public Validator {
501 463 ///
502 464 /// Note that the constructor is templated, but the struct is not, so C++17 is not
503 465 /// needed to provide nice syntax for Range(a,b).
504   - template <typename T> Range(T min, T max) {
505   - std::stringstream out;
506   - out << detail::type_name<T>() << " in [" << min << " - " << max << "]";
507   - description(out.str());
  466 + template <typename T>
  467 + Range(T min, T max, const std::string &validator_name = std::string{}) : Validator(validator_name) {
  468 + if(validator_name.empty()) {
  469 + std::stringstream out;
  470 + out << detail::type_name<T>() << " in [" << min << " - " << max << "]";
  471 + description(out.str());
  472 + }
508 473  
509 474 func_ = [min, max](std::string &input) {
510 475 T val;
... ... @@ -518,9 +483,17 @@ class Range : public Validator {
518 483 }
519 484  
520 485 /// Range of one value is 0 to value
521   - template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {}
  486 + template <typename T>
  487 + explicit Range(T max, const std::string &validator_name = std::string{})
  488 + : Range(static_cast<T>(0), max, validator_name) {}
522 489 };
523 490  
  491 +/// Check for a non negative number
  492 +const Range NonNegativeNumber(std::numeric_limits<double>::max(), "NONNEGATIVE");
  493 +
  494 +/// Check for a positive valued number (val>0.0), min() her is the smallest positive number
  495 +const Range PositiveNumber(std::numeric_limits<double>::min(), std::numeric_limits<double>::max(), "POSITIVE");
  496 +
524 497 /// Produce a bounded range (factory). Min and max are inclusive.
525 498 class Bound : public Validator {
526 499 public:
... ...
tests/AppTest.cpp
... ... @@ -1815,6 +1815,24 @@ TEST_F(TApp, RangeDouble) {
1815 1815 run();
1816 1816 }
1817 1817  
  1818 +TEST_F(TApp, typeCheck) {
  1819 +
  1820 + /// Note that this must be a double in Range, too
  1821 + app.add_option("--one")->check(CLI::TypeValidator<unsigned int>());
  1822 +
  1823 + args = {"--one=1"};
  1824 + EXPECT_NO_THROW(run());
  1825 +
  1826 + args = {"--one=-7"};
  1827 + EXPECT_THROW(run(), CLI::ValidationError);
  1828 +
  1829 + args = {"--one=error"};
  1830 + EXPECT_THROW(run(), CLI::ValidationError);
  1831 +
  1832 + args = {"--one=4.568"};
  1833 + EXPECT_THROW(run(), CLI::ValidationError);
  1834 +}
  1835 +
1818 1836 // Check to make sure programmatic access to left over is available
1819 1837 TEST_F(TApp, AllowExtras) {
1820 1838  
... ...