Skip to content

Instantly share code, notes, and snippets.

@Ryex
Created February 7, 2025 06:56
Show Gist options
  • Save Ryex/80457ee937b19447f2dab479c4beee08 to your computer and use it in GitHub Desktop.
Save Ryex/80457ee937b19447f2dab479c4beee08 to your computer and use it in GitHub Desktop.
#include <qlogging.h>
#include <QDebug>
#include <QMetaClassInfo>
#include <QObject>
#include <QString>
#include <QStringList>
#include <iostream>
#include "serialize.h"
class TestClass : QObject {
Q_OBJECT
Q_CLASSINFO("serde", "true")
Q_CLASSINFO("serde-ignore", "testIgnoreVal")
Q_CLASSINFO("serde-ignore", "invalidIgnoreVal")
Q_PROPERTY(QString testIgnoreVal MEMBER m_testIgnoreVal)
Q_PROPERTY(QString testVal MEMBER m_testVal)
public:
TestClass() = default;
private:
QString m_testVal;
QString m_testIgnoreVal;
};
class Person2 : public QObject {
Q_OBJECT
Q_CLASSINFO("serde", "true")
public:
Q_INVOKABLE Person2() = default;
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(QStringList names MEMBER m_names)
QString m_name;
QStringList m_names;
};
class Person : public QObject {
Q_OBJECT
Q_CLASSINFO("serde", "true")
public:
Q_INVOKABLE Person() = default;
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(QStringList names MEMBER m_names)
Q_PROPERTY(Person2* p MEMBER m_p)
QString m_name;
QStringList m_names;
Person2* m_p;
};
Q_DECLARE_METATYPE(Person2*)
Q_DECLARE_METATYPE(Person*)
#include "main.moc"
int main(int argc, char* argv[])
{
std::cout << "Hello" << std::endl;
QMetaObject metaobj = TestClass::staticMetaObject;
int infoCount = metaobj.classInfoCount();
for (int i = 0; i < infoCount; ++i) {
QMetaClassInfo info = metaobj.classInfo(i);
std::cout << info.name() << " " << info.value() << std::endl;
}
Person p = {}; // must be constructed first
try {
prism::serde::deserializeQObject({ { "name", "name" },
{ "names", QVariantList({ "1", "@" }) },
{ "p",
QVariantMap{
{ "name", "name" },
} } },
&p);
} catch (prism::serde::ConversionException err) {
qDebug() << err.getErrorMessage() << err.getPath();
}
qDebug() << "test deserialize : " << p.m_p->m_name;
std::cout << "Goodbye" << std::endl;
return 0;
}
#include "serialize.h"
#include <qhashfunctions.h>
#include <qlogging.h>
#include <cstring>
#include <QMetaClassInfo>
#include <QMetaType>
struct QObjectSerdeInfo {
bool tagged;
QSet<QString> ignored;
QMap<QString, QString> renames;
QMap<QString, QString> aliases;
QSet<QString> optional;
};
/**
* @brief collect serde info from classInfo on the meta object
*
* @param metaObject
* @return the info struct
* @throws ConversionException If a serde* classInfo value has an invalid format
*/
QObjectSerdeInfo collectSerdeInfo(const QMetaObject* metaObject, QStringList path)
{
QObjectSerdeInfo serdeInfo;
int infoCount = metaObject->classInfoCount();
for (int i = 0; i < infoCount; ++i) {
QMetaClassInfo info = metaObject->classInfo(i);
const char* name = info.name();
if (std::strcmp(name, "serde") == 0) {
serdeInfo.tagged = true;
} else if (std::strcmp(name, "serde-ignore") == 0) {
serdeInfo.ignored.insert(info.value());
} else if (std::strcmp(name, "serde-optional") == 0) {
serdeInfo.optional.insert(info.value());
} else if (std::strcmp(name, "serde-rename") == 0) {
QStringList parts = QString(info.value()).split('=');
if (parts.length() != 2) {
throw prism::serde::ConversionException(QString("Invalid serde-rename info value '%1'").arg(info.value()), path);
}
QString prop = parts[0];
QString rename = parts[1];
if (serdeInfo.renames.contains(prop)) {
throw prism::serde::ConversionException(QString("Invalid serde-rename '%1=%2': property '%1' already has rename '%3'")
.arg(prop, rename, serdeInfo.renames.value(prop)),
path);
}
if (serdeInfo.aliases.contains(rename)) {
throw prism::serde::ConversionException(QString("Invalid serde-rename '%1=%2': alias '%2' already used for property '%3'")
.arg(prop, rename, serdeInfo.aliases.value(rename)),
path);
}
serdeInfo.renames.insert(prop, rename);
serdeInfo.aliases.insert(rename, prop);
}
}
return serdeInfo;
}
QVariantMap prism::serde::serializeQObject(const QObject* obj, int maxDepth, QStringList path)
{
int depth = path.length();
if (depth > maxDepth) {
throw ConversionException(QString("Depth '%1' greater than max depth '%2'").arg(depth, maxDepth), path);
}
QVariantMap result;
if (!obj) {
throw ConversionException("Null pointer passed as object.", path);
}
const QMetaObject* metaObject = obj->metaObject();
QObjectSerdeInfo serdeInfo = collectSerdeInfo(metaObject, path);
if (!serdeInfo.tagged) {
throw ConversionException(
QString("QObject class '%1' is not tagged as serializeable in it's class info").arg(metaObject->className()), path);
}
int propCount = metaObject->propertyCount();
for (int i = 0; i < propCount; ++i) {
QMetaProperty prop = metaObject->property(i);
const char* name = prop.name();
QStringList propPath(path);
propPath.append(name);
if (!prop.isReadable() || !prop.isStored() || serdeInfo.ignored.contains(name))
continue;
QVariant value = obj->property(name);
QString propName = serdeInfo.renames.value(name, name);
QMetaType propType = prop.metaType();
const QMetaObject* propMetaObject = propType.metaObject();
QMetaType::TypeFlags flags = propType.flags();
if (flags & QMetaType::PointerToQObject) {
if (!value.isNull() && value.canConvert<QVariantMap>()) {
result[propName] = value.value<QVariantMap>();
} else if (!value.isNull()) {
QObjectSerdeInfo propSerdeInfo = collectSerdeInfo(propMetaObject, propPath);
if (!propSerdeInfo.tagged) {
throw ConversionException(QString("QObject pointer of class '%1' is not tagged as serializeable in it's class info")
.arg(propMetaObject->className()),
propPath);
}
if (!(value.canConvert(propType) && value.canConvert<QObject*>())) {
throw ConversionException(QString("QVariant of '%1' can not be converted to 'QObject*'").arg(propType.name()),
propPath);
}
QObject* propObj = value.value<QObject*>();
QVariantMap propVal = serializeQObject(propObj, maxDepth, propPath); // Recursive call
result[propName] = propVal;
} else {
result[propName] = value;
}
} else if (flags & QMetaType::IsPointer) {
throw ConversionException("Non QObject pointer types are not currently supported", propPath);
} else {
result[propName] = value;
}
}
return result;
}
void prism::serde::deserializeQObject(const QVariantMap& map, QObject* obj, int maxDepth, QStringList path)
{
int depth = path.length();
if (depth > maxDepth) {
throw ConversionException(QString("Depth '%1' greater than max depth '%2'").arg(depth, maxDepth), path);
}
if (!obj) {
throw ConversionException("Null pointer passed as object.", path);
}
const QMetaObject* metaObject = obj->metaObject();
QStringList keys;
QObjectSerdeInfo serdeInfo = collectSerdeInfo(metaObject, path);
if (!serdeInfo.tagged) {
throw ConversionException(
QString("QObject class '%1' is not tagged as serializeable in it's class info").arg(metaObject->className()), path);
}
for (auto it = map.begin(); it != map.end(); ++it) {
auto key = it.key();
QByteArray propName = serdeInfo.aliases.value(key, key).toUtf8();
int propIndex = metaObject->indexOfProperty(propName.constData());
QStringList propPath(path);
propPath.append(propName);
if (propIndex == -1) {
throw ConversionException(
QString("Property '%1' aka '%2' not found in object at path: %3").arg(propName, it.key(), propPath.join(".")), propPath);
}
qDebug() << "property:" << propPath.join(".");
QMetaProperty prop = metaObject->property(propIndex);
QMetaType propType = prop.metaType();
const QMetaObject* propMetaObject = propType.metaObject();
QMetaType::TypeFlags flags = propType.flags();
keys << prop.name();
QVariant value = it.value();
if (value.canConvert(prop.metaType())) {
if (!prop.write(obj, value)) {
throw ConversionException(QString("Failed to set property '%1' at path: '%2'").arg(propName).arg(propPath.join(".")),
propPath);
}
} else if (flags & QMetaType::PointerToQObject) {
if (value.isNull()) {
if (!prop.write(obj, value)) {
throw ConversionException(QString("Failed to set property '%1' at path: '%2'").arg(propName).arg(propPath.join(".")),
propPath);
}
} else {
if (!value.canConvert<QVariantMap>()) {
throw ConversionException(
QString("Failed to convert value for property '%1' as path '%2' : Non map can not become a QObject")
.arg(propName)
.arg(propPath.join(".")),
propPath);
}
QObjectSerdeInfo propSerdeInfo = collectSerdeInfo(propMetaObject, propPath);
if (!propSerdeInfo.tagged) {
throw ConversionException(
QString("QObjectclass '%1' is not tagged as serializeable in it's class info").arg(propMetaObject->className()),
propPath);
}
const QVariant objPropVal = prop.read(obj);
QObject* propObj = nullptr;
if (objPropVal.isValid() && !objPropVal.isNull() && objPropVal.canConvert<QObject*>()) {
propObj = qvariant_cast<QObject*>(objPropVal);
}
if (!propObj) {
int propTypeConstructorCount = propMetaObject->constructorCount();
bool foundDefaultConstructor = false;
for (int c = 0; c < propTypeConstructorCount; ++c) {
QMetaMethod constructor = propMetaObject->constructor(c);
if (constructor.parameterCount() == 0 && constructor.isValid()) {
foundDefaultConstructor = true;
}
}
if (!foundDefaultConstructor) {
throw ConversionException(QString("QObject class '%1' does not have a default constructor marked with Q_INVOKABLE")
.arg(propMetaObject->className()),
propPath);
}
propObj = propMetaObject->newInstance();
if (!prop.write(obj, QVariant::fromValue(propObj))) {
throw ConversionException(
QString("Failed to set property '%1' at path: '%2'").arg(propName).arg(propPath.join(".")), propPath);
}
}
QVariantMap propObjMap = value.toMap();
deserializeQObject(propObjMap, propObj, maxDepth, propPath); // Recursive call
}
} else if (flags & QMetaType::IsPointer) {
throw ConversionException("Non QObject pointer types are not currently supported", propPath);
} else {
throw ConversionException(QString("Failed to convert value for property '%1' at path %2 : Can not convert '%3' to '%4'")
.arg(propName)
.arg(propPath.join("."))
.arg(value.metaType().name())
.arg(propType.name()),
propPath);
}
}
for (int i = 0; i < metaObject->propertyCount(); ++i) {
QMetaProperty metaProp = metaObject->property(i);
if (metaProp.isStored() && metaProp.isRequired() && !serdeInfo.ignored.contains(metaProp.name()) &&
!keys.contains(metaProp.name())) {
throw ConversionException(QString("Property '%1' is required but not set").arg(metaProp.name()), path);
}
}
}
#pragma once
#include <qmap.h>
#include <qset.h>
#include <stdexcept>
#include <QList>
#include <QMap>
#include <QMetaObject>
#include <QSet>
#include <QString>
#include <QVariant>
#include <QVariantMap>
namespace prism {
namespace serde {
class ConversionException : public std::runtime_error {
public:
explicit ConversionException(const QString& message, QStringList path)
: std::runtime_error(message.toStdString()), m_errorMessage(message), m_path(path)
{}
const QString& getErrorMessage() const { return m_errorMessage; }
QStringList getPath() const { return m_path; }
private:
QString m_errorMessage;
QStringList m_path;
};
/**
* @brief Convert a QObject tagged with appropriate class info into a QVarientMap
*
* @param obj pointer to the QObject to serialize
* @param path nested property path to this object - mainly for internal use
* @param maxDepth maximum nested depth to set
* @param path nested property path to this object - internal use
* @return A QVarientMap with all the stored QObject properties
* @throws ConversionException if an error is encountered in the object structure or properties
*/
QVariantMap serializeQObject(const QObject* obj, int maxDepth = 20, QStringList path = QStringList());
/**
* @brief Convert a QVariantMap to a QObject by mapping property names
*
* @param map the QVariantMap of property names to values
* @param obj pointer to QObject where properties should be set
* @param maxDepth maximum nested depth to set
* @param path nested property path to this object - internal use
*/
void deserializeQObject(const QVariantMap& map, QObject* obj, int maxDepth = 20, QStringList path = QStringList());
/**
* @brief Template Overload : Convert a QVarientMap into a QObject returning a new pointer
*
* @tparam T QObject class
* @param map the QVariantMap of property names to values
* @param maxDepth maximum nested depth to set
* @param path nested property path to this object - internal use
* @return pointer to QObject with properties set
*/
template <class T>
T* deserializeQObject(const QVariantMap& map, int maxDepth = 20, QStringList path = QStringList())
{
T* obj = new T();
deserializeQObject(map, qobject_cast<QObject*>(obj), maxDepth, path);
return obj;
}
} // namespace serde
} // namespace prism
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment