diff --git a/README.md b/README.md index ee89300..4f02abb 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,14 @@ The declarative XML parser provided here easily turns such XML - + image/png 384 256 /icons/test.png - + image/webp 768 512 @@ -93,6 +93,9 @@ struct TestResult { struct Icon { + enum Type { Default, Banner }; + + Type type = {}; QString mimeType = {}; QSize size = {}; QUrl url = {}; @@ -131,6 +134,7 @@ const auto states = StateTable { } }, { State::Icon, { + {u"@type", assign<&TestResult::Icon::type>(result)}, {u"mimetype", assign<&TestResult::Icon::mimeType>(result)}, {u"width", assign<&TestResult::Icon::size, &QSize::setWidth>(result)}, {u"height", assign<&TestResult::Icon::size, &QSize::setHeight>(result)}, diff --git a/tests/auto/tst_xmlparser.cpp b/tests/auto/tst_xmlparser.cpp index 9de7bd6..e126f29 100644 --- a/tests/auto/tst_xmlparser.cpp +++ b/tests/auto/tst_xmlparser.cpp @@ -13,6 +13,9 @@ #include namespace qnc::xml::tests { + +Q_NAMESPACE + namespace { Q_LOGGING_CATEGORY(lcTest, "qnc.xml.tests", QtInfoMsg) @@ -38,17 +41,34 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(Options) QT_WARNING_POP +enum Direction +{ + Input = 1, + Output = 2, +}; + +enum class KnownTypes +{ + KnownType, +}; + +Q_ENUM_NS(KnownTypes) + +using DataType = OpportunisticEnum; + struct TestResult { struct Icon { - QString id = {}; - QString mimeType = {}; - QSize size = {}; - QUrl url = {}; - QString urlId = {}; - QStringList topics = {}; - Options options = {}; + QString id = {}; + QString mimeType = {}; + QSize size = {}; + QUrl url = {}; + QString urlId = {}; + QStringList topics = {}; + Options options = {}; + Direction direction = {}; + DataType type = {}; }; QVersionNumber version = {}; @@ -65,11 +85,28 @@ Q_DECLARE_METATYPE(QXmlStreamReader::Error) Q_DECLARE_METATYPE(qnc::xml::tests::ConversionTest) Q_DECLARE_METATYPE(qnc::xml::tests::TestResult) +namespace qnc::xml { + template <> -qnc::xml::tests::TestResult::Icon & -qnc::xml::currentObject(tests::TestResult &result) { return result.icons.last(); } +tests::TestResult::Icon & +currentObject(tests::TestResult &result) { return result.icons.last(); } -namespace qnc::xml::tests { +template <> +constexpr std::size_t keyCount = 2; + +template <> +constexpr KeyValueMap +keyValueMap() +{ + return { + { + {tests::Direction::Input, u"in"}, + {tests::Direction::Output, u"out"}, + } + }; +} + +namespace tests { namespace { class ParserTest : public QObject @@ -118,6 +155,8 @@ private slots: yes OFF 1 + in + KnownType @@ -130,6 +169,8 @@ private slots: no on 0 + out + UnknownType @@ -144,11 +185,13 @@ private slots: { "icon-a"_L1, "image/png"_L1, {384, 256}, "/icons/test.png"_url, "url-a"_L1, - {}, Option::A | Option::C | Option::E + {}, Option::A | Option::C | Option::E, + Input, KnownTypes::KnownType, }, { "icon-b"_L1, "image/webp"_L1, {768, 512}, "/icons/test.webp"_url, "url-b"_L1, - {"test"_L1}, Option::B | Option::D + {"test"_L1}, Option::B | Option::D, + Output, "UnknownType"_L1, }, }, { "https://ecosia.org/"_url, @@ -214,6 +257,8 @@ private slots: {u"option3", parser.assign<&TestResult::Icon::options, Option::C>(result)}, {u"option4", parser.assign<&TestResult::Icon::options, Option::D>(result)}, {u"option5", parser.assign<&TestResult::Icon::options, Option::E>(result)}, + {u"direction", parser.assign<&TestResult::Icon::direction>(result)}, + {u"type", parser.assign<&TestResult::Icon::type>(result)}, } } }; @@ -245,6 +290,10 @@ private slots: std::make_pair(i, expectedResult.icons[i].topics)); QCOMPARE(std::make_pair(i, result.icons[i].options), std::make_pair(i, expectedResult.icons[i].options)); + QCOMPARE(std::make_pair(i, result.icons[i].direction), + std::make_pair(i, expectedResult.icons[i].direction)); + QCOMPARE(std::make_pair(i, result.icons[i].type), + std::make_pair(i, expectedResult.icons[i].type)); } } @@ -354,7 +403,8 @@ private slots: }; } // namespace -} // namespace qnc::xml::tests +} // namespace tests +} // namespace qnc::xml QTEST_GUILESS_MAIN(qnc::xml::tests::ParserTest) diff --git a/xml/xmlparser.cpp b/xml/xmlparser.cpp index 39b023f..f74307a 100644 --- a/xml/xmlparser.cpp +++ b/xml/xmlparser.cpp @@ -136,6 +136,24 @@ void updateVersion(QVersionNumber &version, VersionSegment segment, int number) version = QVersionNumber{segments}; } +void ParserBase::parseEnum(QStringView text, KeyToIntFunction keyToInt, + const std::function &store) +{ + if (const auto &value = keyToInt(text); Q_LIKELY(value)) + store(*value); + else + m_xml->raiseError(tr("Invalid value for enumeration: %1").arg(text)); +} + +void ParserBase::parseEnum(QStringView text, KeyToIntFunction keyToInt, + const std::function &store) +{ + if (const auto &value = keyToInt(text); Q_LIKELY(value)) + store(*value, QStringView{}); + else + store(0, std::move(text)); +} + template void ParserBase::parseValue(QStringView text, const std::function &store) { diff --git a/xml/xmlparser.h b/xml/xmlparser.h index 3525ed5..cd95856 100644 --- a/xml/xmlparser.h +++ b/xml/xmlparser.h @@ -13,6 +13,7 @@ #include // STL headers +#include #include #include @@ -27,6 +28,18 @@ void updateVersion(QVersionNumber &version, VersionSegment segment, int number); template Object ¤tObject(Context &context) { return context; } +template +constexpr std::size_t keyCount = 0; + +template > +using KeyValueMap = std::array, N>; + +template +constexpr KeyValueMap keyValueMap() { return {}; } + +template +using OpportunisticEnum = std::variant; + namespace detail { struct MemberTraits @@ -62,6 +75,57 @@ using ObjectType = decltype(MemberTraits::objectType(field)); template = true> using ArgumentType = decltype(MemberTraits::argumentType(member)); +template struct is_opportunistic_enum : std::false_type {}; + +template +struct is_opportunistic_enum> : std::true_type {}; + +template +constexpr bool is_opportunistic_enum_v = is_opportunistic_enum::value; + +template +constexpr const char *qt_getEnumName(T) { return nullptr; } + +template +std::optional keyToValue(QStringView key) +{ + if constexpr (!keyValueMap().empty()) { + static constexpr auto map = keyValueMap(); + + static_assert(!std::get(map.front()).isEmpty(), + "Unsupported enum type: keyValueMap() has invalid keys"); + + const auto it = std::find_if(map.cbegin(), map.cend(), [key](const auto &pair) { + return std::get(pair) == key; + }); + + if (Q_LIKELY(it != map.cend())) + return std::get(*it); + } else if constexpr (qt_getEnumName(T{}) != nullptr) { + static const auto &metaEnum = QMetaEnum::fromType(); + + auto isValid = false; + const auto value = metaEnum.keyToValue(key.toLatin1().constData(), &isValid); + + if (Q_LIKELY(isValid)) + return static_cast(value); + } else { + static_assert(static_cast(T{}) && false, + "Unsupported enum type: Neither keyValueMap() nor Q_ENUM() found"); + } + + return {}; +} + +template +std::optional keyToInt(QStringView key) +{ + if (const auto &value = keyToValue(std::move(key)); Q_LIKELY(value)) + return static_cast(*value); + + return {}; +} + } // namespace detail class ParserBase : public QObject @@ -197,6 +261,30 @@ class ParserBase : public QObject void parseStartElement(const QLoggingCategory &category, AbstractContext &context); void parseEndElement(const QLoggingCategory &category, AbstractContext &context); + using KeyToIntFunction = std::optional (*)(QStringView); + + void parseEnum(QStringView text, KeyToIntFunction keyToInt, const std::function &store); + void parseEnum(QStringView text, KeyToIntFunction keyToInt, const std::function &store); + + template + void parseEnum(QStringView text, const std::function &store) + { + parseEnum(text, &detail::keyToInt, [store](int value) { + store(static_cast(value)); + }); + } + + template + void parseEnum(QStringView text, const std::function)> &store) + { + parseEnum(text, &detail::keyToInt, [store](int value, QStringView text) { + if (Q_LIKELY(text.isEmpty())) + store(static_cast(value)); + else + store(text.toString()); + }); + } + void parseFlag(QStringView text, const std::function &store); template @@ -205,7 +293,13 @@ class ParserBase : public QObject template void read(const QXmlStreamAttribute *attribute, const std::function &store) { - parseValue(readValue(attribute), store); + const auto &text = readValue(attribute); + + if constexpr (std::is_enum_v || detail::is_opportunistic_enum_v) { + parseEnum(text, store); + } else { + parseValue(text, store); + } } QString readValue(const QXmlStreamAttribute *attribute);