Improve experimental value conversion for arg parser

* Fix issues and handle conversion errors
* Add tests
This commit is contained in:
Martchus 2018-09-22 17:04:14 +02:00
parent 3a14d39a14
commit 3d3378c878
3 changed files with 143 additions and 34 deletions

View File

@ -1725,4 +1725,26 @@ void NoColorArgument::apply()
}
}
/*!
* \brief Throws a Failure for the current instance and the specified \a argumentPath.
*/
void ValueConversion::Helper::ArgumentValueConversionError::throwFailure(const std::vector<Argument *> &argumentPath) const
{
throw Failure(argumentPath.empty()
? argsToString("Conversion of top-level value \"", valueToConvert, "\" to type \"", targetTypeName, "\" failed: ", errorMessage)
: argsToString("Conversion of value \"", valueToConvert, "\" (for argument --", argumentPath.back()->name(), ") to type \"",
targetTypeName, "\" failed: ", errorMessage));
}
/*!
* \brief Throws a Failure for insufficient number of values.
*/
void ArgumentOccurrence::throwNumberOfValuesNotSufficient(unsigned long valuesToConvert) const
{
throw Failure(path.empty()
? argsToString("Expected ", valuesToConvert, " top-level values to be present but only ", values.size(), " have been specified.")
: argsToString("Expected ", valuesToConvert, " values for argument --", path.back()->name(), " to be present but only ", values.size(),
" have been specified."));
}
} // namespace ApplicationUtilities

View File

