Skip to content

Commit

Permalink
Introduce an optional, simplified option parsing interface
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterTh authored and BlackMark29A committed Nov 22, 2024
1 parent b02a3fd commit ff50a2b
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 16 deletions.
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,44 +296,43 @@ For the complete code see [examples/libenvpp_range_example.cpp](examples/libenvp

### Option Variables

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:
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:

```cpp
enum class option {
first_choice,
second_choice,
third_choice,
default_choice,
first,
second,
fallback,
};

int main()
{
auto pre = env::prefix("OPTION");

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

const auto parsed_and_validated_pre = pre.parse_and_validate();

if (parsed_and_validated_pre.ok()) {
const auto opt = parsed_and_validated_pre.get_or(option_id, option::default_choice);
const auto opt = parsed_and_validated_pre.get_or(option_id, option::fallback);
}
}
```
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.
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.
_Note:_ The list of options provided when registering must not be empty, and must not contain duplicates.
_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.
_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.
_Note:_ For the variant where no mapping to strings is provided, a specialized `default_parser` for the `enum class` type must exist.
_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.
#### Option Variables - Code
For the full code, including the parser for the enum class, see [examples/libenvpp_option_example.cpp](examples/libenvpp_option_example.cpp).
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).
### Deprecated Variables
Expand Down
24 changes: 24 additions & 0 deletions examples/libenvpp_option_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,22 @@ struct default_parser<option> {
};
} // namespace env

enum class simple_option {
opt_a,
opt_b,
opt_c,
};

int main()
{
auto pre = env::prefix("OPTION");

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

const auto simple_option_id = pre.register_option<simple_option>(
"SIMPLE", {{"opt_a", simple_option::opt_a}, {"opt_b", simple_option::opt_b}, {"opt_c", simple_option::opt_c}});

const auto parsed_and_validated_pre = pre.parse_and_validate();

if (parsed_and_validated_pre.ok()) {
Expand All @@ -63,6 +72,21 @@ int main()
std::cout << "default_choice" << std::endl;
break;
}

const auto simple_opt = parsed_and_validated_pre.get_or(simple_option_id, simple_option::opt_a);

std::cout << "Simple option: ";
switch (simple_opt) {
case simple_option::opt_a:
std::cout << "opt_a" << std::endl;
break;
case simple_option::opt_b:
std::cout << "opt_b" << std::endl;
break;
case simple_option::opt_c:
std::cout << "opt_c" << std::endl;
break;
}
} else {
std::cout << parsed_and_validated_pre.warning_message();
std::cout << parsed_and_validated_pre.error_message();
Expand Down
58 changes: 53 additions & 5 deletions include/libenvpp/env.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <vector>

#include <fmt/core.h>
#include <fmt/ranges.h>

#include <libenvpp/detail/edit_distance.hpp>
#include <libenvpp/detail/environment.hpp>
Expand Down Expand Up @@ -58,6 +59,20 @@ class variable_data {
friend class ::env::parsed_and_validated_prefix;
};

template <typename T>
std::pair<std::vector<std::string>, std::vector<T>>
extract_options(const std::initializer_list<std::pair<std::string, T>>& options)
{
std::vector<std::string> option_strings;
std::vector<T> option_values;
option_strings.reserve(options.size());
option_values.reserve(options.size());
for (const auto& [str, val] : options) {
option_strings.push_back(str);
option_values.push_back(val);
}
return {option_strings, option_values};
}
} // namespace detail

template <typename T, bool IsRequired>
Expand Down Expand Up @@ -325,10 +340,26 @@ class prefix {
return registration_option_helper<T, false>(name, options);
}

template <typename T>
[[nodiscard]] auto register_option(const std::string_view name,
const std::initializer_list<std::pair<std::string, T>> options)
{
const auto [option_strings, option_values] = detail::extract_options(options);
return registration_option_helper<T, false, true>(name, option_values, option_strings);
}

template <typename T>
[[nodiscard]] auto register_required_option(const std::string_view name, const std::initializer_list<T> options)
{
return registration_option_helper<T, true>(name, options);
return registration_option_helper<T, true, false>(name, options);
}

template <typename T>
[[nodiscard]] auto register_required_option(const std::string_view name,
const std::initializer_list<std::pair<std::string, T>> options)
{
const auto [option_strings, option_values] = detail::extract_options(options);
return registration_option_helper<T, true, true>(name, option_values, option_strings);
}

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

