feat(core): add JSON document save/load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c2501ff04e
commit
81f2a9ff8b
@ -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
128
src/core/JsonIo.cpp
Normal 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
19
src/core/JsonIo.h
Normal 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
|
||||
@ -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
84
tests/tst_json.cpp
Normal 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"
|
||||
Loading…
x
Reference in New Issue
Block a user