@ -130,20 +130,54 @@ Argument CPP_UTILITIES_EXPORT *firstPresentUncombinableArg(const ArgumentVector
/*!
* \brief Contains functions to convert raw argument values to certain types.
*
* Extend this namespace by additional convert() functions to allow use of Argument::valuesAs() with your custom types.
*
* \remarks Still experimental. Might be removed/adjusted in next minor release.
*/
namespace ValueConversion {
template <typename TargetType> TargetType convert(const char *value);
template <typename TargetType, Traits::EnableIf<std::is_same<TargetType, std::string>>> TargetType convert(const char *value)
template <typename TargetType, Traits::EnableIf<std::is_same<TargetType, std::string>> * = nullptr> TargetType convert(const char *value)
{
return std::string(value);
}
template <typename TargetType, Traits::EnableIf<std::is_arithmetic<TargetType>>> TargetType convert(const char *value)
template <typename TargetType, Traits::EnableIf<std::is_arithmetic<TargetType>> * = nullptr> TargetType convert(const char *value)
{
return ConversionUtilities::stringToNumber<TargetType>(value);
}
/// \cond
namespace Helper {
struct ArgumentValueConversionError {
const char *const errorMessage;
const char *const valueToConvert;
const char *const targetTypeName;
[[noreturn]] void throwFailure(const std::vector<Argument *> &argumentPath) const;
};
template <std::size_t N, typename FirstTargetType, typename... RemainingTargetTypes> struct ArgumentValueConverter {
static std::tuple<FirstTargetType, RemainingTargetTypes...> convertValues(std::vector<const char *>::const_iterator firstValue)
{
return std::tuple_cat(ArgumentValueConverter<1, FirstTargetType>::convertValues(firstValue),
ArgumentValueConverter<N - 1, RemainingTargetTypes...>::convertValues(firstValue + 1));
}
};
template <typename FirstTargetType, typename... RemainingTargetTypes> struct ArgumentValueConverter<1, FirstTargetType, RemainingTargetTypes...> {
static std::tuple<FirstTargetType> convertValues(std::vector<const char *>::const_iterator firstValue)
{
// FIXME: maybe use std::expected here when available
try {
return std::make_tuple<FirstTargetType>(ValueConversion::convert<FirstTargetType>(*firstValue));
} catch (const ConversionUtilities::ConversionException &exception) {
throw ArgumentValueConversionError{ exception.what(), *firstValue, typeid(FirstTargetType).name() };
}
}
};
} // namespace Helper
/// \endcond
} // namespace ValueConversion
/*!
@ -169,42 +203,29 @@ struct CPP_UTILITIES_EXPORT ArgumentOccurrence {
*/
std::vector<Argument *> path;
template <typename TargetType> std::tuple<TargetType> convertValues() const;
template <typename FirstTargetType, typename... RemainingTargetTypes> std::tuple<FirstTargetType, RemainingTargetTypes...> convertValues() const;
template <typename... RemainingTargetTypes> std::tuple<RemainingTargetTypes...> convertValues() const;
private:
template <typename FirstTargetType, typename... RemainingTargetTypes>
std::tuple<FirstTargetType, RemainingTargetTypes...> convertValues(std::vector<const char *>::const_iterator firstValue) const;
[[noreturn]] void throwNumberOfValuesNotSufficient(unsigned long valuesToConvert) const;
};
/*!
* \brief Converts the present value to the specified target type. There must be at least one value present.
* \remarks Still experimental. Might be removed/adjusted in next minor release.
*/
template <typename TargetType> std::tuple<TargetType> ArgumentOccurrence::convertValues() const
{
return std::make_tuple<TargetType>(ValueConversion::convert<TargetType>(values.front()));
}
/*!
* \brief Converts the present values to the specified target types. There must be as many values present as types are specified.
* \throws Throws ArgumentUtilities::Failure when the number of present values is not sufficient or a conversion error occurs.
* \remarks Still experimental. Might be removed/adjusted in next minor release.
*/
template <typename FirstTargetType, typename... RemainingTargetTypes>
std::tuple<FirstTargetType, RemainingTargetTypes...> ArgumentOccurrence::convertValues() const
template <typename... RemainingTargetTypes> std::tuple<RemainingTargetTypes...> ArgumentOccurrence::convertValues() const
{
return std::tuple_cat(std::make_tuple<FirstTargetType>(ValueConversion::convert<FirstTargetType>(values.front())),
convertValues<RemainingTargetTypes...>(values.cbegin() + 1));
constexpr auto valuesToConvert = sizeof...(RemainingTargetTypes);
if (values.size() < valuesToConvert) {
throwNumberOfValuesNotSufficient(valuesToConvert);
}
try {
return ValueConversion::Helper::ArgumentValueConverter<valuesToConvert, RemainingTargetTypes...>::convertValues(values.cbegin());
} catch (const ValueConversion::Helper::ArgumentValueConversionError &error) {
error.throwFailure(path);
}
/// \cond
template <typename FirstTargetType, typename... RemainingTargetTypes>
std::tuple<FirstTargetType, RemainingTargetTypes...> ArgumentOccurrence::convertValues(std::vector<const char *>::const_iterator firstValue) const
{
return std::tuple_cat(std::make_tuple<FirstTargetType>(ValueConversion::convert<FirstTargetType>(*firstValue)),
convertValues<RemainingTargetTypes...>(firstValue + 1));
}
/// \endcond
/*!
* \brief Constructs an argument occurrence for the specified \a index.
@ -292,8 +313,8 @@ public:
// declare getter/read-only properties for parsing results: those properties will be populated when parsing
const std::vector<const char *> &values(std::size_t occurrence = 0) const;
template <typename... TargetType> std::tuple<TargetType...> convertValues(std::size_t occurrence = 0) const;
template <typename... TargetType> std::vector<std::tuple<TargetType...>> convertAllValues() const;
template <typename... TargetType> std::tuple<TargetType...> valuesAs(std::size_t occurrence = 0) const;
template <typename... TargetType> std::vector<std::tuple<TargetType...>> allValuesAs() const;
const char *firstValue() const;
bool allRequiredValuesPresent(std::size_t occurrence = 0) const;
@ -345,18 +366,20 @@ private:
/*!
* \brief Converts the present values for the specified \a occurrence to the specified target types. There must be as many values present as types are specified.
* \throws Throws ArgumentUtilities::Failure when the number of present values is not sufficient or a conversion error occurs.
* \remarks Still experimental. Might be removed/adjusted in next minor release.
*/
template <typename... TargetType> std::tuple<TargetType...> Argument::convertValues(std::size_t occurrence) const
template <typename... TargetType> std::tuple<TargetType...> Argument::valuesAs(std::size_t occurrence) const
{
return m_occurrences[occurrence].convertValues<TargetType...>();
}
/*!
* \brief Converts the present values for all occurrence to the specified target types. For each occurrence, there must be as many values present as types are specified.
* \throws Throws ArgumentUtilities::Failure when the number of present values is not sufficient or a conversion error occurs.
* \remarks Still experimental. Might be removed/adjusted in next minor release.
*/
template <typename... TargetType> std::vector<std::tuple<TargetType...>> Argument::convertAllValues() const
template <typename... TargetType> std::vector<std::tuple<TargetType...>> Argument::allValuesAs() const
{
std::vector<std::tuple<TargetType...>> res;
res.reserve(m_occurrences.size());

View File

@ -2,6 +2,7 @@
#include "./testutils.h"
#include "../conversion/stringbuilder.h"
#include "../conversion/stringconversion.h"
#include "../application/argumentparser.h"
#include "../application/argumentparserprivate.h"
@ -40,6 +41,7 @@ class ArgumentParserTests : public TestFixture {
CPPUNIT_TEST(testHelp);
CPPUNIT_TEST(testSetMainArguments);
CPPUNIT_TEST(testNoColorArgument);
CPPUNIT_TEST(testValueConversion);
CPPUNIT_TEST_SUITE_END();
public:
@ -53,6 +55,7 @@ public:
void testHelp();
void testSetMainArguments();
void testNoColorArgument();
void testValueConversion();
private:
void callback();
@ -856,3 +859,64 @@ void ArgumentParserTests::testNoColorArgument()
CPPUNIT_ASSERT(EscapeCodes::enabled);
}
}
template <typename ValueTuple> void checkConvertedValues(const std::string &message, const ValueTuple &values)
{
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, "foo"s, get<0>(values));
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, 42u, get<1>(values));
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, 7.5, get<2>(values));
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, -42, get<3>(values));
}
/*!
* \brief Tests value conversion provided by Argument and ArgumentOccurrence.
*/
void ArgumentParserTests::testValueConversion()
{
// convert values directly from ArgumentOccurrence
ArgumentOccurrence occurrence(0);
occurrence.values = { "foo", "42", "7.5", "-42" };
checkConvertedValues("values from ArgumentOccurrence::convertValues", occurrence.convertValues<string, unsigned int, double, int>());
static_assert(std::is_same<std::tuple<>, decltype(occurrence.convertValues<>())>::value, "specifying no types yields empty tuple");
// convert values via Argument's API
Argument arg("test", '\0');
arg.m_occurrences = { occurrence, occurrence };
checkConvertedValues("values from Argument::convertValues", arg.valuesAs<string, unsigned int, double, int>());
checkConvertedValues("values from Argument::convertValues(1)", arg.valuesAs<string, unsigned int, double, int>(1));
const auto allValues = arg.allValuesAs<string, unsigned int, double, int>();
CPPUNIT_ASSERT_EQUAL(2_st, allValues.size());
for (const auto &values : allValues) {
checkConvertedValues("values from Argument::convertAllValues", values);
}
static_assert(std::is_same<std::tuple<>, decltype(arg.valuesAs<>())>::value, "specifying no types yields empty tuple");
// error handling
try {
occurrence.convertValues<string, unsigned int, double, int, int>();
CPPUNIT_FAIL("Expected exception");
} catch (const Failure &failure) {
CPPUNIT_ASSERT_EQUAL("Expected 5 top-level values to be present but only 4 have been specified."s, string(failure.what()));
}
try {
occurrence.convertValues<int>();
CPPUNIT_FAIL("Expected exception");
} catch (const Failure &failure) {
CPPUNIT_ASSERT_EQUAL(
"Conversion of top-level value \"foo\" to type \"i\" failed: The character \"f\" is no valid digit."s, string(failure.what()));
}
occurrence.path = { &arg };
try {
occurrence.convertValues<string, unsigned int, double, int, int>();
CPPUNIT_FAIL("Expected exception");
} catch (const Failure &failure) {
CPPUNIT_ASSERT_EQUAL("Expected 5 values for argument --test to be present but only 4 have been specified."s, string(failure.what()));
}
try {
occurrence.convertValues<int>();
CPPUNIT_FAIL("Expected exception");
} catch (const Failure &failure) {
CPPUNIT_ASSERT_EQUAL("Conversion of value \"foo\" (for argument --test) to type \"i\" failed: The character \"f\" is no valid digit."s,
string(failure.what()));
}
}