Created
February 7, 2025 06:56
-
-
Save Ryex/80457ee937b19447f2dab479c4beee08 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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