diff --git a/include/seqan3/argument_parser/argument_parser.hpp b/include/seqan3/argument_parser/argument_parser.hpp index 43375d9808c..28c527b8a5f 100644 --- a/include/seqan3/argument_parser/argument_parser.hpp +++ b/include/seqan3/argument_parser/argument_parser.hpp @@ -230,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, @@ -292,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..a61b42fbe7a 100644 --- a/include/seqan3/argument_parser/auxiliary.hpp +++ b/include/seqan3/argument_parser/auxiliary.hpp @@ -15,12 +15,209 @@ #include #include +#include #include +#include #include +#include +#include + +namespace seqan3::custom +{ + +/*!\brief A type that can be specialised to provide customisation point implementations for the seqan3::argument_parser + * such that third party types may be adapted. + * \tparam t The type you wish to specialise for. + * \ingroup argument_parser + * + * \details + * + * ### Named Enumerations + * + * In order to use a third party type within the seqan3::argument_parser::add_option or + * seqan3::argument_parser::add_positional_option call, you can specialise this struct in the following way: + * + * \include test/snippet/argument_parser/custom_argument_parsing_enumeration.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. See the tutorial + * \ref tutorial_argument_parser for an example of customising a type within your own namespace. + */ +template +struct argument_parsing +{}; // forward + +//!\cond +template +struct argument_parsing : argument_parsing +{}; + +template +struct argument_parsing : argument_parsing +{}; + +template +struct argument_parsing : argument_parsing +{}; +//!\endcond + +} // seqan3::custom + +namespace seqan3::detail::adl_only +{ + +//!\brief Poison-pill overload to prevent non-ADL forms of unqualified lookup. +template +std::unordered_map enumeration_names(t) = delete; + +/*!\brief Functor definition for seqan3::enumeration_names. + * + * We need a class template here because we need the original option_t next to s_option_t that may be wrapped + * std::type_identity to be default constructible. We need option_t to be default constructible because the + * respective CPO should be callable without a function parameter. + */ +template +struct enumeration_names_fn +{ + //!\brief `option_t` with cvref removed and possibly wrapped in std::type_identity. + using s_option_t = std::conditional_t> && + seqan3::is_constexpr_default_constructible_v>, + remove_cvref_t, + std::type_identity>; + + SEQAN3_CPO_IMPL(1, (deferred_type_t, decltype(v)>::enumeration_names)) + SEQAN3_CPO_IMPL(0, (enumeration_names(v))) // ADL + + //!\brief Operator definition. + template // need to make this a template to enforce deferred initialisation + //!\cond + requires requires + { + { impl(priority_tag<1>{}, s_option_t{}, dummy{}) }; + std::same_as{}, s_option_t{}, dummy{})), + std::unordered_map>>; + } + //!\endcond + auto operator()() const + { + return impl(priority_tag<1>{}, s_option_t{}); + } +}; + +} // namespace seqan3::detail::adl_only namespace seqan3 { +/*!\name Customisation Points + * \{ + */ + +/*!\brief Return a conversion map from std::string_view 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 two possible implementations (in this order): + * + * 1. A static member `enumeration_names` in `seqan3::custom::argument_parsing` that is of type + * `std::unordered_map>`. + * 2. A free function `enumeration_names(your_type const a)` in the namespace of your type (or as `friend`) which + * returns a `std::unordered_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/custom_enumeration.cpp + * + * **Only if you cannot access the namespace of your type to customize** you may specialize + * the seqan3::custom::argument_parsing struct like this: + * + * \include test/snippet/argument_parser/custom_argument_parsing_enumeration.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 two functions specified above. + */ +template +//!\cond + requires requires { { detail::adl_only::enumeration_names_fn{}() }; } +//!\endcond +inline auto const enumeration_names = detail::adl_only::enumeration_names_fn{}(); +//!\} + +/*!\interface seqan3::named_enumeration <> + * \brief Checks whether the free function seqan3::enumeration_names can be called on the type. + * \ingroup argument_parser + * \tparam option_type The type to check. + * + * ### Requirements + * + * * A instance of seqan3::enumeration_names must exist and be of type + * `std::unordered_map`. + */ +//!\cond +template +SEQAN3_CONCEPT named_enumeration = requires +{ + { seqan3::enumeration_names }; +}; +//!\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 be streamable to std::istringstream or + * model seqan3::named_enumeration. + */ +//!\cond +template +SEQAN3_CONCEPT argument_parser_compatible_option = input_stream_over || + named_enumeration; +//!\endcond + +/*!\name Formatted output overloads + * \{ + */ +/*!\brief A type (e.g. an enum) can be made debug streamable by customizing the seqan3::enumeration_names. + * \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::enumeration_names 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 named_enumeration> +//!\endcond +inline debug_stream_type & operator<<(debug_stream_type & s, option_type && op) +{ + for (auto & [key, value] : enumeration_names) + { + if (op == 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 05835989560..48fea4cf774 100644 --- a/include/seqan3/argument_parser/detail/format_base.hpp +++ b/include/seqan3/argument_parser/detail/format_base.hpp @@ -242,7 +242,9 @@ class format_help_base : public format_base option_spec const & spec, validator_type && validator) { - parser_set_up_calls.push_back([this, &value, short_id, long_id, desc, spec, validator] () + std::string msg = validator.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) + @@ -251,7 +253,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); }); } @@ -289,7 +291,9 @@ class format_help_base : public format_base std::string const & desc, validator_type & validator) { - positional_option_calls.push_back([this, &value, desc, validator] () + std::string msg = validator.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 ", @@ -299,7 +303,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..f1977f6687a 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 @@ -301,8 +299,33 @@ class format_parse : public format_base stream >> value; if (stream.fail() || !stream.eof()) + { throw type_conversion_failed("Argument " + in + " could not be casted to type " + get_type_name_as_string(value) + "."); + } + } + + /*!\brief Sets an option value depending on the keys found in seqan3::enumeration_names. + * \tparam option_t Must model seqan3::named_enumeration. + * \param[out] value Stores the cast value. + * \param[in] in The input argument to be cast. + * + * \throws seqan3::type_conversion_failed + */ + template + void retrieve_value(option_t & value, std::string_view const in) + { + auto map = seqan3::enumeration_names; + + if (auto it = map.find(in); it == map.end()) + { + throw type_conversion_failed("Argument " + std::string{in} + " could not be cast to enum type " + + get_display_name_v.str() + "."); + } + else + { + value = it->second; + } } //!\cond diff --git a/test/snippet/argument_parser/custom_argument_parsing_enumeration.cpp b/test/snippet/argument_parser/custom_argument_parsing_enumeration.cpp new file mode 100644 index 00000000000..d1c076df269 --- /dev/null +++ b/test/snippet/argument_parser/custom_argument_parsing_enumeration.cpp @@ -0,0 +1,47 @@ +#include + +#include +#include + +namespace seqan3::custom +{ +// Specialise the seqan3::custom::argument_parsing data structure to enable parsing of std::errc. +template <> +struct argument_parsing +{ + // Specialise a mapping from an identifying string to the respective value of your type Foo. + static inline std::unordered_map const enumeration_names + { + {"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 argument_parsing struct and + // the static member function enumeration_names + // 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.", seqan3::option_spec::DEFAULT, + seqan3::value_list_validator{(seqan3::enumeration_names | seqan3::views::get<1>)}); + + 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/snippet/argument_parser/custom_enumeration.cpp b/test/snippet/argument_parser/custom_enumeration.cpp new file mode 100644 index 00000000000..317812304d4 --- /dev/null +++ b/test/snippet/argument_parser/custom_enumeration.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +namespace foo +{ + +enum class bar +{ + one, + two, + three +}; + +// Specialise a mapping from an identifying string to the respective value of your type bar. +auto enumeration_names(bar) +{ + return std::unordered_map{{{"one", bar::one}, {"two", bar::two}, {"three", bar::three}}};; +} + +} // namespace foo + +int main(int argc, char const * argv[]) +{ + foo::bar value{}; + + seqan3::argument_parser parser{"my_program", argc, argv}; + + // Because of the enumeration_names function + // you can now add an option that takes a value of type bar: + parser.add_option(value, 'f', "foo", "Give me a foo value.", seqan3::option_spec::DEFAULT, + seqan3::value_list_validator{(seqan3::enumeration_names | seqan3::views::get<1>)}); + + 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/unit/argument_parser/detail/format_help_test.cpp b/test/unit/argument_parser/detail/format_help_test.cpp index 78c3e9342fa..35e877c7fae 100644 --- a/test/unit/argument_parser/detail/format_help_test.cpp +++ b/test/unit/argument_parser/detail/format_help_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include using namespace seqan3; @@ -196,10 +197,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 enumeration_names(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}; @@ -209,6 +223,8 @@ 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.", option_spec::DEFAULT, + value_list_validator{seqan3::enumeration_names | views::get<1>}); parser6.add_option(required_option, 'r', "required-int", "this is another int option.", option_spec::REQUIRED); parser6.add_section("Flags"); parser6.add_subsection("SubFlags"); @@ -237,6 +253,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..b6d07b52876 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 foo +{ +enum class bar +{ + one, + two, + three +}; + +auto enumeration_names(bar) +{ + return std::unordered_map{{{"one", bar::one}, {"two", bar::two}, {"three", bar::three}}}; +} +} // namespace foo + +namespace Other +{ +enum class bar +{ + one, + two +}; +} // namespace Other + +namespace seqan3::custom +{ +template <> +struct argument_parsing +{ + static inline std::unordered_map const enumeration_names + { + {{"one", Other::bar::one}, {"two", Other::bar::two}} + }; +}; +} // namespace seqan3::custom + +TEST(parse_type_test, parse_success_enum_option) +{ + { + foo::bar 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 == foo::bar::two); + } + + { + Other::bar 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::bar::two); + } +} + +TEST(parse_type_test, parse_error_enum_option) +{ + foo::bar 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 b475ebd9fc8..08f8f054191 100644 --- a/test/unit/core/debug_stream_test.cpp +++ b/test/unit/core/debug_stream_test.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -229,3 +230,33 @@ TEST(debug_stream_test, optional) o.flush(); EXPECT_EQ(o.str(), "3"); } + +enum Foo +{ + one, + two, + three +}; + +auto enumeration_names(Foo) +{ + return std::unordered_map{{{"one", Foo::one}, {"two", Foo::two}}}; +} + +TEST(debug_stream_test, named_enumeration) +{ + 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"); +}