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);