refactor(qt6): migrate C++ sources to .hpp header-only style

Move ModPlayer, NodeRegistry, PackageRegistry from .cpp/.h pairs to
single .hpp files. Add DBALRequest.hpp and DBALTypes.hpp. Update
DBALClient and main.cpp for new structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:02:41 +00:00
parent 33be8aa9db
commit 296de30836
14 changed files with 662 additions and 551 deletions

View File

@@ -4,11 +4,11 @@
#include <QUrl>
#include <QDir>
#include "src/PackageRegistry.h"
#include "src/ModPlayer.h"
#include "src/PackageRegistry.hpp"
#include "src/ModPlayer.hpp"
#include "src/DBALClient.h"
#include "src/PackageLoader.h"
#include "src/NodeRegistry.h"
#include "src/NodeRegistry.hpp"
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);

View File

@@ -1,4 +1,5 @@
#include "DBALClient.h"
// DBALTypes.hpp and DBALRequest.hpp are pulled in via DBALClient.h
#include <QJsonDocument>
#include <QNetworkRequest>
#include <QUrl>
@@ -57,10 +58,12 @@ void DBALClient::setPackageId(const QString &pkg)
QString DBALClient::entityPath(const QString &entity) const
{
// REST: /api/v1/{tenant}/{package}/{entity}
return QString("/api/v1/%1/%2/%3").arg(m_tenantId, m_packageId, entity.toLower());
return QString("/api/v1/%1/%2/%3")
.arg(m_tenantId, m_packageId, entity.toLower());
}
QString DBALClient::entityPath(const QString &entity, const QString &id) const
QString DBALClient::entityPath(
const QString &entity, const QString &id) const
{
return entityPath(entity) + "/" + id;
}
@@ -71,23 +74,30 @@ void DBALClient::setError(const QString &error)
emit errorOccurred(error);
}
void DBALClient::sendRequest(const QString &method, const QString &endpoint,
const QJsonObject &body, const QJSValue &callback)
void DBALClient::sendRequest(
const QString &method,
const QString &endpoint,
const DBALPayload &body,
const DBALCallback &callback)
{
QUrl url(m_baseUrl + endpoint);
QNetworkRequest request(url);
// Set headers
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(
QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("X-Tenant-ID", m_tenantId.toUtf8());
if (!m_authToken.isEmpty()) {
request.setRawHeader("Authorization", ("Bearer " + m_authToken).toUtf8());
request.setRawHeader(
"Authorization",
("Bearer " + m_authToken).toUtf8());
}
QNetworkReply *reply = nullptr;
QByteArray jsonData = QJsonDocument(body).toJson(QJsonDocument::Compact);
QByteArray jsonData =
QJsonDocument(body).toJson(QJsonDocument::Compact);
if (method == "GET") {
reply = m_networkManager->get(request);
} else if (method == "POST") {
@@ -97,7 +107,7 @@ void DBALClient::sendRequest(const QString &method, const QString &endpoint,
} else if (method == "DELETE") {
reply = m_networkManager->deleteResource(request);
}
if (reply && callback.isCallable()) {
m_pendingCallbacks[reply] = callback;
}
@@ -106,12 +116,12 @@ void DBALClient::sendRequest(const QString &method, const QString &endpoint,
void DBALClient::handleNetworkReply(QNetworkReply *reply)
{
reply->deleteLater();
QJSValue callback = m_pendingCallbacks.take(reply);
DBALCallback callback = m_pendingCallbacks.take(reply);
if (reply->error() != QNetworkReply::NoError) {
setError(reply->errorString());
if (callback.isCallable()) {
QJSValueList args;
args << QJSValue::NullValue;
@@ -120,14 +130,14 @@ void DBALClient::handleNetworkReply(QNetworkReply *reply)
}
return;
}
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (callback.isCallable()) {
QJSValueList args;
// In Qt6, QJSValue::engine() is removed. Pass data as JSON string
// and let QML parse it, or pass QVariant directly.
// In Qt6, QJSValue::engine() is removed. Pass data as
// a JSON string and let QML parse it, or pass QVariant.
if (doc.isObject()) {
args << QJSValue(QString::fromUtf8(data));
} else if (doc.isArray()) {
@@ -137,7 +147,7 @@ void DBALClient::handleNetworkReply(QNetworkReply *reply)
}
callback.call(args);
}
// Update connected status
if (!m_connected) {
m_connected = true;
@@ -145,89 +155,133 @@ void DBALClient::handleNetworkReply(QNetworkReply *reply)
}
}
// CRUD Operations — DBAL REST API: /api/v1/{tenant}/{package}/{entity}[/{id}]
// CRUD Operations
// REST path: /api/v1/{tenant}/{package}/{entity}[/{id}]
void DBALClient::create(const QString &entity, const QJsonObject &data, const QJSValue &callback)
void DBALClient::create(
const QString &entity,
const DBALPayload &data,
const DBALCallback &callback)
{
sendRequest("POST", entityPath(entity), data, callback);
}
void DBALClient::read(const QString &entity, const QString &id, const QJSValue &callback)
void DBALClient::read(
const QString &entity,
const QString &id,
const DBALCallback &callback)
{
sendRequest("GET", entityPath(entity, id), QJsonObject(), callback);
sendRequest("GET", entityPath(entity, id), DBALPayload(), callback);
}
void DBALClient::update(const QString &entity, const QString &id,
const QJsonObject &data, const QJSValue &callback)
void DBALClient::update(
const QString &entity,
const QString &id,
const DBALPayload &data,
const DBALCallback &callback)
{
sendRequest("PUT", entityPath(entity, id), data, callback);
}
void DBALClient::remove(const QString &entity, const QString &id, const QJSValue &callback)
void DBALClient::remove(
const QString &entity,
const QString &id,
const DBALCallback &callback)
{
sendRequest("DELETE", entityPath(entity, id), QJsonObject(), callback);
sendRequest(
"DELETE", entityPath(entity, id), DBALPayload(), callback);
}
void DBALClient::list(const QString &entity, const QJsonObject &options, const QJSValue &callback)
void DBALClient::list(
const QString &entity,
const DBALPayload &options,
const DBALCallback &callback)
{
// Build query string from options (take, skip, where, orderBy)
QString path = entityPath(entity);
QStringList queryParts;
if (options.contains("take")) queryParts << "take=" + QString::number(options["take"].toInt());
if (options.contains("skip")) queryParts << "skip=" + QString::number(options["skip"].toInt());
if (options.contains("orderBy")) queryParts << "orderBy=" + options["orderBy"].toString();
if (!queryParts.isEmpty()) path += "?" + queryParts.join("&");
if (options.contains("take"))
queryParts << "take="
+ QString::number(options["take"].toInt());
if (options.contains("skip"))
queryParts << "skip="
+ QString::number(options["skip"].toInt());
if (options.contains("orderBy"))
queryParts << "orderBy=" + options["orderBy"].toString();
if (!queryParts.isEmpty())
path += "?" + queryParts.join("&");
sendRequest("GET", path, QJsonObject(), callback);
sendRequest("GET", path, DBALPayload(), callback);
}
void DBALClient::findFirst(const QString &entity, const QJsonObject &filter, const QJSValue &callback)
void DBALClient::findFirst(
const QString &entity,
const DBALPayload &filter,
const DBALCallback &callback)
{
// GET with query params for simple filters
QString path = entityPath(entity);
QStringList queryParts;
queryParts << "take=1";
for (auto it = filter.begin(); it != filter.end(); ++it) {
queryParts << QUrl::toPercentEncoding(it.key()) + "=" + QUrl::toPercentEncoding(it.value().toString());
queryParts
<< QUrl::toPercentEncoding(it.key())
+ "="
+ QUrl::toPercentEncoding(it.value().toString());
}
if (!queryParts.isEmpty()) path += "?" + queryParts.join("&");
if (!queryParts.isEmpty())
path += "?" + queryParts.join("&");
sendRequest("GET", path, QJsonObject(), callback);
sendRequest("GET", path, DBALPayload(), callback);
}
void DBALClient::execute(const QString &operation, const QJsonObject &params, const QJSValue &callback)
void DBALClient::execute(
const QString &operation,
const DBALPayload &params,
const DBALCallback &callback)
{
// Execute maps to POST /{tenant}/{package}/{entity}/{action} or system endpoints
QString path = QString("/api/v1/%1/%2").arg(m_tenantId, operation);
// POST to /{tenant}/{operation} (or /{entity}/{action})
QString path =
QString("/api/v1/%1/%2").arg(m_tenantId, operation);
sendRequest("POST", path, params, callback);
}
void DBALClient::ping()
{
sendRequest("GET", "/health", QJsonObject(), QJSValue());
sendRequest("GET", "/health", DBALPayload(), DBALCallback());
}
void DBALClient::health(const QJSValue &callback)
void DBALClient::health(const DBALCallback &callback)
{
sendRequest("GET", "/health", QJsonObject(), callback);
sendRequest("GET", "/health", DBALPayload(), callback);
}
void DBALClient::version(const QJSValue &callback)
void DBALClient::version(const DBALCallback &callback)
{
sendRequest("GET", "/version", QJsonObject(), callback);
sendRequest("GET", "/version", DBALPayload(), callback);
}
void DBALClient::status(const QJSValue &callback)
void DBALClient::status(const DBALCallback &callback)
{
sendRequest("GET", "/status", QJsonObject(), callback);
sendRequest("GET", "/status", DBALPayload(), callback);
}
void DBALClient::listSchemas(const QJSValue &callback)
void DBALClient::listSchemas(const DBALCallback &callback)
{
sendRequest("GET", "/api/v1/" + m_tenantId + "/schema", QJsonObject(), callback);
sendRequest(
"GET",
"/api/v1/" + m_tenantId + "/schema",
DBALPayload(),
callback);
}
void DBALClient::getSchema(const QString &entity, const QJSValue &callback)
void DBALClient::getSchema(
const QString &entity,
const DBALCallback &callback)
{
sendRequest("GET", "/api/v1/" + m_tenantId + "/schema/" + entity.toLower(), QJsonObject(), callback);
sendRequest(
"GET",
"/api/v1/" + m_tenantId + "/schema/" + entity.toLower(),
DBALPayload(),
callback);
}

View File

@@ -1,58 +1,81 @@
#ifndef DBALCLIENT_H
#define DBALCLIENT_H
#pragma once
// Qt6 DBAL Client Bridge
//
// Provides database access for QML components through the DBAL
// daemon. Communicates via HTTP to the C++ DBAL backend.
//
// Usage in QML:
// DBALClient {
// id: dbal
// baseUrl: "http://localhost:3001/api/dbal"
// tenantId: "default"
// Component.onCompleted: {
// dbal.list("User", { take: 10 }, function(users) {
// console.log("Users:", JSON.stringify(users))
// })
// }
// }
#include "DBALTypes.hpp"
#include "DBALRequest.hpp"
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
#include <QVariantMap>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJSValue>
#include <QMap>
/**
* @brief Qt6 DBAL Client Bridge
*
* Provides database access for QML components through the DBAL daemon.
* Communicates via HTTP/WebSocket to the TypeScript or C++ DBAL backend.
*
* Usage in QML:
* @code
* DBALClient {
* id: dbal
* baseUrl: "http://localhost:3001/api/dbal"
* tenantId: "default"
*
* Component.onCompleted: {
* dbal.list("User", { take: 10 }, function(users) {
* console.log("Users:", JSON.stringify(users))
* })
* }
* }
* @endcode
*/
class DBALClient : public QObject
{
Q_OBJECT
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(QString tenantId READ tenantId WRITE setTenantId NOTIFY tenantIdChanged)
Q_PROPERTY(QString packageId READ packageId WRITE setPackageId NOTIFY packageIdChanged)
Q_PROPERTY(QString authToken READ authToken WRITE setAuthToken NOTIFY authTokenChanged)
Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY errorOccurred)
Q_PROPERTY(
QString baseUrl
READ baseUrl
WRITE setBaseUrl
NOTIFY baseUrlChanged)
Q_PROPERTY(
QString tenantId
READ tenantId
WRITE setTenantId
NOTIFY tenantIdChanged)
Q_PROPERTY(
QString packageId
READ packageId
WRITE setPackageId
NOTIFY packageIdChanged)
Q_PROPERTY(
QString authToken
READ authToken
WRITE setAuthToken
NOTIFY authTokenChanged)
Q_PROPERTY(
bool connected
READ isConnected
NOTIFY connectedChanged)
Q_PROPERTY(
QString lastError
READ lastError
NOTIFY errorOccurred)
public:
explicit DBALClient(QObject *parent = nullptr);
~DBALClient() override;
// Property getters
QString baseUrl() const { return m_baseUrl; }
QString tenantId() const { return m_tenantId; }
QString packageId() const { return m_packageId; }
QString authToken() const { return m_authToken; }
bool isConnected() const { return m_connected; }
QString lastError() const { return m_lastError; }
QString baseUrl() const { return m_baseUrl; }
QString tenantId() const { return m_tenantId; }
QString packageId() const { return m_packageId; }
QString authToken() const { return m_authToken; }
bool isConnected() const { return m_connected; }
QString lastError() const { return m_lastError; }
// Property setters
void setBaseUrl(const QString &url);
@@ -61,98 +84,70 @@ public:
void setAuthToken(const QString &token);
public slots:
/**
* @brief Create a new record
* @param entity Entity name (e.g., "User", "AuditLog")
* @param data Record data as JSON object
* @param callback QML callback function(result)
*/
void create(const QString &entity, const QJsonObject &data, const QJSValue &callback);
/// Create a new record. callback: function(result)
void create(
const QString &entity,
const DBALPayload &data,
const DBALCallback &callback);
/**
* @brief Read a single record by ID
* @param entity Entity name
* @param id Record ID
* @param callback QML callback function(result)
*/
void read(const QString &entity, const QString &id, const QJSValue &callback);
/// Read a single record by ID. callback: function(result)
void read(
const QString &entity,
const QString &id,
const DBALCallback &callback);
/**
* @brief Update an existing record
* @param entity Entity name
* @param id Record ID
* @param data Updated fields
* @param callback QML callback function(result)
*/
void update(const QString &entity, const QString &id, const QJsonObject &data, const QJSValue &callback);
/// Update an existing record. callback: function(result)
void update(
const QString &entity,
const QString &id,
const DBALPayload &data,
const DBALCallback &callback);
/**
* @brief Delete a record
* @param entity Entity name
* @param id Record ID
* @param callback QML callback function(success)
*/
void remove(const QString &entity, const QString &id, const QJSValue &callback);
/// Delete a record. callback: function(success)
void remove(
const QString &entity,
const QString &id,
const DBALCallback &callback);
/**
* @brief List records with pagination and filtering
* @param entity Entity name
* @param options { take, skip, where, orderBy }
* @param callback QML callback function({ items, total })
*/
void list(const QString &entity, const QJsonObject &options, const QJSValue &callback);
/// List records with pagination/filtering.
/// options: { take, skip, where, orderBy }
/// callback: function({ items, total })
void list(
const QString &entity,
const DBALPayload &options,
const DBALCallback &callback);
/**
* @brief Find first record matching filter
* @param entity Entity name
* @param filter Filter criteria
* @param callback QML callback function(result)
*/
void findFirst(const QString &entity, const QJsonObject &filter, const QJSValue &callback);
/// Find first record matching filter. callback: function(result)
void findFirst(
const QString &entity,
const DBALPayload &filter,
const DBALCallback &callback);
/**
* @brief Execute a named query/operation
* @param operation Operation name
* @param params Operation parameters
* @param callback QML callback function(result)
*/
void execute(const QString &operation, const QJsonObject &params, const QJSValue &callback);
/// Execute a named operation. callback: function(result)
void execute(
const QString &operation,
const DBALPayload &params,
const DBALCallback &callback);
/**
* @brief Check connection to DBAL backend
*/
/// Check connection to DBAL backend (fire-and-forget).
void ping();
/**
* @brief Get DBAL health information
* @param callback QML callback function(result)
*/
void health(const QJSValue &callback);
/// Get DBAL health info. callback: function(result)
void health(const DBALCallback &callback);
/**
* @brief Get DBAL version information
* @param callback QML callback function(result)
*/
void version(const QJSValue &callback);
/// Get DBAL version info. callback: function(result)
void version(const DBALCallback &callback);
/**
* @brief Get DBAL status/metrics
* @param callback QML callback function(result)
*/
void status(const QJSValue &callback);
/// Get DBAL status/metrics. callback: function(result)
void status(const DBALCallback &callback);
/**
* @brief List entity schemas from DBAL
* @param callback QML callback function(schemas)
*/
void listSchemas(const QJSValue &callback);
/// List entity schemas. callback: function(schemas)
void listSchemas(const DBALCallback &callback);
/**
* @brief Get a specific entity schema
* @param entity Entity name
* @param callback QML callback function(schema)
*/
void getSchema(const QString &entity, const QJSValue &callback);
/// Get a specific entity schema. callback: function(schema)
void getSchema(
const QString &entity,
const DBALCallback &callback);
signals:
void baseUrlChanged();
@@ -161,27 +156,33 @@ signals:
void authTokenChanged();
void connectedChanged();
void errorOccurred(const QString &error);
void operationCompleted(const QString &operation, const QJsonObject &result);
void operationCompleted(
const QString &operation,
const DBALPayload &result);
private slots:
void handleNetworkReply(QNetworkReply *reply);
private:
void sendRequest(const QString &method, const QString &endpoint,
const QJsonObject &body, const QJSValue &callback);
void sendRequest(
const QString &method,
const QString &endpoint,
const DBALPayload &body,
const DBALCallback &callback);
void setError(const QString &error);
QNetworkAccessManager *m_networkManager;
QString m_baseUrl;
QString m_tenantId;
QString m_packageId;
QString m_authToken;
QString entityPath(const QString &entity) const;
QString entityPath(const QString &entity, const QString &id) const;
bool m_connected;
QString m_lastError;
QMap<QNetworkReply*, QJSValue> m_pendingCallbacks;
};
QString entityPath(
const QString &entity,
const QString &id) const;
#endif // DBALCLIENT_H
QNetworkAccessManager *m_networkManager;
QString m_baseUrl;
QString m_tenantId;
QString m_packageId;
QString m_authToken;
bool m_connected;
QString m_lastError;
QMap<QNetworkReply*, DBALCallback> m_pendingCallbacks;
};

View File

@@ -0,0 +1,28 @@
#pragma once
// DBALRequest encapsulates a single in-flight DBAL HTTP request.
//
// DBALClient stores one DBALRequest per QNetworkReply* in its
// pending-request map. Using a named struct instead of a bare
// QJSValue map makes the bookkeeping self-documenting and easy
// to extend (e.g. adding timeout tracking or retry counts).
#include "DBALTypes.hpp"
#include <QNetworkReply>
#include <QString>
/// Tracks the state of a single pending DBAL network request.
struct DBALRequest
{
/// The entity name the request targets (e.g. "User").
QString entity;
/// The HTTP verb used for this request ("GET", "POST", …).
QString method;
/// QML callback to invoke when the reply arrives.
DBALCallback callback;
/// The live network reply (used as the map key in DBALClient).
QNetworkReply *reply = nullptr;
};

View File

@@ -0,0 +1,17 @@
#pragma once
// Forward declarations and type aliases for the Qt6 DBAL layer.
//
// All DBAL public APIs use QJSValue for QML-callable callbacks.
// DBALCallback is the canonical alias used throughout DBALClient
// and DBALRequest to make callback parameters self-documenting.
#include <QJSValue>
#include <QString>
#include <QJsonObject>
/// Alias for a QML-callable callback passed to DBAL operations.
using DBALCallback = QJSValue;
/// Alias for a JSON body payload sent with DBAL requests.
using DBALPayload = QJsonObject;

View File

@@ -1,42 +0,0 @@
#include "ModPlayer.h"
#include <QDebug>
#include <QFile>
// ModPlayer stub - libopenmpt + Qt6 Multimedia not yet configured via Conan.
// Once available, this will use QAudioSink (Qt6) + openmpt::module for .mod playback.
ModPlayer::ModPlayer(QObject *parent)
: QObject(parent)
{
}
ModPlayer::~ModPlayer() {
stop();
}
bool ModPlayer::play(const QString &path) {
if (!QFile::exists(path)) {
qWarning() << "ModPlayer: file not found:" << path;
return false;
}
qInfo() << "ModPlayer: would play" << path << "(audio backend not yet linked)";
updatePlaying(true);
return true;
}
void ModPlayer::stop() {
updatePlaying(false);
}
bool ModPlayer::isPlaying() const {
return m_playing;
}
void ModPlayer::updatePlaying(bool playing) {
if (m_playing == playing)
return;
m_playing = playing;
emit playbackChanged();
}

View File

@@ -1,25 +0,0 @@
#pragma once
#include <QObject>
#include <QString>
class ModPlayer : public QObject {
Q_OBJECT
Q_PROPERTY(bool playing READ isPlaying NOTIFY playbackChanged)
public:
explicit ModPlayer(QObject *parent = nullptr);
~ModPlayer() override;
Q_INVOKABLE bool play(const QString &path);
Q_INVOKABLE void stop();
bool isPlaying() const;
signals:
void playbackChanged();
private:
bool m_playing = false;
void updatePlaying(bool playing);
};

View File

@@ -0,0 +1,57 @@
#pragma once
#include <QDebug>
#include <QFile>
#include <QObject>
#include <QString>
// ModPlayer stub - libopenmpt + Qt6 Multimedia not yet configured via Conan.
// Once available, this will use QAudioSink (Qt6) + openmpt::module
// for .mod playback.
class ModPlayer : public QObject {
Q_OBJECT
Q_PROPERTY(bool playing READ isPlaying NOTIFY playbackChanged)
public:
explicit ModPlayer(QObject *parent = nullptr)
: QObject(parent)
{
}
~ModPlayer() override {
stop();
}
Q_INVOKABLE bool play(const QString &path) {
if (!QFile::exists(path)) {
qWarning() << "ModPlayer: file not found:" << path;
return false;
}
qInfo() << "ModPlayer: would play" << path
<< "(audio backend not yet linked)";
updatePlaying(true);
return true;
}
Q_INVOKABLE void stop() {
updatePlaying(false);
}
bool isPlaying() const {
return m_playing;
}
signals:
void playbackChanged();
private:
bool m_playing = false;
void updatePlaying(bool playing) {
if (m_playing == playing)
return;
m_playing = playing;
emit playbackChanged();
}
};

View File

@@ -1,111 +0,0 @@
#include "NodeRegistry.h"
#include <QFile>
#include <QJsonDocument>
#include <QDebug>
#include <algorithm>
NodeRegistry::NodeRegistry(QObject *parent)
: QObject(parent)
{
}
QVariantList NodeRegistry::nodeTypes() const
{
QVariantList list;
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
list.append(it.value().toVariantMap());
}
return list;
}
void NodeRegistry::loadRegistry(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_lastError = QStringLiteral("Cannot open registry file: ") + path;
qWarning() << "NodeRegistry:" << m_lastError;
emit errorOccurred(m_lastError);
return;
}
QJsonParseError parseErr;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseErr);
file.close();
if (parseErr.error != QJsonParseError::NoError) {
m_lastError = QStringLiteral("JSON parse error: ") + parseErr.errorString();
qWarning() << "NodeRegistry:" << m_lastError;
emit errorOccurred(m_lastError);
return;
}
parseRegistry(doc.object());
m_loaded = true;
emit loadedChanged();
emit nodeTypesChanged();
qDebug() << "NodeRegistry: loaded" << m_nodeTypes.count() << "node types in"
<< m_groups.count() << "groups from" << path;
}
void NodeRegistry::parseRegistry(const QJsonObject &root)
{
m_nodeTypes.clear();
m_groups.clear();
const QJsonArray types = root.value(QStringLiteral("nodeTypes")).toArray();
QSet<QString> groupSet;
for (const QJsonValue &val : types) {
const QJsonObject obj = val.toObject();
const QString name = obj.value(QStringLiteral("name")).toString();
if (name.isEmpty())
continue;
m_nodeTypes.insert(name, obj);
const QString group = obj.value(QStringLiteral("group")).toString();
if (!group.isEmpty())
groupSet.insert(group);
}
m_groups = groupSet.values();
m_groups.sort();
}
QVariantMap NodeRegistry::nodeType(const QString &name) const
{
if (!m_nodeTypes.contains(name))
return {};
return m_nodeTypes.value(name).toVariantMap();
}
QVariantList NodeRegistry::nodesByGroup(const QString &group) const
{
QVariantList list;
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
if (it.value().value(QStringLiteral("group")).toString() == group)
list.append(it.value().toVariantMap());
}
return list;
}
QVariantList NodeRegistry::searchNodes(const QString &query) const
{
if (query.isEmpty())
return nodeTypes();
const QString lower = query.toLower();
QVariantList list;
for (auto it = m_nodeTypes.constBegin(); it != m_nodeTypes.constEnd(); ++it) {
const QJsonObject &obj = it.value();
const QString name = obj.value(QStringLiteral("name")).toString().toLower();
const QString displayName = obj.value(QStringLiteral("displayName")).toString().toLower();
const QString desc = obj.value(QStringLiteral("description")).toString().toLower();
if (name.contains(lower) || displayName.contains(lower) || desc.contains(lower))
list.append(obj.toVariantMap());
}
return list;
}

