From eda8ad331924f4e1a03b884de132e445f7f59967 Mon Sep 17 00:00:00 2001 From: smehringer Date: Fri, 6 Sep 2019 15:37:00 +0200 Subject: [PATCH] [FEATURE] Enum support for the argument parser. --- .../argument_parser/argument_parser.hpp | 9 +- include/seqan3/argument_parser/auxiliary.hpp | 148 ++++++++++++++++++ .../argument_parser/detail/format_base.hpp | 34 +++- .../argument_parser/detail/format_parse.hpp | 31 +++- include/seqan3/core/debug_stream.hpp | 1 + .../debug_stream_string_convertible.hpp | 52 ++++++ .../argument_parser/string_conversion_map.cpp | 41 +++++ .../argument_parser/string_convertible.cpp | 47 ++++++ .../detail/format_help_test.cpp | 16 ++ .../argument_parser/format_parse_test.cpp | 71 +++++++++ test/unit/core/debug_stream_test.cpp | 30 ++++ 11 files changed, 464 insertions(+), 16 deletions(-) create mode 100644 include/seqan3/core/detail/debug_stream_string_convertible.hpp create mode 100644 test/snippet/argument_parser/string_conversion_map.cpp create mode 100644 test/snippet/argument_parser/string_convertible.cpp diff --git a/include/seqan3/argument_parser/argument_parser.hpp b/include/seqan3/argument_parser/argument_parser.hpp index 9ba89dccb6d..2c451fab096 100644 --- a/include/seqan3/argument_parser/argument_parser.hpp +++ b/include/seqan3/argument_parser/argument_parser.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -229,8 +230,8 @@ class argument_parser */ template > //!\cond - requires (input_stream_over || - input_stream_over) && + requires (argument_parser_compatible_option || + argument_parser_compatible_option) && std::invocable //!\endcond void add_option(option_type & value, @@ -291,8 +292,8 @@ class argument_parser */ template > //!\cond - requires (input_stream_over || - input_stream_over) && + requires (argument_parser_compatible_option || + argument_parser_compatible_option) && std::invocable //!\endcond void add_positional_option(option_type & value, diff --git a/include/seqan3/argument_parser/auxiliary.hpp b/include/seqan3/argument_parser/auxiliary.hpp index dc5e6fe7ba0..99a77a334f0 100644 --- a/include/seqan3/argument_parser/auxiliary.hpp +++ b/include/seqan3/argument_parser/auxiliary.hpp @@ -15,12 +15,160 @@ #include #include +#include #include +#include #include +#include + +namespace seqan3::custom +{ + +/*!\brief A type that can be specialised to provide customisation point implementations so that third party types + * model the seqan3::string_convertible concept. + * \tparam t The type you wish to specialise for. + * \ingroup argument_parser + * + * \details + * + * You can specialise a class like this: + * + * \include test/snippet/argument_parser/string_convertible.cpp + * + * Please note that by default the `t const`, `t &` and `t const &` specialisations of this class inherit the + * specialisation for `t` so you usually only need to provide a specialisation for `t`. + * + * \note Only use this if you cannot provide respective functions in your namespace. + */ +template +struct string_convertibility +{}; // forward + +//!\cond +template +struct string_convertibility : string_convertibility +{}; + +template +struct string_convertibility : string_convertibility +{}; + +template +struct string_convertibility : string_convertibility +{}; +//!\endcond + +} // seqan3::custom + +namespace seqan3::detail::adl_only +{ + +//!\brief Poison-pill overload to prevent non-ADL forms of unqualified lookup. +template +std::unordered_map string_conversion_map(t v) = delete; + +//!\brief Functor definition for seqan3::string_conversion_map. +struct string_conversion_map_fn +{ +private: + SEQAN3_CPO_IMPL(2, seqan3::custom::string_convertibility::string_conversion_map(v)) // explicit custom. + SEQAN3_CPO_IMPL(1, string_conversion_map(v)) // ADL + SEQAN3_CPO_IMPL(0, v.string_conversion_map()) // member + +public: + //!\brief Operator definition. + template + //!\cond + requires requires (option_type const a) + { + { impl(priority_tag<2>{}, a) }; + } + //!\endcond + constexpr auto operator()(option_type const a) const noexcept + { + return impl(priority_tag<2>{}, a); + } +}; + +} // namespace seqan3::detail::adl_only namespace seqan3 { +/*!\name Customisation Points + * \{ + */ + +/*!\brief Return a conversion map from std::string to option_type. + * \tparam your_type Type of the value to retrieve the conversion map for. + * \param value The value is not used, just its type. + * \returns A std::unordered_map that maps a string identifier to a value of your_type. + * \ingroup argument_parser + * \details + * + * This is a function object. Invoke it with the parameter(s) specified above. + * + * It acts as a wrapper and looks for three possible implementations (in this order): + * + * 1. A static member function `string_conversion_map(your_type const a)` in + * `seqan3::custom::string_convertibility`. + * 2. A free function `string_conversion_map(your_type const a)` in the namespace of your type (or as `friend`). + * 3. A member function called `string_conversion_map()`. + * + * ### Example + * + * If you are working on a type in your own namespace, you should implement a free function like this: + * + * \include test/snippet/argument_parser/string_conversion_map.cpp + * + * **Only if you cannot access the namespace of your type to customize** you may specialize + * the seqan3::custom::string_convertibility struct like this: + * + * \include test/snippet/argument_parser/string_convertible.cpp + * + * ### Customisation point + * + * This is a customisation point (see \ref about_customisation). To specify the behaviour for your own type, + * simply provide one of the three functions specified above. + */ +inline constexpr auto string_conversion_map = detail::adl_only::string_conversion_map_fn{}; +//!\} + +/*!\interface seqan3::string_convertible <> + * \brief Checks whether the free function seqan3::string_conversion_map can be called on the type. + * \ingroup argument_parser + * \tparam option_type The type to check. + * + * ### Requirements + * + * * The free function seqan3::string_conversion_map must be defined on a value of the type and return + * a `std::unordered_map`. + */ +//!\cond +template +SEQAN3_CONCEPT string_convertible = requires +{ + { seqan3::string_conversion_map(option_type{}) } -> std::unordered_map; +}; +//!\endcond + +/*!\interface seqan3::argument_parser_compatible_option <> + * \brief Checks whether the the type can be used in an add_(positional_)option call on the argument parser. + * \ingroup argument_parser + * \tparam option_type The type to check. + * + * ### Requirements + * + * In order to model this concept, the type must either define the `operator>>` over std::string stream or + * a free function seqan3::string_conversion_map must be defined on a value of the type and return + * a `std::unordered_map`. + */ +//!\cond +template +SEQAN3_CONCEPT argument_parser_compatible_option = input_stream_over || + string_convertible; +//!\endcond + /*!\brief Used to further specify argument_parser options/flags. * \ingroup argument_parser * diff --git a/include/seqan3/argument_parser/detail/format_base.hpp b/include/seqan3/argument_parser/detail/format_base.hpp index 8479078a341..f4fb18eb1cb 100644 --- a/include/seqan3/argument_parser/detail/format_base.hpp +++ b/include/seqan3/argument_parser/detail/format_base.hpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include namespace seqan3::detail @@ -233,7 +235,7 @@ class format_help_base : public format_base * \param[in] long_id The long identifier for the option (e.g. "integer"). * \param[in] desc The description of the option. * \param[in] spec Advanced option specification, see seqan3::option_spec. - * \param[in] validator The validator applied to the value after parsing (callable). + * \param[in] val The validator applied to the value after parsing (callable). */ template void add_option(option_type & value, @@ -241,9 +243,17 @@ class format_help_base : public format_base std::string const & long_id, std::string const & desc, option_spec const & spec, - validator_type && validator) + validator_type && val) { - parser_set_up_calls.push_back([this, &value, short_id, long_id, desc, spec, validator] () + std::string msg = val.get_help_page_message(); + + if constexpr (string_convertible) + { + auto table = seqan3::string_conversion_map(value); + msg = (val | value_list_validator{(view::all(table) | view::get<1>)}).get_help_page_message(); + } + + parser_set_up_calls.push_back([this, &value, short_id, long_id, desc, spec, msg] () { if (!(spec & option_spec::HIDDEN) && (!(spec & option_spec::ADVANCED) || show_advanced_options)) derived_t().print_list_item(prep_id_for_help(short_id, long_id) + @@ -252,7 +262,7 @@ class format_help_base : public format_base ((spec & option_spec::REQUIRED) ? std::string{" "} : detail::to_string(" Default: ", value, ". ")) + - validator.get_help_page_message()); + msg); }); } @@ -283,14 +293,22 @@ class format_help_base : public format_base * * \param[out] value The variable in which to store the given command line argument. * \param[in] desc The description of the positional option. - * \param[in] validator The validator applied to the value after parsing (callable). + * \param[in] val The validator applied to the value after parsing (callable). */ template void add_positional_option(option_type & value, std::string const & desc, - validator_type & validator) + validator_type & val) { - positional_option_calls.push_back([this, &value, desc, validator] () + std::string msg = val.get_help_page_message(); + + if constexpr (string_convertible) + { + auto table = seqan3::string_conversion_map(value); + msg = (val | value_list_validator{(view::all(table) | view::get<1>)}).get_help_page_message(); + } + + positional_option_calls.push_back([this, &value, desc, msg] () { ++positional_option_count; derived_t().print_list_item(detail::to_string("\\fBARGUMENT-", positional_option_count, "\\fP ", @@ -300,7 +318,7 @@ class format_help_base : public format_base ((sequence_container && !std::same_as) ? detail::to_string(" Default: ", value, ". ") : std::string{" "}) + - validator.get_help_page_message()); + msg); }); } diff --git a/include/seqan3/argument_parser/detail/format_parse.hpp b/include/seqan3/argument_parser/detail/format_parse.hpp index 9890f3502b5..2ae4b594fb1 100644 --- a/include/seqan3/argument_parser/detail/format_parse.hpp +++ b/include/seqan3/argument_parser/detail/format_parse.hpp @@ -283,13 +283,11 @@ class format_parse : public format_base } /*!\brief Tries to cast an input string into a value. - * - * \tparam option_t Must satisfy the seqan3::input_stream_over. - * + * \tparam option_t Must model seqan3::input_stream_over. * \param[out] value Stores the casted value. * \param[in] in The input argument to be casted. * - * \throws seqan3::parser_invalid_argument + * \throws seqan3::type_conversion_failed */ template //!\cond @@ -305,6 +303,31 @@ class format_parse : public format_base get_type_name_as_string(value) + "."); } + /*!\brief Tries to cast an input string into a value. + * \tparam option_t Must model seqan3::string_convertible. + * \param[out] value Stores the cast value. + * \param[in] in The input argument to be cast. + * + * \throws seqan3::type_conversion_failed + */ + template + //!\cond + requires string_convertible + //!\endcond + void retrieve_value(option_t & value, std::string const & in) + { + std::string tmp; + std::istringstream is{in}; + is >> tmp; + auto it = seqan3::string_conversion_map(value).find(tmp); + + if (it == seqan3::string_conversion_map(value).end()) + throw type_conversion_failed("Argument " + in + " could not be cast to enum type " + + get_display_name_v.str() + "."); + else + value = it->second; + } + //!\cond void retrieve_value(std::string & value, std::string const & in) { diff --git a/include/seqan3/core/debug_stream.hpp b/include/seqan3/core/debug_stream.hpp index cd4e81a179d..3da910299ac 100644 --- a/include/seqan3/core/debug_stream.hpp +++ b/include/seqan3/core/debug_stream.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/include/seqan3/core/detail/debug_stream_string_convertible.hpp b/include/seqan3/core/detail/debug_stream_string_convertible.hpp new file mode 100644 index 00000000000..ef366f0b702 --- /dev/null +++ b/include/seqan3/core/detail/debug_stream_string_convertible.hpp @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------------------------------- +// Copyright (c) 2006-2019, Knut Reinert & Freie Universität Berlin +// Copyright (c) 2016-2019, Knut Reinert & MPI für molekulare Genetik +// This file may be used, modified and/or redistributed under the terms of the 3-clause BSD-License +// shipped with this file and also available at: https://github.com/seqan/seqan3/blob/master/LICENSE.md +// ----------------------------------------------------------------------------------------------------- + +/*!\file + * \author Svenja Mehringer + * \brief Provides an overload for the seqan3::debug_stream if a seqan3::string_conversion_map of the type exists. + */ + +#pragma once + +#include + +#include +#include + +namespace seqan3 +{ +/*!\name Formatted output overloads + * \{ + */ +/*!\brief A type (e.g. an enum) can be made debug streamable by customizing the seqan3::string_conversion_map. + * \tparam option_type Type of the enum to be printed. + * \param s The seqan3::debug_stream. + * \param op The value to print. + * \relates seqan3::debug_stream_type + * + * \details + * + * This searches the seqan3::string_conversion_map of the respective type for the value \p op and prints the + * respective string if found or '\' if the value cannot be found in the map. + */ +template +//!\cond + requires string_convertible> +//!\endcond +inline debug_stream_type & operator<<(debug_stream_type & s, option_type && op) +{ + for (auto & [key, value] : string_conversion_map(op)) + { + if (op == value) + return s << key; + } + + return s << ""; +} +//!\} + +} // namespace seqan3 diff --git a/test/snippet/argument_parser/string_conversion_map.cpp b/test/snippet/argument_parser/string_conversion_map.cpp new file mode 100644 index 00000000000..32eb087a0ec --- /dev/null +++ b/test/snippet/argument_parser/string_conversion_map.cpp @@ -0,0 +1,41 @@ +#include + +namespace your_namespace +{ + +enum class Foo +{ + one, + two, + three +}; + +// Specialise a mapping from an identifying string to the respective value of your type Foo. +auto string_conversion_map(Foo) +{ + return std::unordered_map{{{"one", Foo::one}, {"two", Foo::two}, {"three", Foo::three}}}; +} + +} // namespace your_namespace + + +int main(int argc, char const * argv[]) +{ + your_namespace::Foo value{}; + + seqan3::argument_parser parser{"my_program", argc, argv}; + + // Because of the string_conversion_map function + // you can now add an option that takes a value of type Foo: + parser.add_option(value, 'f', "foo", "Give me a foo value."); + + try + { + parser.parse(); + } + catch (seqan3::parser_invalid_argument const & ext) // the user did something wrong + { + std::cerr << "[PARSER ERROR] " << ext.what() << "\n"; // customize your error message + return -1; + } +} diff --git a/test/snippet/argument_parser/string_convertible.cpp b/test/snippet/argument_parser/string_convertible.cpp new file mode 100644 index 00000000000..e02cc94e173 --- /dev/null +++ b/test/snippet/argument_parser/string_convertible.cpp @@ -0,0 +1,47 @@ +#include + +#include + +namespace seqan3::custom +{ +// Specialise the seqan3::custom::string_convertibility data structure to enable parsing of std::errc. +template <> +struct string_convertibility +{ + // Specialise a mapping from an identifying string to the respective value of your type Foo. + static auto string_conversion_map(std::errc) + { + return std::unordered_map + {{ + {"no_error", std::errc{}}, + {"timed_out", std::errc::timed_out}, + {"invalid_argument", std::errc::invalid_argument}, + {"io_error", std::errc::io_error} + }}; + } +}; +} // namespace seqan3::custom + +int main(int argc, char const * argv[]) +{ + std::errc value{}; + + seqan3::argument_parser parser{"my_program", argc, argv}; + + // Because of the string_convertibility struct and + // the static member function string_conversion_map + // you can now add an option that takes a value of type std::errc: + parser.add_option(value, 'e', "errc", "Give me a std::errc value."); + + try + { + parser.parse(); + } + catch (seqan3::parser_invalid_argument const & ext) // the user did something wrong + { + std::cerr << "[PARSER ERROR] " << ext.what() << "\n"; // customize your error message + return -1; + } + + return 0; +} diff --git a/test/unit/argument_parser/detail/format_help_test.cpp b/test/unit/argument_parser/detail/format_help_test.cpp index 90b5d8cb78d..e834fda4b75 100644 --- a/test/unit/argument_parser/detail/format_help_test.cpp +++ b/test/unit/argument_parser/detail/format_help_test.cpp @@ -195,10 +195,23 @@ TEST(help_page_printing, do_not_print_hidden_options) EXPECT_TRUE(ranges::equal((std_cout | std::views::filter(!is_space)), expected | std::views::filter(!is_space))); } +enum class Foo +{ + one, + two, + three +}; + +auto string_conversion_map(Foo) +{ + return std::unordered_map{{{"one", Foo::one}, {"two", Foo::two}, {"three", Foo::three}}}; +} + TEST(help_page_printing, full_information) { int8_t required_option{}; int8_t non_list_optional{1}; + Foo enum_option_value{}; // Add synopsis, description, short description, positional option, option, flag, and example. argument_parser parser6{"test_parser", 2, argv1}; @@ -208,6 +221,7 @@ TEST(help_page_printing, full_information) parser6.info.description.push_back("description2"); parser6.info.short_description = "so short"; parser6.add_option(option_value, 'i', "int", "this is a int option."); + parser6.add_option(enum_option_value, 'e', "enum", "this is an enum option."); parser6.add_option(required_option, 'r', "required-int", "this is another int option.", option_spec::REQUIRED); parser6.add_section("Flags"); parser6.add_subsection("SubFlags"); @@ -236,6 +250,8 @@ TEST(help_page_printing, full_information) basic_options_str + "-i, --int (signed 32 bit integer)\n" "this is a int option. Default: 5.\n" + "-e, --enum (Foo)\n" + "this is an enum option. Default: one. Value must be one of [three,two,one].\n" "-r, --required-int (signed 8 bit integer)\n" "this is another int option.\n" "FLAGS\n" diff --git a/test/unit/argument_parser/format_parse_test.cpp b/test/unit/argument_parser/format_parse_test.cpp index 904f3e085db..7bf4ad0c58c 100644 --- a/test/unit/argument_parser/format_parse_test.cpp +++ b/test/unit/argument_parser/format_parse_test.cpp @@ -581,6 +581,77 @@ TEST(parse_type_test, parse_error_double_option) EXPECT_THROW(parser2.parse(), parser_invalid_argument); } +namespace Bar +{ +enum class Foo +{ + one, + two, + three +}; + +auto string_conversion_map(Foo) +{ + return std::unordered_map{{{"one", Foo::one}, {"two", Foo::two}, {"three", Foo::three}}}; +} +} // namespace Bar + +namespace Other +{ +enum class Foo +{ + one, + two +}; +} // namespace Other + +namespace seqan3::custom +{ +template <> +struct string_convertibility +{ + static auto string_conversion_map(Other::Foo) + { + return std::unordered_map{{{"one", Other::Foo::one}, {"two", Other::Foo::two}}}; + } +}; +} // namespace seqan3::custom + +TEST(parse_type_test, parse_success_enum_option) +{ + { + Bar::Foo option_value{}; + + const char * argv[] = {"./argument_parser_test", "-e", "two"}; + argument_parser parser{"test_parser", 3, argv, false}; + parser.add_option(option_value, 'e', "enum-option", "this is an enum option."); + + EXPECT_NO_THROW(parser.parse()); + EXPECT_TRUE(option_value == Bar::Foo::two); + } + + { + Other::Foo option_value{}; + + const char * argv[] = {"./argument_parser_test", "-e", "two"}; + argument_parser parser{"test_parser", 3, argv, false}; + parser.add_option(option_value, 'e', "enum-option", "this is an enum option."); + + EXPECT_NO_THROW(parser.parse()); + EXPECT_TRUE(option_value == Other::Foo::two); + } +} + +TEST(parse_type_test, parse_error_enum_option) +{ + Bar::Foo option_value{}; + + const char * argv[] = {"./argument_parser_test", "-e", "four"}; + argument_parser parser{"test_parser", 3, argv, false}; + parser.add_option(option_value, 'e', "enum-option", "this is an enum option."); + + EXPECT_THROW(parser.parse(), parser_invalid_argument); +} TEST(parse_test, too_many_arguments_error) { diff --git a/test/unit/core/debug_stream_test.cpp b/test/unit/core/debug_stream_test.cpp index d2d8fc1bfd2..4329622edbc 100644 --- a/test/unit/core/debug_stream_test.cpp +++ b/test/unit/core/debug_stream_test.cpp @@ -214,3 +214,33 @@ TEST(debug_stream_test, optional) o.flush(); EXPECT_EQ(o.str(), "3"); } + +enum Foo +{ + one, + two, + three +}; + +auto string_conversion_map(Foo) +{ + return std::unordered_map{{{"one", Foo::one}, {"two", Foo::two}}}; +} + +TEST(debug_stream_test, string_convertible) +{ + std::ostringstream o; + debug_stream_type my_stream{o}; + + Foo fo{}; + + my_stream << fo; + o.flush(); + EXPECT_EQ(o.str(), "one"); + + fo = Foo::three; // unknown to the conversion map + + my_stream << fo; + o.flush(); + EXPECT_EQ(o.str(), "one"); +}