Commit 2b6b62c52c0994a424415e8fe7f665cbee2a5fa7

Authored by Henry Fredrick Schreiner
Committed by Henry Schreiner
1 parent 3917b1ab

Adding smart validators

include/CLI/Option.hpp
@@ -272,6 +272,14 @@ class Option : public OptionBase<Option> { @@ -272,6 +272,14 @@ class Option : public OptionBase<Option> {
272 return this; 272 return this;
273 } 273 }
274 274
  275 + /// Adds a validator with a built in type name
  276 + Option *check(const Validator &validator) {
  277 + validators_.emplace_back(validator.func);
  278 + if(!validator.tname.empty())
  279 + set_type_name(validator.tname);
  280 + return this;
  281 + }
  282 +
275 /// Adds a validator 283 /// Adds a validator
276 Option *check(std::function<std::string(const std::string &)> validator) { 284 Option *check(std::function<std::string(const std::string &)> validator) {
277 validators_.emplace_back(validator); 285 validators_.emplace_back(validator);
include/CLI/Validators.hpp
@@ -18,74 +18,171 @@ @@ -18,74 +18,171 @@
18 namespace CLI { 18 namespace CLI {
19 19
20 /// @defgroup validator_group Validators 20 /// @defgroup validator_group Validators
  21 +
21 /// @brief Some validators that are provided 22 /// @brief Some validators that are provided
22 /// 23 ///
23 -/// These are simple `void(std::string&)` validators that are useful. They throw  
24 -/// a ValidationError if they fail (or the normally expected error if the cast fails) 24 +/// These are simple `std::string(const std::string&)` validators that are useful. They return
  25 +/// a string if the validation fails. A custom struct is provided, as well, with the same user
  26 +/// semantics, but with the ability to provide a new type name.
25 /// @{ 27 /// @{
26 28
27 -/// Check for an existing file  
28 -inline std::string ExistingFile(const std::string &filename) {  
29 - struct stat buffer;  
30 - bool exist = stat(filename.c_str(), &buffer) == 0;  
31 - bool is_dir = (buffer.st_mode & S_IFDIR) != 0;  
32 - if(!exist) {  
33 - return "File does not exist: " + filename;  
34 - } else if(is_dir) {  
35 - return "File is actually a directory: " + filename; 29 +///
  30 +struct Validator {
  31 + /// This is the type name, if emtpy the type name will not be changed
  32 + std::string tname;
  33 + std::function<std::string(const std::string &filename)> func;
  34 +
  35 + /// This is the required operator for a validator - provided to help
  36 + /// users (CLI11 uses the func directly)
  37 + std::string operator()(const std::string &filename) const { return func(filename); };
  38 +
  39 + /// Combining validators is a new validator
  40 + Validator operator&(const Validator &other) const {
  41 + Validator newval;
  42 + newval.tname = (tname == other.tname ? tname : "");
  43 +
  44 + // Give references (will make a copy in lambda function)
  45 + const std::function<std::string(const std::string &filename)> &f1 = func;
  46 + const std::function<std::string(const std::string &filename)> &f2 = other.func;
  47 +
  48 + newval.func = [f1, f2](const std::string &filename) {
  49 + std::string s1 = f1(filename);
  50 + std::string s2 = f2(filename);
  51 + if(!s1.empty() && !s2.empty())
  52 + return s1 + " & " + s2;
  53 + else
  54 + return s1 + s2;
  55 + };
  56 + return newval;
  57 + }
  58 +
  59 + /// Combining validators is a new validator
  60 + Validator operator|(const Validator &other) const {
  61 + Validator newval;
  62 + newval.tname = (tname == other.tname ? tname : "");
  63 +
  64 + // Give references (will make a copy in lambda function)
  65 + const std::function<std::string(const std::string &filename)> &f1 = func;
  66 + const std::function<std::string(const std::string &filename)> &f2 = other.func;
  67 +
  68 + newval.func = [f1, f2](const std::string &filename) {
  69 + std::string s1 = f1(filename);
  70 + std::string s2 = f2(filename);
  71 + if(s1.empty() || s2.empty())
  72 + return std::string();
  73 + else
  74 + return s1 + " & " + s2;
  75 + };
  76 + return newval;
  77 + }
  78 +};
  79 +
  80 +// The implemntation of the built in validators is using the Validator class;
  81 +// the user is only expected to use the const static versions (since there's no setup).
  82 +// Therefore, this is in detail.
  83 +namespace detail {
  84 +
  85 +/// Check for an existing file (returns error message if check fails)
  86 +struct ExistingFileValidator : public Validator {
  87 + ExistingFileValidator() {
  88 + tname = "FILE";
  89 + func = [](const std::string &filename) {
  90 + struct stat buffer;
  91 + bool exist = stat(filename.c_str(), &buffer) == 0;
  92 + bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
  93 + if(!exist) {
  94 + return "File does not exist: " + filename;
  95 + } else if(is_dir) {
  96 + return "File is actually a directory: " + filename;
  97 + }
  98 + return std::string();
  99 + };
36 } 100 }
37 - return std::string();  
38 -}  
39 -  
40 -/// Check for an existing directory  
41 -inline std::string ExistingDirectory(const std::string &filename) {  
42 - struct stat buffer;  
43 - bool exist = stat(filename.c_str(), &buffer) == 0;  
44 - bool is_dir = (buffer.st_mode & S_IFDIR) != 0;  
45 - if(!exist) {  
46 - return "Directory does not exist: " + filename;  
47 - } else if(!is_dir) {  
48 - return "Directory is actually a file: " + filename; 101 +};
  102 +
  103 +/// Check for an existing directory (returns error message if check fails)
  104 +struct ExistingDirectoryValidator : public Validator {
  105 + ExistingDirectoryValidator() {
  106 + tname = "DIR";
  107 + func = [](const std::string &filename) {
  108 + struct stat buffer;
  109 + bool exist = stat(filename.c_str(), &buffer) == 0;
  110 + bool is_dir = (buffer.st_mode & S_IFDIR) != 0;
  111 + if(!exist) {
  112 + return "Directory does not exist: " + filename;
  113 + } else if(!is_dir) {
  114 + return "Directory is actually a file: " + filename;
  115 + }
  116 + return std::string();
  117 + };
49 } 118 }
50 - return std::string();  
51 -} 119 +};
52 120
53 /// Check for an existing path 121 /// Check for an existing path
54 -inline std::string ExistingPath(const std::string &filename) {  
55 - struct stat buffer;  
56 - bool const exist = stat(filename.c_str(), &buffer) == 0;  
57 - if(!exist) {  
58 - return "Path does not exist: " + filename; 122 +struct ExistingPathValidator : public Validator {
  123 + ExistingPathValidator() {
  124 + tname = "PATH";
  125 + func = [](const std::string &filename) {
  126 + struct stat buffer;
  127 + bool const exist = stat(filename.c_str(), &buffer) == 0;
  128 + if(!exist) {
  129 + return "Path does not exist: " + filename;
  130 + }
  131 + return std::string();
  132 + };
59 } 133 }
60 - return std::string();  
61 -}  
62 -  
63 -/// Check for a non-existing path  
64 -inline std::string NonexistentPath(const std::string &filename) {  
65 - struct stat buffer;  
66 - bool exist = stat(filename.c_str(), &buffer) == 0;  
67 - if(exist) {  
68 - return "Path already exists: " + filename; 134 +};
  135 +
  136 +/// Check for an non-existing path
  137 +struct NonexistentPathValidator : public Validator {
  138 + NonexistentPathValidator() {
  139 + tname = "PATH";
  140 + func = [](const std::string &filename) {
  141 + struct stat buffer;
  142 + bool exist = stat(filename.c_str(), &buffer) == 0;
  143 + if(exist) {
  144 + return "Path already exists: " + filename;
  145 + }
  146 + return std::string();
  147 + };
69 } 148 }
70 - return std::string();  
71 -}  
72 -  
73 -/// Produce a range validator function  
74 -template <typename T> std::function<std::string(const std::string &)> Range(T min, T max) {  
75 - return [min, max](std::string input) {  
76 - T val;  
77 - detail::lexical_cast(input, val);  
78 - if(val < min || val > max)  
79 - return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max);  
80 -  
81 - return std::string();  
82 - };  
83 -}  
84 -  
85 -/// Range of one value is 0 to value  
86 -template <typename T> std::function<std::string(const std::string &)> Range(T max) {  
87 - return Range(static_cast<T>(0), max);  
88 -} 149 +};
  150 +} // namespace detail
  151 +
  152 +// Static is not needed here, because global const implies static.
  153 +
  154 +/// Check for existing file (returns error message if check fails)
  155 +const static detail::ExistingFileValidator ExistingFile;
  156 +
  157 +/// Check for an existing directory (returns error message if check fails)
  158 +const static detail::ExistingDirectoryValidator ExistingDirectory;
  159 +
  160 +/// Check for an existing path
  161 +const static detail::ExistingPathValidator ExistingPath;
  162 +
  163 +/// Check for an non-existing path
  164 +const static detail::NonexistentPathValidator NonexistentPath;
  165 +
  166 +/// Produce a range (factory). Min and max are inclusive.
  167 +struct Range : public Validator {
  168 + template <typename T> Range(T min, T max) {
  169 + std::stringstream out;
  170 + out << detail::type_name<T>() << " in [" << min << " - " << max << "]";
  171 +
  172 + tname = out.str();
  173 + func = [min, max](std::string input) {
  174 + T val;
  175 + detail::lexical_cast(input, val);
  176 + if(val < min || val > max)
  177 + return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max);
  178 +
  179 + return std::string();
  180 + };
  181 + }
  182 +
  183 + /// Range of one value is 0 to value
  184 + template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {}
  185 +};
89 186
90 /// @} 187 /// @}
91 188
tests/HelpTest.cpp
@@ -588,3 +588,81 @@ TEST(THelp, GroupOrder) { @@ -588,3 +588,81 @@ TEST(THelp, GroupOrder) {
588 EXPECT_NE(aee_loc, std::string::npos); 588 EXPECT_NE(aee_loc, std::string::npos);
589 EXPECT_LT(zee_loc, aee_loc); 589 EXPECT_LT(zee_loc, aee_loc);
590 } 590 }
  591 +
  592 +TEST(THelp, ValidatorsText) {
  593 + CLI::App app;
  594 +
  595 + std::string filename;
  596 + int x;
  597 + unsigned int y;
  598 + app.add_option("--f1", filename)->check(CLI::ExistingFile);
  599 + app.add_option("--f3", x)->check(CLI::Range(1, 4));
  600 + app.add_option("--f4", y)->check(CLI::Range(12));
  601 +
  602 + std::string help = app.help();
  603 + EXPECT_THAT(help, HasSubstr("FILE"));
  604 + EXPECT_THAT(help, HasSubstr("INT in [1 - 4]"));
  605 + EXPECT_THAT(help, HasSubstr("INT in [0 - 12]")); // Loses UINT
  606 + EXPECT_THAT(help, Not(HasSubstr("TEXT")));
  607 +}
  608 +
  609 +TEST(THelp, ValidatorsNonPathText) {
  610 + CLI::App app;
  611 +
  612 + std::string filename;
  613 + app.add_option("--f2", filename)->check(CLI::NonexistentPath);
  614 +
  615 + std::string help = app.help();
  616 + EXPECT_THAT(help, HasSubstr("PATH"));
  617 + EXPECT_THAT(help, Not(HasSubstr("TEXT")));
  618 +}
  619 +
  620 +TEST(THelp, ValidatorsDirText) {
  621 + CLI::App app;
  622 +
  623 + std::string filename;
  624 + app.add_option("--f2", filename)->check(CLI::ExistingDirectory);
  625 +
  626 + std::string help = app.help();
  627 + EXPECT_THAT(help, HasSubstr("DIR"));
  628 + EXPECT_THAT(help, Not(HasSubstr("TEXT")));
  629 +}
  630 +
  631 +TEST(THelp, ValidatorsPathText) {
  632 + CLI::App app;
  633 +
  634 + std::string filename;
  635 + app.add_option("--f2", filename)->check(CLI::ExistingPath);
  636 +
  637 + std::string help = app.help();
  638 + EXPECT_THAT(help, HasSubstr("PATH"));
  639 + EXPECT_THAT(help, Not(HasSubstr("TEXT")));
  640 +}
  641 +
  642 +TEST(THelp, CombinedValidatorsText) {
  643 + CLI::App app;
  644 +
  645 + std::string filename;
  646 + app.add_option("--f1", filename)->check(CLI::ExistingFile | CLI::ExistingDirectory);
  647 +
  648 + // This would be nice if it put something other than string, but would it be path or file?
  649 + // Can't programatically tell!
  650 + // (Users can use ExistingPath, by the way)
  651 + std::string help = app.help();
  652 + EXPECT_THAT(help, HasSubstr("TEXT"));
  653 + EXPECT_THAT(help, Not(HasSubstr("PATH")));
  654 + EXPECT_THAT(help, Not(HasSubstr("FILE")));
  655 +}
  656 +
  657 +// Don't do this in real life, please
  658 +TEST(THelp, CombinedValidatorsPathyText) {
  659 + CLI::App app;
  660 +
  661 + std::string filename;
  662 + app.add_option("--f1", filename)->check(CLI::ExistingPath | CLI::NonexistentPath);
  663 +
  664 + // Combining validators with the same type string is OK
  665 + std::string help = app.help();
  666 + EXPECT_THAT(help, Not(HasSubstr("TEXT")));
  667 + EXPECT_THAT(help, HasSubstr("PATH"));
  668 +}
tests/HelpersTest.cpp
@@ -155,6 +155,63 @@ TEST(Validators, PathNotExistsDir) { @@ -155,6 +155,63 @@ TEST(Validators, PathNotExistsDir) {
155 EXPECT_NE(CLI::ExistingPath(mydir), ""); 155 EXPECT_NE(CLI::ExistingPath(mydir), "");
156 } 156 }
157 157
  158 +TEST(Validators, CombinedAndRange) {
  159 + auto crange = CLI::Range(0, 12) & CLI::Range(4, 16);
  160 + EXPECT_TRUE(crange("4").empty());
  161 + EXPECT_TRUE(crange("12").empty());
  162 + EXPECT_TRUE(crange("7").empty());
  163 +
  164 + EXPECT_FALSE(crange("-2").empty());
  165 + EXPECT_FALSE(crange("2").empty());
  166 + EXPECT_FALSE(crange("15").empty());
  167 + EXPECT_FALSE(crange("16").empty());
  168 + EXPECT_FALSE(crange("18").empty());
  169 +}
  170 +
  171 +TEST(Validators, CombinedOrRange) {
  172 + auto crange = CLI::Range(0, 4) | CLI::Range(8, 12);
  173 +
  174 + EXPECT_FALSE(crange("-2").empty());
  175 + EXPECT_TRUE(crange("2").empty());
  176 + EXPECT_FALSE(crange("5").empty());
  177 + EXPECT_TRUE(crange("8").empty());
  178 + EXPECT_TRUE(crange("12").empty());
  179 + EXPECT_FALSE(crange("16").empty());
  180 +}
  181 +
  182 +TEST(Validators, CombinedPaths) {
  183 + std::string myfile{"TestFileNotUsed.txt"};
  184 + EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
  185 + bool ok = static_cast<bool>(std::ofstream(myfile.c_str()).put('a')); // create file
  186 + EXPECT_TRUE(ok);
  187 +
  188 + std::string dir{"../tests"};
  189 + std::string notpath{"nondirectory"};
  190 +
  191 + auto path_or_dir = CLI::ExistingPath | CLI::ExistingDirectory;
  192 + EXPECT_TRUE(path_or_dir(dir).empty());
  193 + EXPECT_TRUE(path_or_dir(myfile).empty());
  194 + EXPECT_FALSE(path_or_dir(notpath).empty());
  195 +
  196 + auto file_or_dir = CLI::ExistingFile | CLI::ExistingDirectory;
  197 + EXPECT_TRUE(file_or_dir(dir).empty());
  198 + EXPECT_TRUE(file_or_dir(myfile).empty());
  199 + EXPECT_FALSE(file_or_dir(notpath).empty());
  200 +
  201 + auto path_and_dir = CLI::ExistingPath & CLI::ExistingDirectory;
  202 + EXPECT_TRUE(path_and_dir(dir).empty());
  203 + EXPECT_FALSE(path_and_dir(myfile).empty());
  204 + EXPECT_FALSE(path_and_dir(notpath).empty());
  205 +
  206 + auto path_and_file = CLI::ExistingFile & CLI::ExistingDirectory;
  207 + EXPECT_FALSE(path_and_file(dir).empty());
  208 + EXPECT_FALSE(path_and_file(myfile).empty());
  209 + EXPECT_FALSE(path_and_file(notpath).empty());
  210 +
  211 + std::remove(myfile.c_str());
  212 + EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
  213 +}
  214 +
158 // Yes, this is testing an app_helper :) 215 // Yes, this is testing an app_helper :)
159 TEST(AppHelper, TempfileCreated) { 216 TEST(AppHelper, TempfileCreated) {
160 std::string name = "TestFileNotUsed.txt"; 217 std::string name = "TestFileNotUsed.txt";