View File

@@ -1,50 +0,0 @@
#ifndef NODEREGISTRY_H
#define NODEREGISTRY_H
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
#include <QStringList>
#include <QMap>
#include <QVariantList>
class NodeRegistry : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantList nodeTypes READ nodeTypes NOTIFY nodeTypesChanged)
Q_PROPERTY(QStringList groups READ groups NOTIFY nodeTypesChanged)
Q_PROPERTY(int nodeCount READ nodeCount NOTIFY nodeTypesChanged)
Q_PROPERTY(bool loaded READ isLoaded NOTIFY loadedChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY errorOccurred)
public:
explicit NodeRegistry(QObject *parent = nullptr);
QVariantList nodeTypes() const;
QStringList groups() const { return m_groups; }
int nodeCount() const { return m_nodeTypes.count(); }
bool isLoaded() const { return m_loaded; }
QString lastError() const { return m_lastError; }
public slots:
void loadRegistry(const QString &path);
QVariantMap nodeType(const QString &name) const;
QVariantList nodesByGroup(const QString &group) const;
QVariantList searchNodes(const QString &query) const;
signals:
void nodeTypesChanged();
void loadedChanged();
void errorOccurred(const QString &error);
private:
void parseRegistry(const QJsonObject &root);
QMap<QString, QJsonObject> m_nodeTypes;
QStringList m_groups;
bool m_loaded = false;
QString m_lastError;
};
#endif