template <typename T, bool IsRequired>
[[nodiscard]] auto registration_option_helper(const std::string_view name, const std::initializer_list<T> options)
template <typename T, bool IsRequired, bool SimpleParsing = false>
[[nodiscard]] auto registration_option_helper(const std::string_view name, const std::vector<T> options,
const std::vector<std::string> option_strings = {})
{
if (options.size() == 0) {
throw empty_option{fmt::format("No options provided for '{}'", get_full_env_var_name(name))};
Expand All @@ -437,8 +469,24 @@ class prefix {
if (options_set.size() != options.size()) {
throw duplicate_option{fmt::format("Duplicate option specified for '{}'", get_full_env_var_name(name))};
}
const auto parser_and_validator = [options = std::move(options_set)](const std::string_view str) {
const auto value = default_parser<T>{}(str);
const auto parser_and_validator = [options = std::move(options),
strings = std::move(option_strings)](const std::string_view str) {
const auto value = [&]() {
if constexpr(SimpleParsing) {
if(strings.size() != options.size()) {
throw option_error{fmt::format("Option strings must be provided for simple option parsing")};
}
const auto it = std::find(strings.begin(), strings.end(), str);
if (it != strings.end()) {
return options.at(std::distance(strings.begin(), it));
} else {
throw option_error{fmt::format("Unrecognized option '{}', should be one of [{}]", str,
fmt::join(strings, ", "))};
}
} else {
return default_parser<T>{}(str);
}
}();
default_validator<T>{}(value);
if (std::all_of(options.begin(), options.end(), [&value](const auto& option) { return option != value; })) {
throw option_error{fmt::format("Unrecognized option '{}'", str)};
Expand Down
24 changes: 23 additions & 1 deletion test/libenvpp_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,18 @@ struct default_parser<testing_option> {
}
};

enum class testing_simple_option {
OPT_A,
OPT_B,
};

class option_var_fixture {
public:
option_var_fixture() : m_var("LIBENVPP_TESTING_OPTION", "SECOND_OPTION") {}
option_var_fixture() : m_var("LIBENVPP_TESTING_OPTION", "SECOND_OPTION"), m_simple("LIBENVPP_TESTING_SIMPLE_OPTION", "OPT_A") {}

private:
detail::set_scoped_environment_variable m_var;
detail::set_scoped_environment_variable m_simple;
};

TEST_CASE_METHOD(int_var_fixture, "Retrieving integer environment variable", "[libenvpp]")
Expand Down Expand Up @@ -110,11 +116,27 @@ TEST_CASE_METHOD(option_var_fixture, "Retrieving option environment variable", "
{
auto pre = env::prefix("LIBENVPP_TESTING");
const auto option_id = pre.register_variable<testing_option>("OPTION");
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}});
auto parsed_and_validated_pre = pre.parse_and_validate();
REQUIRE(parsed_and_validated_pre.ok());
const auto option_val = parsed_and_validated_pre.get(option_id);
REQUIRE(option_val.has_value());
CHECK(*option_val == testing_option::SECOND_OPTION);
const auto simple_option_val = parsed_and_validated_pre.get(simple_option_id);
REQUIRE(simple_option_val.has_value());
CHECK(*simple_option_val == testing_simple_option::OPT_A);
}

TEST_CASE("Parsing failure with 'simple' option handling", "[libenvpp]")
{
const auto _ = detail::set_scoped_environment_variable{"LIBENVPP_TESTING_SIMPLE_OPTION", "INVALID_OPTION"};

auto pre = env::prefix("LIBENVPP_TESTING");
(void)pre.register_option<testing_simple_option>("SIMPLE_OPTION", {{"OPT_A", testing_simple_option::OPT_A}, {"OPT_B", testing_simple_option::OPT_B}});
auto parsed_and_validated_pre = pre.parse_and_validate();
REQUIRE_FALSE(parsed_and_validated_pre.ok());
CHECK_THAT(parsed_and_validated_pre.error_message(),
ContainsSubstring("'LIBENVPP_TESTING_SIMPLE_OPTION': Unrecognized option 'INVALID_OPTION', should be one of [OPT_A, OPT_B]"));
}

struct user_parsable_type {
Expand Down

0 comments on commit ff50a2b

Please sign in to comment.