diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 60677e0..ec24fa1 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -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 &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 diff --git a/application/argumentparser.h b/application/argumentparser.h index f79e63f..e60557d 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -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 TargetType convert(const char *value); - -template >> TargetType convert(const char *value) +template > * = nullptr> TargetType convert(const char *value) { return std::string(value); } -template >> TargetType convert(const char *value) +template > * = nullptr> TargetType convert(const char *value) { return ConversionUtilities::stringToNumber(value); } + +/// \cond +namespace Helper { +struct ArgumentValueConversionError { + const char *const errorMessage; + const char *const valueToConvert; + const char *const targetTypeName; + + [[noreturn]] void throwFailure(const std::vector &argumentPath) const; +}; + +template struct ArgumentValueConverter { + static std::tuple convertValues(std::vector::const_iterator firstValue) + { + return std::tuple_cat(ArgumentValueConverter<1, FirstTargetType>::convertValues(firstValue), + ArgumentValueConverter::convertValues(firstValue + 1)); + } +}; + +template struct ArgumentValueConverter<1, FirstTargetType, RemainingTargetTypes...> { + static std::tuple convertValues(std::vector::const_iterator firstValue) + { + // FIXME: maybe use std::expected here when available + try { + return std::make_tuple(ValueConversion::convert(*firstValue)); + } catch (const ConversionUtilities::ConversionException &exception) { + throw ArgumentValueConversionError{ exception.what(), *firstValue, typeid(FirstTargetType).name() }; + } + } +}; +} // namespace Helper +/// \endcond + } // namespace ValueConversion /*! @@ -169,43 +203,30 @@ struct CPP_UTILITIES_EXPORT ArgumentOccurrence { */ std::vector path; - template std::tuple convertValues() const; - template std::tuple convertValues() const; + template std::tuple convertValues() const; private: - template - std::tuple convertValues(std::vector::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 std::tuple ArgumentOccurrence::convertValues() const -{ - return std::make_tuple(ValueConversion::convert(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 -std::tuple ArgumentOccurrence::convertValues() const +template std::tuple ArgumentOccurrence::convertValues() const { - return std::tuple_cat(std::make_tuple(ValueConversion::convert(values.front())), - convertValues(values.cbegin() + 1)); + constexpr auto valuesToConvert = sizeof...(RemainingTargetTypes); + if (values.size() < valuesToConvert) { + throwNumberOfValuesNotSufficient(valuesToConvert); + } + try { + return ValueConversion::Helper::ArgumentValueConverter::convertValues(values.cbegin()); + } catch (const ValueConversion::Helper::ArgumentValueConversionError &error) { + error.throwFailure(path); + } } -/// \cond -template -std::tuple ArgumentOccurrence::convertValues(std::vector::const_iterator firstValue) const -{ - return std::tuple_cat(std::make_tuple(ValueConversion::convert(*firstValue)), - convertValues(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 &values(std::size_t occurrence = 0) const; - template std::tuple convertValues(std::size_t occurrence = 0) const; - template std::vector> convertAllValues() const; + template std::tuple valuesAs(std::size_t occurrence = 0) const; + template std::vector> 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 std::tuple Argument::convertValues(std::size_t occurrence) const +template std::tuple Argument::valuesAs(std::size_t occurrence) const { return m_occurrences[occurrence].convertValues(); } /*! * \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 std::vector> Argument::convertAllValues() const +template std::vector> Argument::allValuesAs() const { std::vector> res; res.reserve(m_occurrences.size()); diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index a5dbed9..4aad1f9 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -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 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()); + static_assert(std::is_same, 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()); + checkConvertedValues("values from Argument::convertValues(1)", arg.valuesAs(1)); + const auto allValues = arg.allValuesAs(); + CPPUNIT_ASSERT_EQUAL(2_st, allValues.size()); + for (const auto &values : allValues) { + checkConvertedValues("values from Argument::convertAllValues", values); + } + static_assert(std::is_same, decltype(arg.valuesAs<>())>::value, "specifying no types yields empty tuple"); + + // error handling + try { + occurrence.convertValues(); + 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(); + 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(); + 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(); + 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())); + } +}