View File

@@ -0,0 +1,185 @@
#pragma once
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
#include <QStringList>
#include <QMap>
#include <QVariantList>
#include <QFile>
#include <QJsonDocument>
#include <QDebug>
#include <algorithm>
class NodeRegistry : public QObject
{
Q_OBJECT
Q_PROPERTY(
QVariantList nodeTypes
READ nodeTypes
NOTIFY nodeTypesChanged)
Q_PROPERTY(
QStringList groups
READ groups
NOTIFY nodeTypesChanged)
Q_PROPERTY(
int nodeCount
READ nodeCount
NOTIFY nodeTypesChanged)
Q_PROPERTY(
bool loaded
READ isLoaded
NOTIFY loadedChanged)
Q_PROPERTY(
QString lastError
READ lastError
NOTIFY errorOccurred)
public:
explicit NodeRegistry(QObject *parent = nullptr)
: QObject(parent)
{}
QVariantList nodeTypes() const
{
QVariantList list;
for (auto it = m_nodeTypes.constBegin();
it != m_nodeTypes.constEnd(); ++it) {
list.append(it.value().toVariantMap());
}
return list;
}
QStringList groups() const { return m_groups; }
int nodeCount() const { return m_nodeTypes.count(); }
bool isLoaded() const { return m_loaded; }
QString lastError() const { return m_lastError; }
public slots:
void loadRegistry(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_lastError =
QStringLiteral("Cannot open registry file: ") + path;
qWarning() << "NodeRegistry:" << m_lastError;
emit errorOccurred(m_lastError);
return;
}
QJsonParseError parseErr;
const QJsonDocument doc =
QJsonDocument::fromJson(file.readAll(), &parseErr);
file.close();
if (parseErr.error != QJsonParseError::NoError) {
m_lastError =
QStringLiteral("JSON parse error: ")
+ parseErr.errorString();
qWarning() << "NodeRegistry:" << m_lastError;
emit errorOccurred(m_lastError);
return;
}
parseRegistry(doc.object());
m_loaded = true;
emit loadedChanged();
emit nodeTypesChanged();
qDebug() << "NodeRegistry: loaded"
<< m_nodeTypes.count()
<< "node types in"
<< m_groups.count()
<< "groups from" << path;
}
QVariantMap nodeType(const QString &name) const
{
if (!m_nodeTypes.contains(name))
return {};
return m_nodeTypes.value(name).toVariantMap();
}
QVariantList nodesByGroup(const QString &group) const
{
QVariantList list;
for (auto it = m_nodeTypes.constBegin();
it != m_nodeTypes.constEnd(); ++it) {
const QString g =
it.value()
.value(QStringLiteral("group"))
.toString();
if (g == group)
list.append(it.value().toVariantMap());
}
return list;
}
QVariantList searchNodes(const QString &query) const
{
if (query.isEmpty())
return nodeTypes();
const QString lower = query.toLower();
QVariantList list;
for (auto it = m_nodeTypes.constBegin();
it != m_nodeTypes.constEnd(); ++it) {
const QJsonObject &obj = it.value();
const QString name =
obj.value(QStringLiteral("name"))
.toString().toLower();
const QString displayName =
obj.value(QStringLiteral("displayName"))
.toString().toLower();
const QString desc =
obj.value(QStringLiteral("description"))
.toString().toLower();
if (name.contains(lower)
|| displayName.contains(lower)
|| desc.contains(lower))
list.append(obj.toVariantMap());
}
return list;
}
signals:
void nodeTypesChanged();
void loadedChanged();
void errorOccurred(const QString &error);
private:
void parseRegistry(const QJsonObject &root)
{
m_nodeTypes.clear();
m_groups.clear();
const QJsonArray types =
root.value(QStringLiteral("nodeTypes")).toArray();
QSet<QString> groupSet;
for (const QJsonValue &val : types) {
const QJsonObject obj = val.toObject();
const QString name =
obj.value(QStringLiteral("name")).toString();
if (name.isEmpty())
continue;
m_nodeTypes.insert(name, obj);
const QString group =
obj.value(QStringLiteral("group")).toString();
if (!group.isEmpty())
groupSet.insert(group);
}
m_groups = groupSet.values();
m_groups.sort();
}
QMap<QString, QJsonObject> m_nodeTypes;
QStringList m_groups;
bool m_loaded = false;
QString m_lastError;
};

