From 14d03f6d98092ebbeb03ece71737f741d8a428de Mon Sep 17 00:00:00 2001 From: Jerry Jacobs Date: Tue, 10 Mar 2026 13:16:15 +0100 Subject: [PATCH] Add support for custom field value class codec using QS_VALUE and QSerializerValue. --- .github/workflows/debian-13-build.yml | 58 ++++++ .gitignore | 2 +- LICENSE | 2 +- QSerializer | 2 - README.md | 59 ++++-- example/QSerializeExample.pro | 2 +- example/classes.h | 50 ++++- example/general.json | 182 ----------------- example/general.xml | 189 ------------------ example/main.cpp | 2 + src/QSerializer | 3 + qserializer.pri => src/QSerializer.pri | 2 +- src/{qserializer.h => qserializer.hpp} | 102 +++++++++- {benchmarks => tests/benchmarks}/bench.cpp | 0 {benchmarks => tests/benchmarks}/bench.h | 0 .../benchmarks}/benchmarks.pro | 2 +- .../benchmarks}/testclasses.h | 2 +- 17 files changed, 260 insertions(+), 399 deletions(-) create mode 100644 .github/workflows/debian-13-build.yml delete mode 100644 QSerializer delete mode 100644 example/general.json delete mode 100644 example/general.xml create mode 100644 src/QSerializer rename qserializer.pri => src/QSerializer.pri (85%) rename src/{qserializer.h => qserializer.hpp} (91%) rename {benchmarks => tests/benchmarks}/bench.cpp (100%) rename {benchmarks => tests/benchmarks}/bench.h (100%) rename {benchmarks => tests/benchmarks}/benchmarks.pro (96%) rename {benchmarks => tests/benchmarks}/testclasses.h (96%) diff --git a/.github/workflows/debian-13-build.yml b/.github/workflows/debian-13-build.yml new file mode 100644 index 0000000..c836cd7 --- /dev/null +++ b/.github/workflows/debian-13-build.yml @@ -0,0 +1,58 @@ +# test +name: Debian 13 Qt Build + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + # This forces the job to run inside a Debian 13 environment + container: debian:trixie + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Dependencies + run: | + apt-get update + apt-get install -y \ + build-essential \ + qt6-base-dev \ + qt6-base-private-dev \ + libgl1-mesa-dev \ + make + + - name: Create build directories + run: | + mkdir -p build/example + + # Example + - name: Run qmake on example + run: | + cd build/example && qmake6 -makefile ../../example/*.pro + + - name: Compile example + run: | + cd build/example && make -j$(nproc) + + - name: Run example + run: | + cd build/example + ./QSerializeExample + + # Benchmarks + # NOTE: Broken build, last change 6 years ago (API break) + #- name: Run qmake on benchmarks + # run: | + # cd build/benchmarks && qmake6 -o ../build/benchmarks/Makefile *.pro + + #- name: Compile benchmarks + # run: | + # cd build/benchmarks && make -j$(nproc) + + #- name: Run example + # run: | + # cd build/benchmarks && ./benchmarks diff --git a/.gitignore b/.gitignore index 2307298..61a769a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ - +build #ignore thumbnails created by windows Thumbs.db #Ignore files build by Visual Studio diff --git a/LICENSE b/LICENSE index 71ff71a..53f28d1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2020-2021 Agadzhanov Vladimir - Portions Copyright (c) 2021 Jerry Jacobs + Portions Copyright (c) 2021-2026 Jerry Jacobs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/QSerializer b/QSerializer deleted file mode 100644 index cc73c3f..0000000 --- a/QSerializer +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -#include "src/qserializer.h" diff --git a/README.md b/README.md index d7cfb1a..12f4b0c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,37 @@ [logo]() -This project is designed to convert data from an object view to JSON or XML and opposite in the Qt/C++ ecosystem. C ++ classes by default do not have the required meta-object information for serializing class fields, but Qt is equipped with its own highly efficient meta-object system. -An important feature of the QSerializer is the ability to specify serializable fields of the class without having to serialize the entire class. QSerilaizer generate code and declare `Q_PROPERTY` for every declared member of class. This is convenient because you do not need to create separate structures or classes for serialization of write some code to serialize every class, it's just included to QSerializer. + +This project is designed to convert data from an object view to JSON or XML and opposite in the Qt/C++ ecosystem. C++ classes by default do not have the required meta-object information for serializing class fields, but Qt is equipped with its own highly efficient meta-object system. + +An important feature of the QSerializer is the ability to specify serializable fields of the class without having to serialize the entire class. QSerializer generate code and declare `Q_PROPERTY` for every declared member of class. This is convenient because you do not need to create separate structures or classes for serialization of write some code to serialize every class, it's just included to QSerializer. The serialization code is generated by macros during compilation. ## Installation + Download repository + ```bash $ git clone https://github.com/smurfomen/QSerializer.git ``` -Just include qserializer.h in your project and enjoy simple serialization. qserializer.h located in src folder. -Set compiler define `QS_HAS_JSON` or `QS_HAS_XML` for enabling support for xml or json. Enable both at the same time -[is not supported](https://github.com/smurfomen/QSerializer/issues/7). -
A demo project for using QSerializer located in example folder. +Just include `QSerializer` in your project and enjoy simple serialization. `qserializer.hpp` located in src folder. +Set compiler define `QS_HAS_JSON` or `QS_HAS_XML` for enabling support for xml or json. + + + +
A demo project for using QSerializer located in [`example`](/example) folder. ## Workflow -To get started, include qserializer.h in your code. + +To get started: + +- `include(QSerializer/src/QSerializer.pri)` from within qmake `.pro` project file +- `#include ` in your C++ code + ## Create serialization class + For create serializable member of class and generate propertyes, use macro: + - __QS_FIELD__ +- __QS_VALUE__ - __QS_COLLECTION__ - __QS_OBJECT__ - __QS_COLLECTION_OBJECTS__ @@ -26,23 +40,29 @@ For create serializable member of class and generate propertyes, use macro: - __QS_STL_DICT__ - __QS_STL_DICT_OBJECTS__ -If you want only declare exists fields - use macro QS_JSON_FIELD, QS_XML_FIELD, QS_JSON_COLLECTION and other (look at qserializer.h) +If you want only declare exists fields - use macro `QS_JSON_FIELD`, `QS_XML_FIELD`, `QS_JSON_COLLECTION` and other (look at qserializer.hpp) + ### Inherit from QSerializer Inherit from QSerializer, use macro QS_SERIALIZABLE or override metaObject method and declare some serializable fields.
In this case you must use Q_GADGET in your class. + ```C++ class User : public QSerializer { -Q_GADGET -QS_SERIALIZABLE -// Create data members to be serialized - you can use this members in code -QS_FIELD(int, age) -QS_COLLECTION(QVector, QString, parents) + Q_GADGET + QS_SERIALIZABLE + // Create data members to be serialized - you can use this members in code + QS_FIELD(int, age) + QS_COLLECTION(QVector, QString, parents) }; ``` + ## **Serialize** + Now you can serialize object of this class to JSON or XML. + For example: + ```C++ User u; u.age = 20; @@ -63,11 +83,13 @@ QByteArray dxml = u.toRawXml(); ``` ## **Deserialize** + Opposite of the serialization procedure is the deserialization procedure. You can deserialize object from JSON or XML, declared fields will be modified or resets to default for this type values. + For example: + ```C++ -... User u; /* case: json value */ @@ -86,13 +108,16 @@ u.fromXml(xml); QByteArray rawXml; u.fromXml(rawXml); ``` + ## Macro description + | Macro | Description | | --------------------- | ------------------------------------------------------------ | -| QSERIALIZABLE | Make class or struct is serializable to QSerializer (override QMetaObject method and define Q_GADGET macro) | -| QS_FIELD | Create serializable simple field | +| QSERIALIZABLE | Make class or struct serializable to `QSerializer` (override QMetaObject method and define Q_GADGET macro) | +| QS_FIELD | Create serializable simple field (based on `QVariant`) | +| QS_VALUE | Create serializable inner custom primitive value type (based on to/from Xml/Json custom methods). Inherit from `QSerializerValue` class | | QS_COLLECTION | Create serializable collection values of primitive types | -| QS_OBJECT | Create serializable inner custom type object | +| QS_OBJECT | Create serializable inner custom object type | | QS_COLLECTION_OBJECTS | Create serializable collection of custom type objects | | QS_QT_DICT | Create serializable dictionary of primitive type values FOR QT DICTIONARY TYPES | | QS_QT_DICT_OBJECTS | Create serializable dictionary of custom type values FOR QT DICTIONARY TYPES | diff --git a/example/QSerializeExample.pro b/example/QSerializeExample.pro index bcd8576..4e723f2 100644 --- a/example/QSerializeExample.pro +++ b/example/QSerializeExample.pro @@ -16,7 +16,7 @@ CONFIG -= app_bundle DEFINES += QS_HAS_JSON DEFINES += QS_HAS_XML -include(../qserializer.pri) +include(../src/QSerializer.pri) SOURCES += \ main.cpp diff --git a/example/classes.h b/example/classes.h index 355a3e9..621a788 100644 --- a/example/classes.h +++ b/example/classes.h @@ -1,21 +1,67 @@ #ifndef CLASSES_H #define CLASSES_H + #include #include #include +#include + +class CustomDateTime : public QSerializerValue, public QDateTime { +public: + static const Qt::DateFormat fmt = Qt::ISODateWithMs; + using QDateTime::QDateTime; + + CustomDateTime& operator=(const QDateTime& other) { + if (this != &other) { + QDateTime::operator=(other); + } + + return *this; + } + + QJsonValue toJson(void) const { + return QJsonValue(static_cast(*this).toString(Qt::ISODateWithMs)); + } + + void fromJson(const QJsonValue & varname) { + if (!varname.isString()) { + return; + } + static_cast(*this) = QDateTime::fromString(varname.toString(), CustomDateTime::fmt); + } + + void fromXml(const QString & val) + { + if (val.isNull()) { + return; + } + static_cast(*this) = QDateTime::fromString(val, CustomDateTime::fmt); + } + + QString toXml(void) const + { + auto value = static_cast(*this).toString(CustomDateTime::fmt); + return value; + } +}; class Parent : public QSerializer{ Q_GADGET QS_SERIALIZABLE public: - Parent(){ } + Parent() { + created_at = QDateTime::currentDateTime(); + } Parent(int age, const QString & name, bool isMale) : age(age), name(name), - male(isMale) { } + male(isMale) { + created_at = QDateTime::currentDateTime(); + } QS_FIELD(int, age) QS_FIELD(QString, name) QS_FIELD(bool, male) + QS_VALUE(CustomDateTime, created_at) }; class Student : public QSerializer { diff --git a/example/general.json b/example/general.json deleted file mode 100644 index e8d4550..0000000 --- a/example/general.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "collection": { - "list": [ - "first", - "second", - "third" - ], - "stack": [ - 2.44, - 4.42, - 77 - ], - "vector": [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - }, - "collectionObjects": { - "objects": [ - { - "digit": 0, - "string": [ - "so it's just index number in binary 0", - "so it's just index number in binary 1", - "so it's just index number in binary 10", - "so it's just index number in binary 11", - "so it's just index number in binary 100" - ] - }, - { - "digit": 1, - "string": [ - "so it's just index number in binary 1", - "so it's just index number in binary 10", - "so it's just index number in binary 11", - "so it's just index number in binary 100", - "so it's just index number in binary 101" - ] - }, - { - "digit": 2, - "string": [ - "so it's just index number in binary 10", - "so it's just index number in binary 11", - "so it's just index number in binary 100", - "so it's just index number in binary 101", - "so it's just index number in binary 110" - ] - }, - { - "digit": 3, - "string": [ - "so it's just index number in binary 11", - "so it's just index number in binary 100", - "so it's just index number in binary 101", - "so it's just index number in binary 110", - "so it's just index number in binary 111" - ] - }, - { - "digit": 4, - "string": [ - "so it's just index number in binary 100", - "so it's just index number in binary 101", - "so it's just index number in binary 110", - "so it's just index number in binary 111", - "so it's just index number in binary 1000" - ] - } - ] - }, - "dictionaries": { - "qt_hash": { - }, - "qt_map": { - "abra": "kadabra", - "ping": "pong" - }, - "qt_map_objects": { - "+7(909)000-01-00": { - "age": 22, - "links": [ - "http://katelink.com" - ], - "name": "Kate", - "parents": [ - { - "age": 44, - "male": false, - "name": "Marlin" - }, - { - "age": 48, - "male": true, - "name": "Jake" - } - ] - }, - "+7(909)100-00-10": { - "age": 22, - "links": [ - "http://bobsite.com" - ], - "name": "Bob", - "parents": [ - { - "age": 44, - "male": false, - "name": "Mary" - }, - { - "age": 48, - "male": true, - "name": "Koul" - } - ] - } - }, - "std_map": { - "1": "first", - "2": "second" - }, - "std_map_objects": { - "+7(909)000-10-00": { - "age": 21, - "links": [ - "http://somelink.com" - ], - "name": "Jane", - "parents": [ - { - "age": 38, - "male": false, - "name": "Elie" - }, - { - "age": 48, - "male": true, - "name": "John" - } - ] - }, - "+7(909)001-00-00": { - "age": 22, - "links": [ - "http://github.com/smurfomen" - ], - "name": "Ken", - "parents": [ - { - "age": 44, - "male": false, - "name": "Olga" - }, - { - "age": 48, - "male": true, - "name": "Alex" - } - ] - } - } - }, - "field": { - "d_digit": 88.99, - "digit": 34, - "flag": false, - "string": "some string" - }, - "object": { - "digit": 8, - "string": [ - "third", - "second", - "first" - ] - } -} diff --git a/example/general.xml b/example/general.xml deleted file mode 100644 index cde3806..0000000 --- a/example/general.xml +++ /dev/null @@ -1,189 +0,0 @@ - - - 34 - some string - false - 88.99 - - - - 0 - 1 - 2 - 3 - 4 - 5 - - - first - second - third - - - 2.44 - 4.42 - 77 - - - - 8 - - third - second - first - - - - - - 0 - - so it's just index number in binary 0 - so it's just index number in binary 1 - so it's just index number in binary 10 - so it's just index number in binary 11 - so it's just index number in binary 100 - - - - 1 - - so it's just index number in binary 1 - so it's just index number in binary 10 - so it's just index number in binary 11 - so it's just index number in binary 100 - so it's just index number in binary 101 - - - - 2 - - so it's just index number in binary 10 - so it's just index number in binary 11 - so it's just index number in binary 100 - so it's just index number in binary 101 - so it's just index number in binary 110 - - - - 3 - - so it's just index number in binary 11 - so it's just index number in binary 100 - so it's just index number in binary 101 - so it's just index number in binary 110 - so it's just index number in binary 111 - - - - 4 - - so it's just index number in binary 100 - so it's just index number in binary 101 - so it's just index number in binary 110 - so it's just index number in binary 111 - so it's just index number in binary 1000 - - - - - - - - - - - - - - 22 - Kate - - http://katelink.com - - - - 44 - Marlin - false - - - 48 - Jake - true - - - - - - - 22 - Bob - - http://bobsite.com - - - - 44 - Mary - false - - - 48 - Koul - true - - - - - - - - - - - - - 21 - Jane - - http://somelink.com - - - - 38 - Elie - false - - - 48 - John - true - - - - - - - 22 - Ken - - http://github.com/smurfomen - - - - 44 - Olga - false - - - 48 - Alex - true - - - - - - - diff --git a/example/main.cpp b/example/main.cpp index d5da5b2..923a963 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -17,6 +17,8 @@ void json_example() { mother.male = false; mother.name = "Olga"; + //QString test = mother.boembats; + Parent father; father.age = 48; father.male = true; diff --git a/src/QSerializer b/src/QSerializer new file mode 100644 index 0000000..690c192 --- /dev/null +++ b/src/QSerializer @@ -0,0 +1,3 @@ +#pragma once + +#include "qserializer.hpp" diff --git a/qserializer.pri b/src/QSerializer.pri similarity index 85% rename from qserializer.pri rename to src/QSerializer.pri index 412296b..0ed7d2d 100644 --- a/qserializer.pri +++ b/src/QSerializer.pri @@ -3,7 +3,7 @@ contains(DEFINES, QS_HAS_XML) { } HEADERS += \ - $$PWD/src/qserializer.h + $$PWD/qserializer.hpp INCLUDEPATH += $$PWD/ diff --git a/src/qserializer.h b/src/qserializer.hpp similarity index 91% rename from src/qserializer.h rename to src/qserializer.hpp index 1669dd1..85c5029 100644 --- a/src/qserializer.h +++ b/src/qserializer.hpp @@ -49,7 +49,7 @@ #include #include -#define QS_VERSION "1.2" +#define QS_VERSION "1.3" /* Generate metaObject method */ #define QS_META_OBJECT_METHOD \ @@ -69,6 +69,54 @@ Q_DECLARE_METATYPE(QDomNode) Q_DECLARE_METATYPE(QDomElement) #endif +class QSerializerValue { +public: + virtual ~QSerializerValue() = default; + +#ifdef QS_HAS_JSON + /*! \brief Convert QJsonValue in QJsonDocument as QByteArray. */ + static QByteArray toByteArray(const QJsonValue & value){ + return QJsonDocument(value.toObject()).toJson(); + } +#endif + +#ifdef QS_HAS_XML + /*! \brief Convert QDomNode in QDomDocument as QByteArray. */ + static QByteArray toByteArray(const QDomNode & value) { + QDomDocument doc = value.toDocument(); + return doc.toByteArray(); + } + + /*! \brief Make xml processing instruction (hat) and returns new XML QDomDocument. On deserialization procedure all processing instructions will be ignored. */ + static QDomDocument appendXmlHat(const QDomNode &node, const QString & encoding, const QString & version = "1.0"){ + QDomDocument doc = node.toDocument(); + QDomNode xmlNode = doc.createProcessingInstruction("xml", QString("version=\"%1\" encoding=\"%2\"").arg(version).arg(encoding)); + doc.insertBefore(xmlNode, doc.firstChild()); + return doc; + } +#endif + +#ifdef QS_HAS_JSON + /*! \brief Serialize all accessed JSON propertyes for this object. */ + virtual QJsonValue toJson() const = 0; + + + /*! \brief Returns QByteArray representation this object using json-serialization. */ + QByteArray toRawJson() const { + return toByteArray(toJson()); + } + + /*! \brief Deserialize all accessed XML propertyes for this object. */ + virtual void fromJson(const QJsonValue & val) = 0; +#endif + +#ifdef QS_HAS_XML + virtual void fromXml(const QString &val) = 0; \ + + virtual QString toXml(void) const = 0; +#endif +}; + class QSerializer { Q_GADGET QS_SERIALIZABLE @@ -374,6 +422,48 @@ class QSerializer { #define QS_XML_OBJECT(type, name) #endif +/**** TODO ***/ +#ifdef QS_HAS_JSON +#define QS_JSON_VALUE(type, name) \ +public: \ + Q_PROPERTY(QJsonValue name READ GET(json, name) WRITE SET(json, name)) \ + private: \ + QJsonValue GET(json, name)() const { \ + auto val = name.toJson(); \ + return val; \ +} \ + void SET(json, name)(const QJsonValue & varname) { \ + name.fromJson(varname); \ +} +#else +#define QS_JSON_VALUE(type, name) +#endif + +/**** TODO *****/ +#ifdef QS_HAS_XML +#define QS_XML_VALUE(type, name) \ +Q_PROPERTY(QDomNode name READ GET(xml, name) WRITE SET(xml, name)) \ + private: \ + QDomNode GET(xml, name)() const { \ + QDomDocument doc; \ + QString strname = #name; \ + QDomElement element = doc.createElement(strname); \ + QDomText valueOfProp = doc.createTextNode(name.toXml()); \ + element.appendChild(valueOfProp); \ + doc.appendChild(element); \ + return QDomNode(doc); \ + } \ + void SET(xml, name)(const QDomNode &node) { \ + if(!node.isNull() && node.isElement()){ \ + QDomElement domElement = node.toElement(); \ + if(domElement.tagName() == #name) \ + name.fromXml(domElement.text()); \ + } \ +} +#else +#define QS_XML_VALUE(type, name) +#endif + /* Generate JSON-property and methods for collection of custom type objects */ /* Custom item type must be provide methods fromJson and toJson or inherit from QSerializer */ /* This collection must be provide method append(T) (it's can be QList, QVector) */ @@ -745,6 +835,11 @@ class QSerializer { QS_JSON_OBJECT(type, name) \ QS_XML_OBJECT(type, name) \ +/***** TODO ****/ +#define QS_BIND_VALUE(type, name) \ + QS_JSON_VALUE(type, name) \ + QS_XML_VALUE(type, name) \ + /* BIND: */ /* generate serializable propertyes JSON and XML for collection of custom type objects */ #define QS_BIND_COLLECTION_OBJECTS(itemType, name) \ @@ -786,6 +881,11 @@ class QSerializer { QS_DECLARE_MEMBER(type, name) \ QS_BIND_FIELD(type, name) \ +/*** TODO ****/ +#define QS_VALUE(type, name) \ + QS_DECLARE_MEMBER(type, name) \ + QS_BIND_VALUE(type, name) \ + /* CREATE AND BIND: */ /* Make collection of primitive type objects [collectionType name] and generate serializable propertyes for this collection */ /* This collection must be provide method append(T) (it's can be QList, QVector) */ diff --git a/benchmarks/bench.cpp b/tests/benchmarks/bench.cpp similarity index 100% rename from benchmarks/bench.cpp rename to tests/benchmarks/bench.cpp diff --git a/benchmarks/bench.h b/tests/benchmarks/bench.h similarity index 100% rename from benchmarks/bench.h rename to tests/benchmarks/bench.h diff --git a/benchmarks/benchmarks.pro b/tests/benchmarks/benchmarks.pro similarity index 96% rename from benchmarks/benchmarks.pro rename to tests/benchmarks/benchmarks.pro index 498c091..98156ee 100644 --- a/benchmarks/benchmarks.pro +++ b/tests/benchmarks/benchmarks.pro @@ -22,7 +22,7 @@ qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target -include(../src/QSerializer.pri) +include(../../src/QSerializer.pri) HEADERS += \ bench.h \ diff --git a/benchmarks/testclasses.h b/tests/benchmarks/testclasses.h similarity index 96% rename from benchmarks/testclasses.h rename to tests/benchmarks/testclasses.h index b878518..6871fea 100644 --- a/benchmarks/testclasses.h +++ b/tests/benchmarks/testclasses.h @@ -1,7 +1,7 @@ #ifndef TESTCLASSES_H #define TESTCLASSES_H -#include "../src/qserializer.h" +#include #include class TestField_int {