feat(core): add JSON document save/load

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ilya Ashikhmin 2026-07-02 23:14:11 +02:00
parent c2501ff04e
commit 81f2a9ff8b
5 changed files with 234 additions and 0 deletions

View File

@ -10,6 +10,8 @@ add_library(diagcore STATIC
core/Router.cpp
core/FlowSolver.h
core/FlowSolver.cpp
core/JsonIo.h
core/JsonIo.cpp
)
target_include_directories(diagcore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(diagcore PUBLIC Qt6::Core Qt6::Gui)

128
src/core/JsonIo.cpp Normal file
View File

@ -0,0 +1,128 @@
#include "JsonIo.h"
#include "NetworkModel.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QSaveFile>
namespace diag {
QJsonObject JsonIo::toJson(const NetworkModel& model)
{
QJsonArray nodes;
for (QUuid id : model.nodeIds()) {
const Node* n = model.node(id);
QJsonObject o;
o.insert(QLatin1String("id"), n->id.toString(QUuid::WithoutBraces));
o.insert(QLatin1String("type"), n->typeId);
o.insert(QLatin1String("pos"), QJsonArray{n->pos.x(), n->pos.y()});
if (!n->props.isEmpty())
o.insert(QLatin1String("props"), QJsonObject::fromVariantMap(n->props));
nodes.append(o);
}
QJsonArray edges;
for (QUuid id : model.edgeIds()) {
const Edge* e = model.edge(id);
QJsonObject o;
o.insert(QLatin1String("id"), e->id.toString(QUuid::WithoutBraces));
o.insert(QLatin1String("from"), e->fromNode.toString(QUuid::WithoutBraces));
o.insert(QLatin1String("fromPort"), e->fromPort);
o.insert(QLatin1String("to"), e->toNode.toString(QUuid::WithoutBraces));
o.insert(QLatin1String("toPort"), e->toPort);
if (!e->props.isEmpty())
o.insert(QLatin1String("props"), QJsonObject::fromVariantMap(e->props));
edges.append(o);
}
QJsonObject root;
root.insert(QLatin1String("format"), QStringLiteral("pipediagram"));
root.insert(QLatin1String("version"), 1);
root.insert(QLatin1String("nodes"), nodes);
root.insert(QLatin1String("edges"), edges);
return root;
}
bool JsonIo::fromJson(const QJsonObject& doc, NetworkModel* model, QString* error)
{
if (doc.value(QLatin1String("format")).toString() != QLatin1String("pipediagram")) {
if (error)
*error = QStringLiteral("Not a pipediagram document");
return false;
}
model->clear();
for (const auto& nv : doc.value(QLatin1String("nodes")).toArray()) {
const QJsonObject o = nv.toObject();
const QUuid id(o.value(QLatin1String("id")).toString());
const QJsonArray pos = o.value(QLatin1String("pos")).toArray();
const QUuid added = model->addNode(o.value(QLatin1String("type")).toString(),
{pos.at(0).toDouble(), pos.at(1).toDouble()}, id);
if (added.isNull()) {
if (error)
*error = QStringLiteral("Unknown node type: %1")
.arg(o.value(QLatin1String("type")).toString());
return false;
}
const QVariantMap props = o.value(QLatin1String("props")).toObject().toVariantMap();
for (auto it = props.begin(); it != props.end(); ++it)
model->setNodeProperty(added, it.key(), it.value());
}
for (const auto& ev : doc.value(QLatin1String("edges")).toArray()) {
const QJsonObject o = ev.toObject();
const QUuid id(o.value(QLatin1String("id")).toString());
const QUuid added = model->addEdge(QUuid(o.value(QLatin1String("from")).toString()),
o.value(QLatin1String("fromPort")).toString(),
QUuid(o.value(QLatin1String("to")).toString()),
o.value(QLatin1String("toPort")).toString(), id);
if (added.isNull()) {
if (error)
*error = QStringLiteral("Invalid edge %1").arg(o.value(QLatin1String("id")).toString());
return false;
}
const QVariantMap props = o.value(QLatin1String("props")).toObject().toVariantMap();
for (auto it = props.begin(); it != props.end(); ++it)
model->setEdgeProperty(added, it.key(), it.value());
}
return true;
}
bool JsonIo::save(const NetworkModel& model, const QString& path, QString* error)
{
QSaveFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
if (error)
*error = f.errorString();
return false;
}
f.write(QJsonDocument(toJson(model)).toJson(QJsonDocument::Indented));
if (!f.commit()) {
if (error)
*error = f.errorString();
return false;
}
return true;
}
bool JsonIo::load(NetworkModel* model, const QString& path, QString* error)
{
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
if (error)
*error = f.errorString();
return false;
}
QJsonParseError perr;
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll(), &perr);
if (doc.isNull()) {
if (error)
*error = perr.errorString();
return false;
}
return fromJson(doc.object(), model, error);
}
} // namespace diag

19
src/core/JsonIo.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <QJsonObject>
#include <QString>
namespace diag {
class NetworkModel;
// Document persistence: nodes, edges and their property overrides as JSON.
class JsonIo {
public:
static QJsonObject toJson(const NetworkModel& model);
static bool fromJson(const QJsonObject& doc, NetworkModel* model, QString* error = nullptr);
static bool save(const NetworkModel& model, const QString& path, QString* error = nullptr);
static bool load(NetworkModel* model, const QString& path, QString* error = nullptr);
};
} // namespace diag

View File

@ -10,3 +10,4 @@ diag_add_test(tst_model diagcore)
diag_add_test(tst_properties diagcore)
diag_add_test(tst_router diagcore)
diag_add_test(tst_solver diagcore)
diag_add_test(tst_json diagcore)

84
tests/tst_json.cpp Normal file
View File

@ -0,0 +1,84 @@
#include "TestFixtures.h"
#include "core/JsonIo.h"
#include "core/NetworkModel.h"
#include <QTemporaryDir>
#include <QTest>
using namespace diag;
class TestJson : public QObject {
Q_OBJECT
private slots:
void init()
{
fixtures::fillRegistry(m_reg);
delete m_model;
m_model = new NetworkModel(&m_reg, this);
}
void roundTrip_preservesDocument()
{
const QUuid src = m_model->addNode(QStringLiteral("source"), {20, 40});
const QUuid pump = m_model->addNode(QStringLiteral("pump"), {220, 40});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {420, 40});
const QUuid e1 = m_model->addEdge(src, "out", pump, "in");
const QUuid e2 = m_model->addEdge(pump, "out", c, "in");
m_model->setNodeProperty(src, "title", QStringLiteral("Intake"));
m_model->setNodeProperty(src, "tags", QStringList{"a", "b"});
m_model->setEdgeProperty(e1, "diameter", 150.0);
m_model->setEdgeProperty(e1, "headDecoration", QStringLiteral("Diamond"));
m_model->setEdgeProperty(e2, "series", QStringLiteral("dn100"));
const QJsonObject doc = JsonIo::toJson(*m_model);
NetworkModel restored(&m_reg);
QString err;
QVERIFY2(JsonIo::fromJson(doc, &restored, &err), qPrintable(err));
QCOMPARE(restored.nodeIds().size(), 3);
QCOMPARE(restored.edgeIds().size(), 2);
QCOMPARE(restored.node(src)->pos, QPointF(20, 40));
QCOMPARE(restored.nodeProperty(src, "title").toString(), QStringLiteral("Intake"));
QCOMPARE(restored.nodeProperty(src, "tags").toStringList(), (QStringList{"a", "b"}));
QCOMPARE(restored.edgeProperty(e1, "diameter").toDouble(), 150.0);
QCOMPARE(restored.edgeProperty(e1, "headDecoration").toString(),
QStringLiteral("Diamond"));
QCOMPARE(restored.edgeProperty(e2, "series").toString(), QStringLiteral("dn100"));
QCOMPARE(restored.edge(e1)->fromNode, src);
QCOMPARE(restored.edge(e1)->toNode, pump);
QCOMPARE(restored.edge(e1)->relation, QStringLiteral("water"));
}
void saveAndLoad_file()
{
QTemporaryDir dir;
const QString path = dir.filePath(QStringLiteral("doc.json"));
const QUuid src = m_model->addNode(QStringLiteral("source"), {0, 0});
const QUuid c = m_model->addNode(QStringLiteral("consumer"), {200, 0});
m_model->addEdge(src, "out", c, "in");
QString err;
QVERIFY2(JsonIo::save(*m_model, path, &err), qPrintable(err));
NetworkModel restored(&m_reg);
QVERIFY2(JsonIo::load(&restored, path, &err), qPrintable(err));
QCOMPARE(restored.nodeIds().size(), 2);
QCOMPARE(restored.edgeIds().size(), 1);
}
void invalidDocument_rejected()
{
NetworkModel restored(&m_reg);
QString err;
QVERIFY(!JsonIo::fromJson(QJsonObject{{"format", "other"}}, &restored, &err));
QVERIFY(!err.isEmpty());
}
private:
TypeRegistry m_reg;
NetworkModel* m_model = nullptr;
};
QTEST_GUILESS_MAIN(TestJson)
#include "tst_json.moc"