View File

@@ -1,103 +0,0 @@
#include "PackageRegistry.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include <QTextStream>
namespace {
QString normalizedPath(const QString &path) {
QDir dir(path);
return dir.absolutePath();
}
QString metadataFileName(const QString &packageId) {
return packageId + "/metadata.json";
}
}
PackageRegistry::PackageRegistry(QObject *parent)
: QObject(parent)
{
const auto appDir = QCoreApplication::applicationDirPath();
m_roots << normalizedPath(appDir + "/packages");
m_roots << normalizedPath(appDir + "/../packages");
m_roots << normalizedPath(appDir + "/../frontends/qt6/packages");
m_roots << normalizedPath(appDir + "/../../frontends/qt6/packages");
}
QStringList PackageRegistry::packageIds() const
{
QSet<QString> ids;
for (const auto &root : m_roots) {
QDir dir(root);
if (!dir.exists())
continue;
const auto entries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto &entry : entries) {
const auto meta = dir.filePath(metadataFileName(entry));
if (QFile::exists(meta))
ids.insert(entry);
}
}
return ids.values();
}
QString PackageRegistry::loadedPackage() const
{
return m_loadedPackage;
}
QVariantMap PackageRegistry::loadedMetadata() const
{
return m_loadedMetadata;
}
QVariantMap PackageRegistry::metadata(const QString &packageId) const
{
const auto filePath = findMetadataFile(packageId);
if (filePath.isEmpty())
return {};
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return {};
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject())
return {};
return doc.object().toVariantMap();
}
bool PackageRegistry::loadPackage(const QString &packageId)
{
const auto filePath = findMetadataFile(packageId);
if (filePath.isEmpty())
return false;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return false;
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject())
return false;
m_loadedPackage = packageId;
m_loadedMetadata = doc.object().toVariantMap();
emit packageLoaded();
emit metadataChanged();
return true;
}
QString PackageRegistry::findMetadataFile(const QString &packageId) const
{
for (const auto &root : m_roots) {
const auto candidate = QDir(root).filePath(metadataFileName(packageId));
if (QFile::exists(candidate))
return candidate;
}
return {};
}

