Improve experimental value conversion for arg parser
* Fix issues and handle conversion errors * Add tests
This commit is contained in:
parent
3a14d39a14
commit
3d3378c878
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue