Skip to content

Commit bb9caf7

Browse files
committed
Introduce an optional, simplified option parsing interface
1 parent b02a3fd commit bb9caf7

File tree

4 files changed

+109
-16
lines changed

4 files changed

+109
-16
lines changed

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -296,44 +296,43 @@ For the complete code see [examples/libenvpp_range_example.cpp](examples/libenvp
296296

297297
### Option Variables
298298

299-
Another frequent use-case is that a value is one of a given set of options. For this an environment variable can be registered with `register_[required]_option`, which takes a list of valid options, against which the value is checked. For example:
299+
Another frequent use-case is that a value is one of a given set of options. For this an environment variable can be registered with `register_[required]_option`, which takes a list of either pairs of strings and corresponding options, or just valid options, against which the value is checked. For example:
300300

301301
```cpp
302302
enum class option {
303-
first_choice,
304-
second_choice,
305-
third_choice,
306-
default_choice,
303+
first,
304+
second,
305+
fallback,
307306
};
308307

309308
int main()
310309
{
311310
auto pre = env::prefix("OPTION");
312311

313312
const auto option_id =
314-
pre.register_option<option>("CHOICE", {option::first_choice, option::second_choice, option::third_choice});
313+
pre.register_option<option>("CHOICE", {{"first", option::first}, {"second", option::second}});
315314

316315
const auto parsed_and_validated_pre = pre.parse_and_validate();
317316

318317
if (parsed_and_validated_pre.ok()) {
319-
const auto opt = parsed_and_validated_pre.get_or(option_id, option::default_choice);
318+
const auto opt = parsed_and_validated_pre.get_or(option_id, option::fallback);
320319
}
321320
}
322321
```
323322
324-
This registers an `enum class` option, where only a subset of all possible values is considered valid, so that `option::default_choice` can be used as the value if the variable is not set.
323+
This registers an `enum class` option, where only a subset of all possible values is considered valid, so that `option::fallback` can be used as the value if the variable is not set.
325324
326325
_Note:_ The list of options provided when registering must not be empty, and must not contain duplicates.
327326
328327
_Note:_ As with range variables, the default value given with `get_or` is not enforced to be within the list of options given when registering the option variable.
329328
330-
_Note:_ Since C++ does not provide any way to automatically parse `enum class` types from string, the example above additionally requires a specialized `default_parser` for the `enum class` type.
329+
_Note:_ For the variant where no mapping to strings is provided, a specialized `default_parser` for the `enum class` type must exist.
331330
332331
_Note:_ Options are mostly intended to be used with `enum class` types, but this is in no way a requirement. Any type can be used as an option, and `enum class` types can also just be normal environment variables.
333332
334333
#### Option Variables - Code
335334
336-
For the full code, including the parser for the enum class, see [examples/libenvpp_option_example.cpp](examples/libenvpp_option_example.cpp).
335+
For the full code, which features both the simple case shown above and a case with a custom parser, see [examples/libenvpp_option_example.cpp](examples/libenvpp_option_example.cpp).
337336
338337
### Deprecated Variables
339338

examples/libenvpp_option_example.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ struct default_parser<option> {
3636
};
3737
} // namespace env
3838