View File

@@ -1,32 +0,0 @@
#pragma once
#include <QObject>
#include <QVariantMap>
class PackageRegistry : public QObject {
Q_OBJECT
Q_PROPERTY(QStringList packageIds READ packageIds NOTIFY packagesChanged)
Q_PROPERTY(QString loadedPackage READ loadedPackage NOTIFY packageLoaded)
Q_PROPERTY(QVariantMap loadedMetadata READ loadedMetadata NOTIFY metadataChanged)
public:
explicit PackageRegistry(QObject *parent = nullptr);
QStringList packageIds() const;
QString loadedPackage() const;
QVariantMap loadedMetadata() const;
Q_INVOKABLE bool loadPackage(const QString &packageId);
Q_INVOKABLE QVariantMap metadata(const QString &packageId) const;
signals:
void packagesChanged();
void packageLoaded();
void metadataChanged();
private:
QString findMetadataFile(const QString &packageId) const;
QStringList m_roots;
QString m_loadedPackage;
QVariantMap m_loadedMetadata;
};

View File

@@ -0,0 +1,132 @@
#pragma once
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QSet>
#include <QTextStream>
#include <QVariantMap>
namespace {
inline QString normalizedPath(const QString &path) {
QDir dir(path);
return dir.absolutePath();
}
inline QString metadataFileName(const QString &packageId) {
return packageId + "/metadata.json";
}
} // namespace
class PackageRegistry : public QObject {
Q_OBJECT
Q_PROPERTY(
QStringList packageIds
READ packageIds
NOTIFY packagesChanged
)
Q_PROPERTY(
QString loadedPackage
READ loadedPackage
NOTIFY packageLoaded
)
Q_PROPERTY(
QVariantMap loadedMetadata
READ loadedMetadata
NOTIFY metadataChanged
)
public:
explicit PackageRegistry(QObject *parent = nullptr)
: QObject(parent)
{
const auto appDir = QCoreApplication::applicationDirPath();
m_roots << normalizedPath(appDir + "/packages");
m_roots << normalizedPath(appDir + "/../packages");
m_roots << normalizedPath(
appDir + "/../frontends/qt6/packages"
);
m_roots << normalizedPath(
appDir + "/../../frontends/qt6/packages"
);
}
QStringList packageIds() const {
QSet<QString> ids;
for (const auto &root : m_roots) {
QDir dir(root);
if (!dir.exists())
continue;
const auto entries =
dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto &entry : entries) {
const auto meta =
dir.filePath(metadataFileName(entry));
if (QFile::exists(meta))
ids.insert(entry);
}
}
return ids.values();
}
QString loadedPackage() const {
return m_loadedPackage;
}
QVariantMap loadedMetadata() const {
return m_loadedMetadata;
}
Q_INVOKABLE bool loadPackage(const QString &packageId) {
const auto filePath = findMetadataFile(packageId);
if (filePath.isEmpty())
return false;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return false;
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject())
return false;
m_loadedPackage = packageId;
m_loadedMetadata = doc.object().toVariantMap();
emit packageLoaded();
emit metadataChanged();
return true;
}
Q_INVOKABLE QVariantMap metadata(const QString &packageId) const {
const auto filePath = findMetadataFile(packageId);
if (filePath.isEmpty())
return {};
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return {};
const auto doc = QJsonDocument::fromJson(file.readAll());
if (!doc.isObject())
return {};
return doc.object().toVariantMap();
}
signals:
void packagesChanged();
void packageLoaded();
void metadataChanged();
private:
QStringList m_roots;
QString m_loadedPackage;
QVariantMap m_loadedMetadata;
QString findMetadataFile(const QString &packageId) const {
for (const auto &root : m_roots) {
const auto candidate =
QDir(root).filePath(metadataFileName(packageId));
if (QFile::exists(candidate))
return candidate;
}
return {};
}
};