Commit 2b6b62c52c0994a424415e8fe7f665cbee2a5fa7
Committed by
Henry Schreiner
1 parent
3917b1ab
Adding smart validators
Showing
4 changed files
with
298 additions
and
58 deletions
include/CLI/Option.hpp
| ... | ... | @@ -272,6 +272,14 @@ class Option : public OptionBase<Option> { |
| 272 | 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 | 283 | /// Adds a validator |
| 276 | 284 | Option *check(std::function<std::string(const std::string &)> validator) { |
| 277 | 285 | validators_.emplace_back(validator); | ... | ... |
include/CLI/Validators.hpp
| ... | ... | @@ -18,74 +18,171 @@ |
| 18 | 18 | namespace CLI { |
| 19 | 19 | |
| 20 | 20 | /// @defgroup validator_group Validators |
| 21 | + | |
| 21 | 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 | 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 | 588 | EXPECT_NE(aee_loc, std::string::npos); |
| 589 | 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 | 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 | 215 | // Yes, this is testing an app_helper :) |
| 159 | 216 | TEST(AppHelper, TempfileCreated) { |
| 160 | 217 | std::string name = "TestFileNotUsed.txt"; | ... | ... |