Skip to content

Commit

Permalink
[FEATURE] Enum support for the argument parser.
Browse files Browse the repository at this point in the history
  • Loading branch information
smehringer committed Oct 11, 2019
1 parent 19ab4cd commit 7055baf
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 16 deletions.
9 changes: 5 additions & 4 deletions include/seqan3/argument_parser/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <seqan3/argument_parser/detail/format_parse.hpp>
#include <seqan3/argument_parser/detail/version_check.hpp>
#include <seqan3/core/char_operations/predicate.hpp>
#include <seqan3/core/detail/debug_stream_named_enumeration.hpp>
#include <seqan3/core/detail/terminal.hpp>
#include <seqan3/core/detail/to_string.hpp>
#include <seqan3/io/stream/concept.hpp>
Expand Down Expand Up @@ -229,8 +230,8 @@ class argument_parser
*/
template <typename option_type, validator validator_type = detail::default_validator<option_type>>
//!\cond
requires (input_stream_over<std::istringstream, option_type> ||
input_stream_over<std::istringstream, typename option_type::value_type>) &&
requires (argument_parser_compatible_option<option_type> ||
argument_parser_compatible_option<typename option_type::value_type>) &&
std::invocable<validator_type, option_type>
//!\endcond
void add_option(option_type & value,
Expand Down Expand Up @@ -291,8 +292,8 @@ class argument_parser
*/
template <typename option_type, validator validator_type = detail::default_validator<option_type>>
//!\cond
requires (input_stream_over<std::istringstream, option_type> ||
input_stream_over<std::istringstream, typename option_type::value_type>) &&
requires (argument_parser_compatible_option<option_type> ||
argument_parser_compatible_option<typename option_type::value_type>) &&
std::invocable<validator_type, option_type>
//!\endcond
void add_positional_option(option_type & value,
Expand Down
167 changes: 167 additions & 0 deletions include/seqan3/argument_parser/auxiliary.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,179 @@
#include <sstream>
#include <vector>

#include <seqan3/core/detail/customisation_point.hpp>
#include <seqan3/core/detail/debug_stream_type.hpp>
#include <seqan3/core/type_traits/basic.hpp>
#include <seqan3/io/stream/concept.hpp>
#include <seqan3/std/concepts>
#include <seqan3/std/type_traits>

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 model 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/named_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 <typename t>
struct argument_parsing
{}; // forward

//!\cond
template <typename t>
struct argument_parsing<t const> : argument_parsing<t>
{};

template <typename t>
struct argument_parsing<t &> : argument_parsing<t>
{};

template <typename t>
struct argument_parsing<t const &> : argument_parsing<t>
{};
//!\endcond

} // seqan3::custom

namespace seqan3::detail::adl_only
{

//!\brief Poison-pill overload to prevent non-ADL forms of unqualified lookup.
template <typename t>
std::unordered_map<std::string_view, t> enumeration_names(t v) = delete;

/*!\brief Functor definition for seqan3::enumeration_names.
*
* This struct is templetised 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 <typename option_t>
struct enumeration_names_fn
{
private:
//!\brief `option_t` with cvref removed and possibly wrapped in std::type_identity.
using s_option_t = std::conditional_t<std::is_nothrow_default_constructible_v<remove_cvref_t<option_t>> &&
seqan3::is_constexpr_default_constructible_v<remove_cvref_t<option_t>>,
remove_cvref_t<option_t>,
std::type_identity<option_t>>;

SEQAN3_CPO_IMPL(1, (deferred_type_t<seqan3::custom::argument_parsing<option_t>, decltype(v)>::enumeration_names))
SEQAN3_CPO_IMPL(0, (enumeration_names(v))) // ADL

public:
//!\brief Operator definition.
template <typename dummy = int> // need to make this a template to enforce deferred initialisation
//!\cond
requires requires
{
{ impl(priority_tag<1>{}, s_option_t{}, dummy{}) } -> std::unordered_map<std::string_view,
remove_cvref_t<option_t>>;
}
//!\endcond
auto operator()() const noexcept
{
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<std::string_view, your_type> 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<your_type>`.
* 2. A free function `enumeration_names(your_type const a)` in the namespace of your type (or as `friend`).
*
* ### 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 <typename option_type>
//!\cond
requires requires { { detail::adl_only::enumeration_names_fn<option_type>{}() }; }
//!\endcond
inline auto enumeration_names = detail::adl_only::enumeration_names_fn<option_type>{}();
//!\}

/*!\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<option_type> must exist and be of type
* `std::unordered_map<std::string, option_type>`.
*/
//!\cond
template <typename option_type>
SEQAN3_CONCEPT named_enumeration = requires
{
{ seqan3::enumeration_names<option_type> } -> std::unordered_map<std::string_view, option_type>;
};
//!\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::enumeration_names must be defined on a value of the type and return
* a `std::unordered_map<std::string, option_type>`.
*/
//!\cond
template <typename option_type>
SEQAN3_CONCEPT argument_parser_compatible_option = input_stream_over<std::istringstream, option_type> ||
named_enumeration<option_type>;
//!\endcond

/*!\brief Used to further specify argument_parser options/flags.
* \ingroup argument_parser
*
Expand Down
37 changes: 29 additions & 8 deletions include/seqan3/argument_parser/detail/format_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include <seqan3/argument_parser/exceptions.hpp>
#include <seqan3/argument_parser/validators.hpp>
#include <seqan3/core/detail/reflection.hpp>
#include <seqan3/range/views/get.hpp>
#include <seqan3/range/views/to.hpp>
#include <seqan3/range/views/view_all.hpp>
#include <seqan3/std/filesystem>

namespace seqan3::detail
Expand Down Expand Up @@ -233,17 +236,26 @@ 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 <typename option_type, typename validator_type>
void add_option(option_type & value,
char const short_id,
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 (named_enumeration<option_type>)
{
auto map = seqan3::enumeration_names<option_type>;
auto enum_values = views::all(map) | views::get<1> | seqan3::views::to<std::vector<option_type>>;
msg.append(" " + value_list_validator<option_type>{std::move(enum_values)}.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) +
Expand All @@ -252,7 +264,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);
});
}

Expand Down Expand Up @@ -283,14 +295,23 @@ 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 <typename option_type, typename validator_type>
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 (named_enumeration<option_type>)
{
auto map = seqan3::enumeration_names<option_type>;
auto enum_values = views::all(map) | views::get<1> | seqan3::views::to<std::vector<option_type>>;
msg.append(" " + value_list_validator<option_type>{std::move(enum_values)}.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 ",
Expand All @@ -300,7 +321,7 @@ class format_help_base : public format_base
((sequence_container<option_type> && !std::same_as<option_type, std::string>)
? detail::to_string(" Default: ", value, ". ")
: std::string{" "}) +
validator.get_help_page_message());
msg);
});
}

Expand Down
39 changes: 35 additions & 4 deletions include/seqan3/argument_parser/detail/format_parse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename option_t>
//!\cond
Expand All @@ -301,8 +299,41 @@ 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 Tries to cast an input string into a value.
* \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 <typename option_t>
//!\cond
requires named_enumeration<option_t>
//!\endcond
void retrieve_value(option_t & value, std::string const & in)
{
std::string tmp;
std::istringstream is{in};
is >> tmp;

auto map = seqan3::enumeration_names<option_t>;
auto it = map.find(tmp);

if (it == map.end())
{
throw type_conversion_failed("Argument " + in + " could not be cast to enum type " +
get_display_name_v<option_t>.str() + ".");
}
else
{
value = it->second;
}
}

//!\cond
Expand Down
1 change: 1 addition & 0 deletions include/seqan3/core/debug_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <seqan3/core/detail/debug_stream_alphabet.hpp>
#include <seqan3/core/detail/debug_stream_optional.hpp>
#include <seqan3/core/detail/debug_stream_range.hpp>
#include <seqan3/core/detail/debug_stream_named_enumeration.hpp>
#include <seqan3/core/detail/debug_stream_tuple.hpp>
#include <seqan3/core/detail/debug_stream_type.hpp>
#include <seqan3/core/detail/debug_stream_variant.hpp>
Expand Down
Loading

0 comments on commit 7055baf

Please sign in to comment.