Skip to content

Commit

Permalink
Allow parsing of enum types
Browse files Browse the repository at this point in the history
  • Loading branch information
hasselmm committed Sep 19, 2024
1 parent 014daab commit ddafbed
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 16 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ The declarative XML parser provided here easily turns such XML
</version>

<icons>
<icon>
<icon type="default">
<mimetype>image/png</mimetype>
<width>384</width>
<height>256</height>
<url>/icons/test.png</url>
</icon>

<icon>
<icon type="banner">
<mimetype>image/webp</mimetype>
<width>768</width>
<height>512</height>
Expand All @@ -93,6 +93,9 @@ struct TestResult
{
struct Icon
{
enum Type { Default, Banner };

Type type = {};
QString mimeType = {};
QSize size = {};
QUrl url = {};
Expand Down Expand Up @@ -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)},
Expand Down
76 changes: 63 additions & 13 deletions tests/auto/tst_xmlparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#include <QVersionNumber>

namespace qnc::xml::tests {

Q_NAMESPACE

namespace {

Q_LOGGING_CATEGORY(lcTest, "qnc.xml.tests", QtInfoMsg)
Expand All @@ -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<KnownTypes>;

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 = {};
Expand All @@ -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<tests::Direction> = 2;

template <>
constexpr KeyValueMap<tests::Direction>
keyValueMap<tests::Direction>()
{
return {
{
{tests::Direction::Input, u"in"},
{tests::Direction::Output, u"out"},
}
};
}

namespace tests {
namespace {

class ParserTest : public QObject
Expand Down Expand Up @@ -118,6 +155,8 @@ private slots:
<option3>yes</option3>
<option4>OFF</option4>
<option5>1</option5>
<direction>in</direction>
<type>KnownType</type>
</icon>
<icon id="icon-b">
Expand All @@ -130,6 +169,8 @@ private slots:
<option3>no</option3>
<option4>on</option4>
<option5>0</option5>
<direction>out</direction>
<type>UnknownType</type>
</icon>
</icons>
Expand All @@ -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,
Expand Down Expand Up @@ -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)},
}
}
};
Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -354,7 +403,8 @@ private slots:
};

} // namespace
} // namespace qnc::xml::tests
} // namespace tests
} // namespace qnc::xml

QTEST_GUILESS_MAIN(qnc::xml::tests::ParserTest)

Expand Down
18 changes: 18 additions & 0 deletions xml/xmlparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void (int)> &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<void (int, QStringView)> &store)
{
if (const auto &value = keyToInt(text); Q_LIKELY(value))
store(*value, QStringView{});
else
store(0, std::move(text));
}

template <typename T>
void ParserBase::parseValue(QStringView text, const std::function<void(T)> &store)
{
Expand Down
96 changes: 95 additions & 1 deletion xml/xmlparser.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <QXmlStreamReader>

// STL headers
#include <array>
#include <optional>
#include <variant>

Expand All @@ -27,6 +28,18 @@ void updateVersion(QVersionNumber &version, VersionSegment segment, int number);
template <class Object, class Context>
Object &currentObject(Context &context) { return context; }

template <typename T>
constexpr std::size_t keyCount = 0;

template <typename T, std::size_t N = keyCount<T>>
using KeyValueMap = std::array<std::pair<T, QStringView>, N>;

template <typename T>
constexpr KeyValueMap<T> keyValueMap() { return {}; }

template<typename T>
using OpportunisticEnum = std::variant<std::monostate, T, QString>;

namespace detail {

struct MemberTraits
Expand Down Expand Up @@ -62,6 +75,57 @@ using ObjectType = decltype(MemberTraits::objectType(field));
template <auto member, RequireMemberFunction<member> = true>
using ArgumentType = decltype(MemberTraits::argumentType(member));

template<typename T> struct is_opportunistic_enum : std::false_type {};

template<typename T>
struct is_opportunistic_enum<OpportunisticEnum<T>> : std::true_type {};

template<typename T>
constexpr bool is_opportunistic_enum_v = is_opportunistic_enum<T>::value;

template<typename T>
constexpr const char *qt_getEnumName(T) { return nullptr; }

template<typename T>
std::optional<T> keyToValue(QStringView key)
{
if constexpr (!keyValueMap<T>().empty()) {
static constexpr auto map = keyValueMap<T>();

static_assert(!std::get<QStringView>(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<QStringView>(pair) == key;
});

if (Q_LIKELY(it != map.cend()))
return std::get<T>(*it);
} else if constexpr (qt_getEnumName(T{}) != nullptr) {
static const auto &metaEnum = QMetaEnum::fromType<T>();

auto isValid = false;
const auto value = metaEnum.keyToValue(key.toLatin1().constData(), &isValid);

if (Q_LIKELY(isValid))
return static_cast<T>(value);
} else {
static_assert(static_cast<int>(T{}) && false,
"Unsupported enum type: Neither keyValueMap() nor Q_ENUM() found");
}

return {};
}

template<typename T>
std::optional<int> keyToInt(QStringView key)
{
if (const auto &value = keyToValue<T>(std::move(key)); Q_LIKELY(value))
return static_cast<int>(*value);

return {};
}

} // namespace detail

class ParserBase : public QObject
Expand Down Expand Up @@ -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<int> (*)(QStringView);

void parseEnum(QStringView text, KeyToIntFunction keyToInt, const std::function<void(int)> &store);
void parseEnum(QStringView text, KeyToIntFunction keyToInt, const std::function<void(int, QStringView)> &store);

template <typename T>
void parseEnum(QStringView text, const std::function<void(T)> &store)
{
parseEnum(text, &detail::keyToInt<T>, [store](int value) {
store(static_cast<T>(value));
});
}

template <typename T>
void parseEnum(QStringView text, const std::function<void(OpportunisticEnum<T>)> &store)
{
parseEnum(text, &detail::keyToInt<T>, [store](int value, QStringView text) {
if (Q_LIKELY(text.isEmpty()))
store(static_cast<T>(value));
else
store(text.toString());
});
}

void parseFlag(QStringView text, const std::function<void(bool)> &store);

template <typename T>
Expand All @@ -205,7 +293,13 @@ class ParserBase : public QObject
template <typename T>
void read(const QXmlStreamAttribute *attribute, const std::function<void(T)> &store)
{
parseValue(readValue(attribute), store);
const auto &text = readValue(attribute);

if constexpr (std::is_enum_v<T> || detail::is_opportunistic_enum_v<T>) {
parseEnum(text, store);
} else {
parseValue(text, store);
}
}

QString readValue(const QXmlStreamAttribute *attribute);
Expand Down

0 comments on commit ddafbed

Please sign in to comment.