39+
enum class simple_option {
40+
opt_a,
41+
opt_b,
42+
opt_c,
43+
};
44+
3945
int main()
4046
{
4147
auto pre = env::prefix("OPTION");
4248

4349
const auto option_id =
4450
pre.register_option<option>("CHOICE", {option::first_choice, option::second_choice, option::third_choice});
4551

52+
const auto simple_option_id = pre.register_option<simple_option>(
53+
"SIMPLE", {{"opt_a", simple_option::opt_a}, {"opt_b", simple_option::opt_b}, {"opt_c", simple_option::opt_c}});
54+
4655
const auto parsed_and_validated_pre = pre.parse_and_validate();
4756

4857
if (parsed_and_validated_pre.ok()) {
@@ -63,6 +72,21 @@ int main()
6372
std::cout << "default_choice" << std::endl;
6473
break;
6574
}
75+
76+
const auto simple_opt = parsed_and_validated_pre.get_or(simple_option_id, simple_option::opt_a);
77+
78+
std::cout << "Simple option: ";
79+
switch (simple_opt) {
80+
case simple_option::opt_a:
81+
std::cout << "opt_a" << std::endl;
82+
break;
83+
case simple_option::opt_b:
84+
std::cout << "opt_b" << std::endl;
85+
break;
86+
case simple_option::opt_c:
87+
std::cout << "opt_c" << std::endl;
88+
break;
89+
}
6690
} else {
6791
std::cout << parsed_and_validated_pre.warning_message();
6892
std::cout << parsed_and_validated_pre.error_message();

include/libenvpp/env.hpp

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <vector>
1515

1616
#include <fmt/core.h>
17+
#include <fmt/ranges.h>
1718

1819
#include <libenvpp/detail/edit_distance.hpp>
1920
#include <libenvpp/detail/environment.hpp>
@@ -58,6 +59,20 @@ class variable_data {
5859
friend class ::env::parsed_and_validated_prefix;
5960
};
6061

62+
template <typename T>
63+
std::pair<std::vector<std::string>, std::vector<T>>
64+
extract_options(const std::initializer_list<std::pair<std::string, T>>& options)
65+
{
66+
std::vector<std::string> option_strings;
67+
std::vector<T> option_values;
68+
option_strings.reserve(options.size());
69+
option_values.reserve(options.size());
70+
for (const auto& [str, val] : options) {
71+
option_strings.push_back(str);
72+
option_values.push_back(val);
73+
}
74+
return {option_strings, option_values};
75+
}
6176
} // namespace detail
6277

6378
template <typename T, bool IsRequired>
@@ -325,10 +340,26 @@ class prefix {
325340
return registration_option_helper<T, false>(name, options);
326341
}
327342

343+
template <typename T>
344+
[[nodiscard]] auto register_option(const std::string_view name,
345+
const std::initializer_list<std::pair<std::string, T>> options)
346+
{
347+
const auto [option_strings, option_values] = detail::extract_options(options);
348+
return registration_option_helper<T, false, true>(name, option_values, option_strings);
349+
}
350+
328351
template <typename T>
329352
[[nodiscard]] auto register_required_option(const std::string_view name, const std::initializer_list<T> options)
330353
{
331-
return registration_option_helper<T, true>(name, options);
354+
return registration_option_helper<T, true, false>(name, options);
355+
}
356+
357+
template <typename T>
358+
[[nodiscard]] auto register_required_option(const std::string_view name,
359+
const std::initializer_list<std::pair<std::string, T>> options)
360+
{
361+
const auto [option_strings, option_values] = detail::extract_options(options);
362+
return registration_option_helper<T, true, true>(name, option_values, option_strings);
332363
}
333364

334365
void register_deprecated(const std::string_view name, const std::string_view deprecation_message)
@@ -426,8 +457,9 @@ class prefix {
426457
return registration_helper<T, IsRequired>(name, std::move(parser_and_validator));
427458
}
428459

