diff --git a/include/seqan3/argument_parser/argument_parser.hpp b/include/seqan3/argument_parser/argument_parser.hpp index 9ba89dccb6d..89a622f5999 100644 --- a/include/seqan3/argument_parser/argument_parser.hpp +++ b/include/seqan3/argument_parser/argument_parser.hpp @@ -229,8 +229,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,9 +291,9 @@ class argument_parser */ template > //!\cond - requires (input_stream_over || - input_stream_over) && - std::invocable + requires (argument_parser_compatible_option || + argument_parser_compatible_option) && + std::invocable //!\endcond void add_positional_option(option_type & value, std::string const & desc, diff --git a/include/seqan3/argument_parser/auxiliary.hpp b/include/seqan3/argument_parser/auxiliary.hpp index dc5e6fe7ba0..c2360d0bde7 100644 --- a/include/seqan3/argument_parser/auxiliary.hpp +++ b/include/seqan3/argument_parser/auxiliary.hpp @@ -15,12 +15,191 @@ #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 this 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_convertiblity +{}; // forward + +//!\cond +template +struct string_convertiblity : string_convertiblity +{}; + +template +struct string_convertiblity : string_convertiblity +{}; + +template +struct string_convertiblity : string_convertiblity +{}; +//!\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_convertiblity::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 Function objects + * \{ + */ + +/*!\brief Return the rank representation of a (semi-)alphabet object. + * \tparam your_type Type of the argument. + * \param alph The (semi-)alphabet object. + * \returns The rank representation; an integral type. + * \ingroup alphabet + * \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_convertiblity`. + * 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()`. + * + * Functions are only considered for one of the above cases if they are marked `noexcept` (`constexpr` is not required, + * but recommended) and if the returned type models std::Integral. + * + * Every (semi-)alphabet type must provide one of the above. + * + * ### 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_convertiblity 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 alphabet type, + * simply provide one of the three functions specified above. + */ +inline constexpr auto string_conversion_map = detail::adl_only::string_conversion_map_fn{}; +//!\} +} // namespace seqan3 + namespace seqan3 { +/*!\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 foo + * \tparam option_type Type of the enum to be printed. + * \param s The seqan3::debug_stream. + * \param em The enum. + * \relates seqan3::debug_stream_type + * + * \details + * + * This prints out an alignment matrix which can be a score matrix or a trace matrix. + */ +template +//!\cond + requires string_convertible> +//!\endcond +inline debug_stream_type & operator<<(debug_stream_type & s, option_type && em) +{ + for (auto & [key, value] : string_conversion_map(em)) + if (em == value) + return s << key; + return s << ""; +} + /*!\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..6cb6c3667c2 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..4ebb47a1ceb 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 casted value. + * \param[in] in The input argument to be casted. + * + * \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 casted 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/test/unit/argument_parser/detail/format_help_test.cpp b/test/unit/argument_parser/detail/format_help_test.cpp index 0b343e2abac..37b9f08ac84 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::view::filter(!is_space)), expected | std::view::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..3c1ac200ac1 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_convertiblity +{ + 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) {