429-
template <typename T, bool IsRequired>
430-
[[nodiscard]] auto registration_option_helper(const std::string_view name, const std::initializer_list<T> options)
460+
template <typename T, bool IsRequired, bool SimpleParsing = false>
461+
[[nodiscard]] auto registration_option_helper(const std::string_view name, const std::vector<T> options,
462+
const std::vector<std::string> option_strings = {})
431463
{
432464
if (options.size() == 0) {
433465
throw empty_option{fmt::format("No options provided for '{}'", get_full_env_var_name(name))};
@@ -437,8 +469,24 @@ class prefix {
437469
if (options_set.size() != options.size()) {
438470
throw duplicate_option{fmt::format("Duplicate option specified for '{}'", get_full_env_var_name(name))};
439471
}
440-
const auto parser_and_validator = [options = std::move(options_set)](const std::string_view str) {
441-
const auto value = default_parser<T>{}(str);
472+
const auto parser_and_validator = [options = std::move(options),
473+
strings = std::move(option_strings)](const std::string_view str) {
474+
const auto value = [&]() {
475+
if constexpr(SimpleParsing) {
476+
if(strings.size() != options.size()) {
477+
throw option_error{fmt::format("Option strings must be provided for simple option parsing")};
478+
}
479+
const auto it = std::find(strings.begin(), strings.end(), str);
480+
if (it != strings.end()) {
481+
return options.at(std::distance(strings.begin(), it));
482+
} else {
483+
throw option_error{fmt::format("Unrecognized option '{}', should be one of [{}]", str,
484+
fmt::join(strings, ", "))};
485+
}
486+
} else {
487+
return default_parser<T>{}(str);
488+
}
489+
}();
442490
default_validator<T>{}(value);
443491
if (std::all_of(options.begin(), options.end(), [&value](const auto& option) { return option != value; })) {
444492
throw option_error{fmt::format("Unrecognized option '{}'", str)};

test/libenvpp_test.cpp

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,18 @@ struct default_parser<testing_option> {
6565
}
6666
};
6767

68+
enum class testing_simple_option {
69+
OPT_A,
70+
OPT_B,
71+
};
72+
6873
class option_var_fixture {
6974
public:
70-
option_var_fixture() : m_var("LIBENVPP_TESTING_OPTION", "SECOND_OPTION") {}
75+
option_var_fixture() : m_var("LIBENVPP_TESTING_OPTION", "SECOND_OPTION"), m_simple("LIBENVPP_TESTING_SIMPLE_OPTION", "OPT_A") {}
7176

7277
private:
7378
detail::set_scoped_environment_variable m_var;
79+
detail::set_scoped_environment_variable m_simple;
7480
};
7581

7682
TEST_CASE_METHOD(int_var_fixture, "Retrieving integer environment variable", "[libenvpp]")
@@ -110,11 +116,27 @@ TEST_CASE_METHOD(option_var_fixture, "Retrieving option environment variable", "
110116
{
111117
auto pre = env::prefix("LIBENVPP_TESTING");
112118
const auto option_id = pre.register_variable<testing_option>("OPTION");
119+
const auto simple_option_id = pre.register_option<testing_simple_option>("SIMPLE_OPTION", {{"OPT_A", testing_simple_option::OPT_A}, {"OPT_B", testing_simple_option::OPT_B}});
113120
auto parsed_and_validated_pre = pre.parse_and_validate();
114121
REQUIRE(parsed_and_validated_pre.ok());
115122
const auto option_val = parsed_and_validated_pre.get(option_id);
116123
REQUIRE(option_val.has_value());
117124
CHECK(*option_val == testing_option::SECOND_OPTION);
125+
const auto simple_option_val = parsed_and_validated_pre.get(simple_option_id);
126+
REQUIRE(simple_option_val.has_value());
127+
CHECK(*simple_option_val == testing_simple_option::OPT_A);
128+
}
129+
130+
TEST_CASE("Parsing failure with 'simple' option handling", "[libenvpp]")
131+
{
132+
const auto _ = detail::set_scoped_environment_variable{"LIBENVPP_TESTING_SIMPLE_OPTION", "INVALID_OPTION"};
133+
134+
auto pre = env::prefix("LIBENVPP_TESTING");
135+
(void)pre.register_option<testing_simple_option>("SIMPLE_OPTION", {{"OPT_A", testing_simple_option::OPT_A}, {"OPT_B", testing_simple_option::OPT_B}});
136+
auto parsed_and_validated_pre = pre.parse_and_validate();
137+
REQUIRE_FALSE(parsed_and_validated_pre.ok());
138+
CHECK_THAT(parsed_and_validated_pre.error_message(),
139+
ContainsSubstring("'LIBENVPP_TESTING_SIMPLE_OPTION': Unrecognized option 'INVALID_OPTION', should be one of [OPT_A, OPT_B]"));
118140
}
119141

120142
struct user_parsable_type {

0 commit comments

Comments
 (0)