1595 lines
40 KiB
C++
1595 lines
40 KiB
C++
#include "procedureparser.h"
|
|
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
|
|
#include <c4/format.hpp>
|
|
#include <ryml_std.hpp>
|
|
|
|
ProcedureParser::ProcedureParser(QObject *parent)
|
|
: QObject(parent), tree(std::make_unique<ryml::Tree>()) {}
|
|
|
|
ProcedureParser::~ProcedureParser() {}
|
|
|
|
bool ProcedureParser::loadConfig(const QString &filePath)
|
|
{
|
|
configFilePath = filePath;
|
|
validationErrors.clear();
|
|
|
|
QFileInfo fileInfo(filePath);
|
|
QString suffix = fileInfo.suffix().toLower();
|
|
|
|
bool success = false;
|
|
if (suffix == "json" || suffix == "yaml" || suffix == "yml")
|
|
{
|
|
success = parseFile(filePath);
|
|
}
|
|
else
|
|
{
|
|
validationErrors << QString("Unsupported file format: %1").arg(suffix);
|
|
emit configError("Unsupported file format", 0, 0);
|
|
return false;
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
buildReferenceCache();
|
|
emit configLoaded(filePath);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool ProcedureParser::parseFile(const QString &filePath)
|
|
{
|
|
QFile file(filePath);
|
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
validationErrors << QString("Failed to open file: %1").arg(filePath);
|
|
emit configError("Failed to open file", 0, 0);
|
|
return false;
|
|
}
|
|
|
|
QByteArray fileContent = file.readAll();
|
|
file.close();
|
|
|
|
QFileInfo fileInfo(filePath);
|
|
QString suffix = fileInfo.suffix().toLower();
|
|
|
|
try
|
|
{
|
|
yamlContent.assign(fileContent.begin(), fileContent.end());
|
|
|
|
// Use appropriate parser based on file type
|
|
if (suffix == "json")
|
|
{
|
|
*tree = ryml::parse_json_in_arena(ryml::to_csubstr(yamlContent));
|
|
}
|
|
else
|
|
{
|
|
// For YAML files, parse and resolve anchors/aliases
|
|
*tree = ryml::parse_in_arena(ryml::to_csubstr(yamlContent));
|
|
|
|
// Resolve all references (anchors and aliases)
|
|
tree->resolve();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
validationErrors << QString("Parsing error: %1").arg(e.what());
|
|
emit configError(QString("Parsing error: %1").arg(e.what()), 0, 0);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void ProcedureParser::buildReferenceCache()
|
|
{
|
|
referenceCache.clear();
|
|
|
|
if (!tree || tree->empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ryml::ConstNodeRef root = tree->rootref();
|
|
if (!root.valid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Cache all top-level collections that can be referenced according to
|
|
// schema.json
|
|
QStringList collections = {"tables", "testTaskGroups", "testActivityGroups",
|
|
"resultDisplays", "testActions", "fieldDefinitions"};
|
|
|
|
for (const QString &collection : collections)
|
|
{
|
|
std::string collectionStr = collection.toStdString();
|
|
ryml::csubstr collectionKey(collectionStr.data(), collectionStr.size());
|
|
|
|
if (root.has_child(collectionKey))
|
|
{
|
|
ryml::ConstNodeRef collectionNode = root[collectionKey];
|
|
|
|
if (collectionNode.is_map())
|
|
{
|
|
// For map-type collections, cache each child with its key
|
|
for (ryml::ConstNodeRef child : collectionNode.children())
|
|
{
|
|
if (child.has_key())
|
|
{
|
|
ryml::csubstr key = child.key();
|
|
QString keyQString = QString::fromUtf8(key.data(), key.size());
|
|
QString refPath = QString("#/%1/%2").arg(collection, keyQString);
|
|
referenceCache[refPath] = child;
|
|
|
|
qDebug() << "Cached reference:" << refPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qDebug() << "Built reference cache with" << referenceCache.size() << "entries";
|
|
}
|
|
|
|
ryml::ConstNodeRef ProcedureParser::resolveReference(const QString &ref)
|
|
{
|
|
// First check cache for performance
|
|
if (referenceCache.contains(ref))
|
|
{
|
|
return referenceCache[ref];
|
|
}
|
|
|
|
// If not in cache, try to navigate the tree manually
|
|
if (!tree || tree->empty())
|
|
{
|
|
qWarning() << "Cannot resolve reference: tree is empty";
|
|
return ryml::ConstNodeRef();
|
|
}
|
|
|
|
// Parse reference path (e.g., "#/tables/resistanceMeasurementTable")
|
|
if (!ref.startsWith("#/"))
|
|
{
|
|
qWarning() << "Invalid reference format (must start with #/):" << ref;
|
|
return ryml::ConstNodeRef();
|
|
}
|
|
|
|
QStringList parts = ref.mid(2).split('/', Qt::SkipEmptyParts);
|
|
if (parts.isEmpty())
|
|
{
|
|
qWarning() << "Invalid reference path (no components):" << ref;
|
|
return ryml::ConstNodeRef();
|
|
}
|
|
|
|
ryml::ConstNodeRef current = tree->rootref();
|
|
|
|
for (const QString &part : parts)
|
|
{
|
|
std::string partStr = part.toStdString();
|
|
ryml::csubstr partKey(partStr.data(), partStr.size());
|
|
|
|
if (!current.has_child(partKey))
|
|
{
|
|
qWarning() << "Reference path not found:" << ref << "at component:" << part;
|
|
return ryml::ConstNodeRef();
|
|
}
|
|
current = current[partKey];
|
|
}
|
|
|
|
// Cache the resolved reference for future use
|
|
referenceCache[ref] = current;
|
|
|
|
return current;
|
|
}
|
|
|
|
QString ProcedureParser::extractRefPath(const ryml::ConstNodeRef &node)
|
|
{
|
|
// Check if node has a "$ref" key (JSON-style references)
|
|
if (node.is_map() && node.has_child("$ref"))
|
|
{
|
|
ryml::ConstNodeRef refNode = node["$ref"];
|
|
if (refNode.is_val())
|
|
{
|
|
ryml::csubstr val = refNode.val();
|
|
return QString::fromUtf8(val.data(), val.size());
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
QString ProcedureParser::nodeToQString(const ryml::ConstNodeRef &node)
|
|
{
|
|
if (node.invalid() || !node.has_val())
|
|
{
|
|
return QString();
|
|
}
|
|
|
|
// Convert node value to QString
|
|
ryml::csubstr val = node.val();
|
|
return QString::fromUtf8(val.data(), val.size());
|
|
}
|
|
|
|
QVariant ProcedureParser::nodeToQVariant(const ryml::ConstNodeRef &node)
|
|
{
|
|
if (node.invalid())
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
if (node.is_map())
|
|
{
|
|
return nodeToQVariantMap(node);
|
|
}
|
|
else if (node.is_seq())
|
|
{
|
|
return nodeToQVariantList(node);
|
|
}
|
|
else if (node.is_val())
|
|
{
|
|
ryml::csubstr val = node.val();
|
|
QString str = QString::fromUtf8(val.data(), val.size());
|
|
|
|
// Try to convert to appropriate type
|
|
bool ok;
|
|
|
|
// Try integer first (most common for numeric values)
|
|
int intVal = str.toInt(&ok);
|
|
if (ok)
|
|
{
|
|
return intVal;
|
|
}
|
|
|
|
// Try double
|
|
double doubleVal = str.toDouble(&ok);
|
|
if (ok)
|
|
{
|
|
return doubleVal;
|
|
}
|
|
|
|
// Try boolean (case-insensitive)
|
|
QString lower = str.toLower();
|
|
if (lower == "true")
|
|
{
|
|
return true;
|
|
}
|
|
else if (lower == "false")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Return as string
|
|
return str;
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QVariantMap ProcedureParser::nodeToQVariantMap(const ryml::ConstNodeRef &node)
|
|
{
|
|
QVariantMap map;
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return map;
|
|
}
|
|
|
|
for (ryml::ConstNodeRef child : node.children())
|
|
{
|
|
if (child.has_key())
|
|
{
|
|
ryml::csubstr keyData = child.key();
|
|
map.insert(QString::fromUtf8(keyData.data(), keyData.size()), nodeToQVariant(child));
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
QVariantList ProcedureParser::nodeToQVariantList(const ryml::ConstNodeRef &node)
|
|
{
|
|
QVariantList list;
|
|
|
|
if (!node.is_seq())
|
|
{
|
|
return list;
|
|
}
|
|
|
|
list.reserve(node.num_children());
|
|
for (ryml::ConstNodeRef child : node.children())
|
|
{
|
|
list.append(nodeToQVariant(child));
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
FieldDefinition ProcedureParser::parseFieldDefinition(const ryml::ConstNodeRef &node)
|
|
{
|
|
#ifdef _WIN32
|
|
VMProtectBeginVirtualizationLockByKey("ProcedureParser::parseFieldDefinition");
|
|
#endif
|
|
|
|
FieldDefinition field;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return field;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseFieldDefinition(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve field definition reference:" << refPath;
|
|
return field;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return field;
|
|
}
|
|
|
|
// Parse basic properties
|
|
if (node.has_child("id"))
|
|
{
|
|
field.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
field.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("type"))
|
|
{
|
|
field.type = nodeToQString(node["type"]);
|
|
}
|
|
if (node.has_child("defaultValue"))
|
|
{
|
|
field.defaultValue = nodeToQVariant(node["defaultValue"]);
|
|
}
|
|
if (node.has_child("options"))
|
|
{
|
|
QVariantList optionsList = nodeToQVariantList(node["options"]);
|
|
for (const QVariant &opt : optionsList)
|
|
{
|
|
field.options.append(opt.toString());
|
|
}
|
|
}
|
|
if (node.has_child("formula"))
|
|
{
|
|
field.formula = nodeToQString(node["formula"]);
|
|
}
|
|
if (node.has_child("validationRules"))
|
|
{
|
|
field.validationRules = nodeToQVariantMap(node["validationRules"]);
|
|
}
|
|
if (node.has_child("isRequired"))
|
|
{
|
|
field.isRequired = nodeToQVariant(node["isRequired"]).toBool();
|
|
}
|
|
if (node.has_child("isReadOnly"))
|
|
{
|
|
field.isReadOnly = nodeToQVariant(node["isReadOnly"]).toBool();
|
|
}
|
|
if (node.has_child("unit"))
|
|
{
|
|
field.unit = nodeToQString(node["unit"]);
|
|
}
|
|
if (node.has_child("description"))
|
|
{
|
|
field.description = nodeToQString(node["description"]);
|
|
}
|
|
if (node.has_child("uploadImmediately"))
|
|
{
|
|
field.uploadImmediately = nodeToQVariant(node["uploadImmediately"]).toBool();
|
|
}
|
|
if (node.has_child("layoutConfig"))
|
|
{
|
|
field.layoutConfig = nodeToQVariantMap(node["layoutConfig"]);
|
|
}
|
|
|
|
return field;
|
|
|
|
#ifdef _WIN32
|
|
VMProtectEnd();
|
|
#endif
|
|
}
|
|
|
|
TableDefinition ProcedureParser::parseTableDefinition(const ryml::ConstNodeRef &node)
|
|
{
|
|
#ifdef _WIN32
|
|
VMProtectBeginVirtualizationLockByKey("ProcedureParser::parseTableDefinition");
|
|
#endif
|
|
|
|
TableDefinition table;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return table;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseTableDefinition(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve table definition reference:" << refPath;
|
|
return table;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return table;
|
|
}
|
|
|
|
// Parse basic properties
|
|
if (node.has_child("id"))
|
|
{
|
|
table.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
table.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("description"))
|
|
{
|
|
table.description = nodeToQString(node["description"]);
|
|
}
|
|
if (node.has_child("tableType"))
|
|
{
|
|
table.tableType = nodeToQString(node["tableType"]);
|
|
}
|
|
else
|
|
{
|
|
table.tableType = "grid"; // Default
|
|
}
|
|
if (node.has_child("layoutConfig"))
|
|
{
|
|
table.layoutConfig = nodeToQVariantMap(node["layoutConfig"]);
|
|
}
|
|
if (node.has_child("isShared"))
|
|
{
|
|
table.isShared = nodeToQVariant(node["isShared"]).toBool();
|
|
}
|
|
if (node.has_child("uploadStrategy"))
|
|
{
|
|
table.uploadStrategy = nodeToQString(node["uploadStrategy"]);
|
|
}
|
|
else
|
|
{
|
|
table.uploadStrategy = "onComplete"; // Default
|
|
}
|
|
|
|
// Parse column headers (for grid-type tables)
|
|
if (node.has_child("columnHeaders"))
|
|
{
|
|
ryml::ConstNodeRef colHeaders = node["columnHeaders"];
|
|
if (colHeaders.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef colNode : colHeaders.children())
|
|
{
|
|
if (!colNode.valid())
|
|
{
|
|
continue;
|
|
}
|
|
table.columnHeaders.append(parseFieldDefinition(colNode));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse row headers (for grid-type tables)
|
|
if (node.has_child("rowHeaders"))
|
|
{
|
|
ryml::ConstNodeRef rowHeaders = node["rowHeaders"];
|
|
if (rowHeaders.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef rowNode : rowHeaders.children())
|
|
{
|
|
if (rowNode.has_child("id") && rowNode.has_child("name"))
|
|
{
|
|
QString id = nodeToQString(rowNode["id"]);
|
|
QString name = nodeToQString(rowNode["name"]);
|
|
table.rowHeaders.append(qMakePair(id, name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse fields (for form-type and series tables)
|
|
if (node.has_child("fields"))
|
|
{
|
|
ryml::ConstNodeRef fields = node["fields"];
|
|
if (fields.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef fieldNode : fields.children())
|
|
{
|
|
if (!fieldNode.valid())
|
|
{
|
|
continue;
|
|
}
|
|
table.fields.append(parseFieldDefinition(fieldNode));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse static cells
|
|
if (node.has_child("staticCells"))
|
|
{
|
|
ryml::ConstNodeRef staticCells = node["staticCells"];
|
|
if (staticCells.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef cellNode : staticCells.children())
|
|
{
|
|
if (!cellNode.valid())
|
|
{
|
|
continue;
|
|
}
|
|
table.staticCells.append(parseStaticCell(cellNode));
|
|
}
|
|
}
|
|
}
|
|
|
|
return table;
|
|
|
|
#ifdef _WIN32
|
|
VMProtectEnd();
|
|
#endif
|
|
}
|
|
|
|
StaticCell ProcedureParser::parseStaticCell(const ryml::ConstNodeRef &node)
|
|
{
|
|
StaticCell cell;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return cell;
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return cell;
|
|
}
|
|
|
|
// Parse field ID (for form-type tables)
|
|
if (node.has_child("field"))
|
|
{
|
|
cell.field = nodeToQString(node["field"]);
|
|
}
|
|
|
|
// Parse row and column (for grid-type tables)
|
|
if (node.has_child("row"))
|
|
{
|
|
QString rowStr = nodeToQString(node["row"]);
|
|
cell.row = rowStr.toInt();
|
|
}
|
|
if (node.has_child("column"))
|
|
{
|
|
QString colStr = nodeToQString(node["column"]);
|
|
cell.column = colStr.toInt();
|
|
}
|
|
|
|
// Parse content
|
|
if (node.has_child("content"))
|
|
{
|
|
cell.content = nodeToQString(node["content"]);
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
FieldSelector ProcedureParser::parseFieldSelector(const ryml::ConstNodeRef &node)
|
|
{
|
|
#ifdef _WIN32
|
|
VMProtectBeginVirtualizationLockByKey("ProcedureParser::parseFieldSelector");
|
|
#endif
|
|
|
|
FieldSelector selector;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return selector;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseFieldSelector(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve field selector reference:" << refPath;
|
|
return selector;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return selector;
|
|
}
|
|
|
|
if (node.has_child("tableRef"))
|
|
{
|
|
selector.tableRef = nodeToQString(node["tableRef"]);
|
|
}
|
|
|
|
if (node.has_child("fields"))
|
|
{
|
|
QVariantList fieldsList = nodeToQVariantList(node["fields"]);
|
|
for (const QVariant &field : fieldsList)
|
|
{
|
|
selector.fields.append(field.toString());
|
|
}
|
|
}
|
|
|
|
if (node.has_child("cells"))
|
|
{
|
|
ryml::ConstNodeRef cells = node["cells"];
|
|
if (cells.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef cellNode : cells.children())
|
|
{
|
|
if (cellNode.has_child("row") && cellNode.has_child("column"))
|
|
{
|
|
QString row = nodeToQString(cellNode["row"]);
|
|
QString column = nodeToQString(cellNode["column"]);
|
|
selector.cells.append(qMakePair(row, column));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.has_child("ignore"))
|
|
{
|
|
selector.ignore = nodeToQVariant(node["ignore"]).toBool();
|
|
}
|
|
|
|
return selector;
|
|
|
|
#ifdef _WIN32
|
|
VMProtectEnd();
|
|
#endif
|
|
}
|
|
|
|
TestAction ProcedureParser::parseTestAction(const ryml::ConstNodeRef &node)
|
|
{
|
|
TestAction action;
|
|
|
|
if (node.invalid())
|
|
{
|
|
qWarning() << "parseTestAction: invalid node";
|
|
return action;
|
|
}
|
|
|
|
qDebug() << "parseTestAction: node is_map:" << node.is_map() << "is_val:" << node.is_val()
|
|
<< "is_ref:" << node.is_ref() << "has_val:" << node.has_val();
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
qDebug() << "parseTestAction: resolving reference:" << refPath;
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseTestAction(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve test action reference:" << refPath;
|
|
return action;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
qWarning() << "parseTestAction: node is not a map, cannot parse";
|
|
return action;
|
|
}
|
|
|
|
if (node.has_child("id"))
|
|
{
|
|
action.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
action.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("document"))
|
|
{
|
|
action.document = nodeToQString(node["document"]);
|
|
}
|
|
if (node.has_child("mode"))
|
|
{
|
|
action.mode = nodeToQString(node["mode"]);
|
|
}
|
|
if (node.has_child("sequence"))
|
|
{
|
|
action.sequence = nodeToQString(node["sequence"]);
|
|
}
|
|
if (node.has_child("functionType"))
|
|
{
|
|
action.functionType = nodeToQString(node["functionType"]);
|
|
}
|
|
if (node.has_child("channel"))
|
|
{
|
|
action.channel = nodeToQString(node["channel"]);
|
|
}
|
|
if (node.has_child("functionParameters"))
|
|
{
|
|
// functionParameters can be object or array per schema
|
|
action.functionParameters = nodeToQVariant(node["functionParameters"]);
|
|
}
|
|
if (node.has_child("metadata"))
|
|
{
|
|
action.metadata = nodeToQVariantMap(node["metadata"]);
|
|
}
|
|
if (node.has_child("dataFields"))
|
|
{
|
|
ryml::ConstNodeRef dataFields = node["dataFields"];
|
|
if (dataFields.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef fieldNode : dataFields.children())
|
|
{
|
|
action.dataFields.append(parseFieldSelector(fieldNode));
|
|
}
|
|
}
|
|
}
|
|
if (node.has_child("validationCriteria"))
|
|
{
|
|
action.validationCriteria = nodeToQVariantMap(node["validationCriteria"]);
|
|
}
|
|
if (node.has_child("uploadStrategy"))
|
|
{
|
|
action.uploadStrategy = nodeToQString(node["uploadStrategy"]);
|
|
}
|
|
else
|
|
{
|
|
action.uploadStrategy = "inherit"; // Default
|
|
}
|
|
if (node.has_child("uploadFields"))
|
|
{
|
|
QVariantList fields = nodeToQVariantList(node["uploadFields"]);
|
|
for (const QVariant &field : fields)
|
|
{
|
|
action.uploadFields.append(field.toString());
|
|
}
|
|
}
|
|
|
|
return action;
|
|
}
|
|
|
|
TestActivityGroup ProcedureParser::parseTestActivityGroup(const ryml::ConstNodeRef &node)
|
|
{
|
|
TestActivityGroup group;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return group;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseTestActivityGroup(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve test activity group reference:" << refPath;
|
|
return group;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return group;
|
|
}
|
|
|
|
if (node.has_child("id"))
|
|
{
|
|
group.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
group.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("document"))
|
|
{
|
|
group.document = nodeToQString(node["document"]);
|
|
}
|
|
if (node.has_child("metadata"))
|
|
{
|
|
group.metadata = nodeToQVariantMap(node["metadata"]);
|
|
}
|
|
if (node.has_child("tableRefs"))
|
|
{
|
|
QVariantList refs = nodeToQVariantList(node["tableRefs"]);
|
|
for (const QVariant &ref : refs)
|
|
{
|
|
group.tableRefs.append(ref.toString());
|
|
}
|
|
}
|
|
if (node.has_child("actions"))
|
|
{
|
|
ryml::ConstNodeRef actions = node["actions"];
|
|
qDebug() << "Parsing actions for TestActivityGroup:" << group.name;
|
|
qDebug() << " actions node is_seq:" << actions.is_seq() << "is_map:" << actions.is_map()
|
|
<< "is_val:" << actions.is_val();
|
|
|
|
if (actions.is_seq())
|
|
{
|
|
qDebug() << " actions sequence has" << actions.num_children() << "children";
|
|
|
|
int childIndex = 0;
|
|
for (ryml::ConstNodeRef actionNode : actions.children())
|
|
{
|
|
qDebug() << " Processing child" << childIndex << "- valid:" << actionNode.valid()
|
|
<< "is_map:" << actionNode.is_map() << "is_val:" << actionNode.is_val();
|
|
|
|
if (!actionNode.valid())
|
|
{
|
|
qWarning() << " Invalid action node at index" << childIndex;
|
|
childIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a YAML alias (reference)
|
|
if (actionNode.is_ref())
|
|
{
|
|
qDebug() << " Child" << childIndex << "is a YAML reference/alias";
|
|
}
|
|
|
|
TestAction action = parseTestAction(actionNode);
|
|
qDebug() << " Parsed action" << childIndex << ":" << action.name << "(" << action.id
|
|
<< ")";
|
|
group.actions.append(action);
|
|
childIndex++;
|
|
}
|
|
qDebug() << " Total actions parsed:" << group.actions.size();
|
|
}
|
|
else
|
|
{
|
|
qWarning() << " actions node is not a sequence!";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qWarning() << " TestActivityGroup has no 'actions' child";
|
|
|
|
// Debug: list all children of this node
|
|
qDebug() << " Node has" << node.num_children() << "children:";
|
|
for (ryml::ConstNodeRef child : node.children())
|
|
{
|
|
if (child.has_key())
|
|
{
|
|
ryml::csubstr key = child.key();
|
|
QString keyStr = QString::fromUtf8(key.data(), key.size());
|
|
qDebug() << " -" << keyStr;
|
|
}
|
|
}
|
|
}
|
|
|
|
qDebug() << "TestActivityGroup" << group.name << "final actions count:" << group.actions.size();
|
|
return group;
|
|
}
|
|
|
|
TestTaskGroup ProcedureParser::parseTestTaskGroup(const ryml::ConstNodeRef &node)
|
|
{
|
|
TestTaskGroup group;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return group;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseTestTaskGroup(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve test task group reference:" << refPath;
|
|
return group;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return group;
|
|
}
|
|
|
|
if (node.has_child("id"))
|
|
{
|
|
group.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
group.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("metadata"))
|
|
{
|
|
group.metadata = nodeToQVariantMap(node["metadata"]);
|
|
}
|
|
if (node.has_child("stages"))
|
|
{
|
|
ryml::ConstNodeRef stages = node["stages"];
|
|
qDebug() << "Stage 1: Parsing stage metadata for TestTaskGroup:" << group.name;
|
|
qDebug() << " stages node is_seq:" << stages.is_seq()
|
|
<< "num_children:" << stages.num_children();
|
|
|
|
if (stages.is_seq())
|
|
{
|
|
int stageIndex = 0;
|
|
for (ryml::ConstNodeRef stageNode : stages.children())
|
|
{
|
|
qDebug() << " Processing stage" << stageIndex << "- valid:" << stageNode.valid()
|
|
<< "is_map:" << stageNode.is_map() << "is_ref:" << stageNode.is_ref();
|
|
|
|
if (!stageNode.valid())
|
|
{
|
|
qWarning() << " Invalid stage node at index" << stageIndex;
|
|
stageIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Resolve the node if it's a reference
|
|
ryml::ConstNodeRef resolvedStage = stageNode;
|
|
QString refPath = extractRefPath(stageNode);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
qDebug() << " Stage" << stageIndex << "is a $ref reference:" << refPath;
|
|
resolvedStage = resolveReference(refPath);
|
|
if (resolvedStage.invalid())
|
|
{
|
|
qWarning() << " Failed to resolve stage reference:" << refPath;
|
|
stageIndex++;
|
|
continue;
|
|
}
|
|
}
|
|
else if (stageNode.is_ref())
|
|
{
|
|
qDebug() << " Stage" << stageIndex << "is a YAML alias (will be auto-resolved)";
|
|
}
|
|
|
|
// Stage 1: Parse only metadata (id, name, document, tableRefs)
|
|
// Do NOT parse actions yet
|
|
TestActivityGroup activityGroup;
|
|
|
|
if (resolvedStage.has_child("id"))
|
|
{
|
|
activityGroup.id = nodeToQString(resolvedStage["id"]);
|
|
}
|
|
if (resolvedStage.has_child("name"))
|
|
{
|
|
activityGroup.name = nodeToQString(resolvedStage["name"]);
|
|
}
|
|
if (resolvedStage.has_child("document"))
|
|
{
|
|
activityGroup.document = nodeToQString(resolvedStage["document"]);
|
|
}
|
|
if (resolvedStage.has_child("metadata"))
|
|
{
|
|
activityGroup.metadata = nodeToQVariantMap(resolvedStage["metadata"]);
|
|
}
|
|
if (resolvedStage.has_child("tableRefs"))
|
|
{
|
|
QVariantList refs = nodeToQVariantList(resolvedStage["tableRefs"]);
|
|
for (const QVariant &ref : refs)
|
|
{
|
|
activityGroup.tableRefs.append(ref.toString());
|
|
}
|
|
}
|
|
|
|
// Mark actions as not parsed yet
|
|
activityGroup.isActionsParsed = false;
|
|
|
|
// Cache the node for later action parsing (Stage 2)
|
|
stageNodeCache[activityGroup.id] = resolvedStage;
|
|
|
|
qDebug() << " Stage" << stageIndex << "metadata parsed:" << activityGroup.name
|
|
<< "(actions deferred)";
|
|
group.stages.append(activityGroup);
|
|
stageIndex++;
|
|
}
|
|
qDebug() << " Total stages metadata parsed:" << group.stages.size();
|
|
}
|
|
else
|
|
{
|
|
qWarning() << " stages node is not a sequence!";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qWarning() << " TestTaskGroup has no 'stages' child";
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
ResultDisplay ProcedureParser::parseResultDisplay(const ryml::ConstNodeRef &node)
|
|
{
|
|
ResultDisplay display;
|
|
|
|
if (node.invalid())
|
|
{
|
|
return display;
|
|
}
|
|
|
|
// Handle JSON $ref references
|
|
QString refPath = extractRefPath(node);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
ryml::ConstNodeRef resolvedNode = resolveReference(refPath);
|
|
if (!resolvedNode.invalid())
|
|
{
|
|
return parseResultDisplay(resolvedNode);
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Failed to resolve result display reference:" << refPath;
|
|
return display;
|
|
}
|
|
}
|
|
|
|
if (!node.is_map())
|
|
{
|
|
return display;
|
|
}
|
|
|
|
if (node.has_child("id"))
|
|
{
|
|
display.id = nodeToQString(node["id"]);
|
|
}
|
|
if (node.has_child("name"))
|
|
{
|
|
display.name = nodeToQString(node["name"]);
|
|
}
|
|
if (node.has_child("document"))
|
|
{
|
|
display.document = nodeToQString(node["document"]);
|
|
}
|
|
if (node.has_child("tableRefs"))
|
|
{
|
|
QVariantList refs = nodeToQVariantList(node["tableRefs"]);
|
|
for (const QVariant &ref : refs)
|
|
{
|
|
display.tableRefs.append(ref.toString());
|
|
}
|
|
}
|
|
|
|
return display;
|
|
}
|
|
|
|
// Stage 2: Parse TestActivityGroup actions on demand
|
|
bool ProcedureParser::parseTestActivityGroupActions(TestActivityGroup &group,
|
|
const QString &groupId)
|
|
{
|
|
// Already parsed
|
|
if (group.isActionsParsed)
|
|
{
|
|
qDebug() << "Actions already parsed for TestActivityGroup:" << group.name;
|
|
return true;
|
|
}
|
|
|
|
// Check if node is cached
|
|
if (!stageNodeCache.contains(groupId))
|
|
{
|
|
qWarning() << "Stage node not found in cache for id:" << groupId;
|
|
qDebug() << "Available cached stage IDs:" << stageNodeCache.keys();
|
|
return false;
|
|
}
|
|
|
|
ryml::ConstNodeRef node = stageNodeCache[groupId];
|
|
|
|
qDebug() << "Stage 2: Parsing actions for TestActivityGroup:" << group.name;
|
|
|
|
// Parse actions
|
|
if (node.has_child("actions"))
|
|
{
|
|
ryml::ConstNodeRef actions = node["actions"];
|
|
qDebug() << " actions node is_seq:" << actions.is_seq() << "is_map:" << actions.is_map()
|
|
<< "is_val:" << actions.is_val();
|
|
|
|
if (actions.is_seq())
|
|
{
|
|
qDebug() << " actions sequence has" << actions.num_children() << "children";
|
|
|
|
int childIndex = 0;
|
|
for (ryml::ConstNodeRef actionNode : actions.children())
|
|
{
|
|
qDebug() << " Processing child" << childIndex << "- valid:" << actionNode.valid()
|
|
<< "is_map:" << actionNode.is_map() << "is_val:" << actionNode.is_val();
|
|
|
|
if (!actionNode.valid())
|
|
{
|
|
qWarning() << " Invalid action node at index" << childIndex;
|
|
childIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a YAML alias (reference)
|
|
if (actionNode.is_ref())
|
|
{
|
|
qDebug() << " Child" << childIndex << "is a YAML reference/alias";
|
|
}
|
|
|
|
TestAction action = parseTestAction(actionNode);
|
|
qDebug() << " Parsed action" << childIndex << ":" << action.name << "(" << action.id
|
|
<< ")";
|
|
group.actions.append(action);
|
|
childIndex++;
|
|
}
|
|
qDebug() << " Total actions parsed:" << group.actions.size();
|
|
}
|
|
else
|
|
{
|
|
qWarning() << " actions node is not a sequence!";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << " TestActivityGroup has no 'actions' child (empty stage)";
|
|
}
|
|
|
|
group.isActionsParsed = true;
|
|
qDebug() << "TestActivityGroup" << group.name
|
|
<< "actions parsing completed:" << group.actions.size();
|
|
return true;
|
|
}
|
|
|
|
ProcedureConfig ProcedureParser::parseProcedureConfig()
|
|
{
|
|
ProcedureConfig config;
|
|
|
|
if (!tree || tree->empty())
|
|
{
|
|
validationErrors << "Tree is empty or not loaded";
|
|
return config;
|
|
}
|
|
|
|
ryml::ConstNodeRef root = tree->rootref();
|
|
|
|
// Parse procedure section
|
|
if (root.has_child("procedure"))
|
|
{
|
|
ryml::ConstNodeRef procedure = root["procedure"];
|
|
|
|
if (procedure.has_child("id"))
|
|
{
|
|
config.procedureId = nodeToQString(procedure["id"]);
|
|
}
|
|
if (procedure.has_child("name"))
|
|
{
|
|
config.procedureName = nodeToQString(procedure["name"]);
|
|
}
|
|
if (procedure.has_child("version"))
|
|
{
|
|
config.version = nodeToQString(procedure["version"]);
|
|
}
|
|
if (procedure.has_child("document"))
|
|
{
|
|
config.document = nodeToQString(procedure["document"]);
|
|
}
|
|
if (procedure.has_child("metadata"))
|
|
{
|
|
config.metadata = nodeToQVariantMap(procedure["metadata"]);
|
|
}
|
|
|
|
// Parse activity sequence (LAZY LOADING - only parse metadata)
|
|
if (procedure.has_child("activitySequence"))
|
|
{
|
|
ryml::ConstNodeRef activitySeq = procedure["activitySequence"];
|
|
if (activitySeq.is_seq())
|
|
{
|
|
for (ryml::ConstNodeRef activityNode : activitySeq.children())
|
|
{
|
|
qDebug() << "Processing activity node - valid:" << activityNode.valid()
|
|
<< "is_map:" << activityNode.is_map() << "is_ref:" << activityNode.is_ref()
|
|
<< "is_val:" << activityNode.is_val();
|
|
|
|
if (!activityNode.valid())
|
|
{
|
|
qWarning() << "Invalid activity node in sequence";
|
|
continue;
|
|
}
|
|
|
|
// Resolve the node if it's a reference
|
|
ryml::ConstNodeRef resolvedNode = activityNode;
|
|
QString refPath = extractRefPath(activityNode);
|
|
|
|
// Handle YAML aliases (references like *protectionGroup1)
|
|
if (activityNode.is_ref())
|
|
{
|
|
qDebug() << "Activity node is a YAML alias (will be auto-resolved "
|
|
"by ryml)";
|
|
// For YAML aliases, ryml should auto-resolve them, so use
|
|
// activityNode directly
|
|
resolvedNode = activityNode;
|
|
}
|
|
else if (!refPath.isEmpty())
|
|
{
|
|
qDebug() << "Activity node has JSON $ref:" << refPath;
|
|
resolvedNode = resolveReference(refPath);
|
|
if (resolvedNode.invalid())
|
|
{
|
|
qWarning() << "Failed to resolve activity reference:" << refPath;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!resolvedNode.is_map())
|
|
{
|
|
qWarning() << "Resolved activity node is not a map";
|
|
continue;
|
|
}
|
|
|
|
// Debug: check what fields the resolved node has
|
|
qDebug() << "Resolved node has" << resolvedNode.num_children() << "children:";
|
|
for (ryml::ConstNodeRef child : resolvedNode.children())
|
|
{
|
|
if (child.has_key())
|
|
{
|
|
ryml::csubstr key = child.key();
|
|
QString keyStr = QString::fromUtf8(key.data(), key.size());
|
|
qDebug() << " -" << keyStr;
|
|
}
|
|
}
|
|
|
|
// Determine activity type: only TestTaskGroup or ResultDisplay
|
|
// allowed
|
|
bool hasStages = resolvedNode.has_child("stages");
|
|
bool hasTableRefs = resolvedNode.has_child("tableRefs");
|
|
qDebug() << "Type detection - refPath contains testTaskGroups:"
|
|
<< refPath.contains("testTaskGroups") << "hasStages:" << hasStages
|
|
<< "refPath contains resultDisplays:" << refPath.contains("resultDisplays")
|
|
<< "hasTableRefs:" << hasTableRefs;
|
|
|
|
if (refPath.contains("testTaskGroups") || hasStages)
|
|
{
|
|
// TestTaskGroup - LAZY: only parse id, name, metadata (NOT stages)
|
|
TestTaskGroup group;
|
|
if (resolvedNode.has_child("id"))
|
|
{
|
|
group.id = nodeToQString(resolvedNode["id"]);
|
|
}
|
|
if (resolvedNode.has_child("name"))
|
|
{
|
|
group.name = nodeToQString(resolvedNode["name"]);
|
|
}
|
|
if (resolvedNode.has_child("metadata"))
|
|
{
|
|
group.metadata = nodeToQVariantMap(resolvedNode["metadata"]);
|
|
}
|
|
group.isParsed = false; // Mark as not fully parsed
|
|
|
|
// Cache the node for later parsing
|
|
activityNodeCache[group.id] = resolvedNode;
|
|
|
|
config.activityVariants.append(QVariant::fromValue(group));
|
|
|
|
qDebug() << "Lazy loaded TestTaskGroup metadata:" << group.id << group.name;
|
|
}
|
|
else if (refPath.contains("resultDisplays") || resolvedNode.has_child("tableRefs"))
|
|
{
|
|
// ResultDisplay - parse fully (lightweight)
|
|
config.activityVariants.append(QVariant::fromValue(parseResultDisplay(resolvedNode)));
|
|
}
|
|
else
|
|
{
|
|
QString nodeId =
|
|
resolvedNode.has_child("id") ? nodeToQString(resolvedNode["id"]) : "<unknown>";
|
|
qWarning() << "Unknown activity type in sequence, id:" << nodeId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert activitySequence to direct storage for efficient access
|
|
qDebug() << "Converting" << config.activityVariants.size()
|
|
<< "activities from QVariant to direct storage";
|
|
for (int i = 0; i < config.activityVariants.size(); ++i)
|
|
{
|
|
const QVariant &activityVariant = config.activityVariants[i];
|
|
qDebug() << "Activity" << i << "- type:" << activityVariant.typeName()
|
|
<< "canConvert<TestTaskGroup>:" << activityVariant.canConvert<TestTaskGroup>()
|
|
<< "canConvert<ResultDisplay>:" << activityVariant.canConvert<ResultDisplay>();
|
|
|
|
if (activityVariant.canConvert<TestTaskGroup>())
|
|
{
|
|
TestTaskGroup group = activityVariant.value<TestTaskGroup>();
|
|
config.taskGroups.append(group);
|
|
qDebug() << "Added TestTaskGroup to direct storage:" << group.id << group.name;
|
|
}
|
|
else if (activityVariant.canConvert<ResultDisplay>())
|
|
{
|
|
ResultDisplay display = activityVariant.value<ResultDisplay>();
|
|
config.resultDisplays[display.id] = display;
|
|
qDebug() << "Added ResultDisplay to direct storage:" << display.id << display.name;
|
|
}
|
|
else
|
|
{
|
|
qWarning() << "Activity" << i
|
|
<< "cannot be converted to known type, typeName:" << activityVariant.typeName();
|
|
}
|
|
}
|
|
|
|
// Parse tables
|
|
if (root.has_child("tables"))
|
|
{
|
|
ryml::ConstNodeRef tables = root["tables"];
|
|
if (tables.is_map())
|
|
{
|
|
for (ryml::ConstNodeRef tableNode : tables.children())
|
|
{
|
|
if (tableNode.has_key())
|
|
{
|
|
// Convert key to QString
|
|
ryml::csubstr keyData = tableNode.key();
|
|
QString tableId = QString::fromUtf8(keyData.data(), keyData.size());
|
|
config.tables[tableId] = parseTableDefinition(tableNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
qDebug() << "Parsed procedure config:" << config.procedureId << "with" << config.taskGroups.size()
|
|
<< "task groups"
|
|
<< "and" << config.resultDisplays.size() << "result displays";
|
|
|
|
return config;
|
|
}
|
|
|
|
TableDefinition ProcedureParser::getTableDefinition(const QString &tableRef)
|
|
{
|
|
ryml::ConstNodeRef tableNode = resolveReference(tableRef);
|
|
if (!tableNode.invalid())
|
|
{
|
|
return parseTableDefinition(tableNode);
|
|
}
|
|
|
|
return TableDefinition();
|
|
}
|
|
|
|
QMap<QString, TableDefinition> ProcedureParser::getAllTableDefinitions()
|
|
{
|
|
QMap<QString, TableDefinition> tables;
|
|
|
|
if (!tree || tree->empty())
|
|
{
|
|
return tables;
|
|
}
|
|
|
|
ryml::ConstNodeRef root = tree->rootref();
|
|
if (root.has_child("tables"))
|
|
{
|
|
ryml::ConstNodeRef tablesNode = root["tables"];
|
|
if (tablesNode.is_map())
|
|
{
|
|
for (ryml::ConstNodeRef tableNode : tablesNode.children())
|
|
{
|
|
if (tableNode.has_key())
|
|
{
|
|
// Convert key to QString
|
|
ryml::csubstr keyData = tableNode.key();
|
|
QString tableId = QString::fromUtf8(keyData.data(), keyData.size());
|
|
tables[tableId] = parseTableDefinition(tableNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return tables;
|
|
}
|
|
|
|
bool ProcedureParser::parseTestTaskGroupStages(TestTaskGroup &group, const QString &groupId)
|
|
{
|
|
// Check if already parsed
|
|
if (group.isParsed)
|
|
{
|
|
qDebug() << "TestTaskGroup" << groupId << "already parsed, skipping";
|
|
return true;
|
|
}
|
|
|
|
// Find cached node
|
|
if (!activityNodeCache.contains(groupId))
|
|
{
|
|
qWarning() << "No cached node found for TestTaskGroup:" << groupId;
|
|
return false;
|
|
}
|
|
|
|
ryml::ConstNodeRef node = activityNodeCache[groupId];
|
|
|
|
qDebug() << "Lazy parsing stages for TestTaskGroup:" << groupId;
|
|
|
|
// Parse stages
|
|
if (node.has_child("stages"))
|
|
{
|
|
ryml::ConstNodeRef stages = node["stages"];
|
|
qDebug() << " Parsing stages - is_seq:" << stages.is_seq()
|
|
<< "num_children:" << stages.num_children();
|
|
|
|
if (stages.is_seq())
|
|
{
|
|
group.stages.clear();
|
|
group.stages.reserve(stages.num_children());
|
|
|
|
int stageIndex = 0;
|
|
for (ryml::ConstNodeRef stageNode : stages.children())
|
|
{
|
|
if (!stageNode.valid())
|
|
{
|
|
qWarning() << " Invalid stage node at index" << stageIndex;
|
|
stageIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Resolve reference if needed
|
|
ryml::ConstNodeRef resolvedStage = stageNode;
|
|
QString refPath = extractRefPath(stageNode);
|
|
if (!refPath.isEmpty())
|
|
{
|
|
resolvedStage = resolveReference(refPath);
|
|
if (resolvedStage.invalid())
|
|
{
|
|
qWarning() << " Failed to resolve stage reference:" << refPath;
|
|
stageIndex++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Parse TestActivityGroup
|
|
TestActivityGroup activityGroup = parseTestActivityGroup(resolvedStage);
|
|
qDebug() << " Parsed stage" << stageIndex << ":" << activityGroup.name << "with"
|
|
<< activityGroup.actions.size() << "actions";
|
|
group.stages.append(activityGroup);
|
|
stageIndex++;
|
|
}
|
|
|
|
qDebug() << " Total stages parsed:" << group.stages.size();
|
|
}
|
|
}
|
|
|
|
group.isParsed = true;
|
|
return true;
|
|
}
|
|
|
|
bool ProcedureParser::validateConfig()
|
|
{
|
|
validationErrors.clear();
|
|
|
|
if (!tree || tree->empty())
|
|
{
|
|
validationErrors << "Configuration tree is empty";
|
|
return false;
|
|
}
|
|
|
|
ryml::ConstNodeRef root = tree->rootref();
|
|
|
|
// Validate required top-level sections
|
|
if (!root.has_child("procedure"))
|
|
{
|
|
validationErrors << "Missing required 'procedure' section";
|
|
}
|
|
if (!root.has_child("tables"))
|
|
{
|
|
validationErrors << "Missing required 'tables' section";
|
|
}
|
|
|
|
// Validate procedure section
|
|
if (root.has_child("procedure"))
|
|
{
|
|
ryml::ConstNodeRef procedure = root["procedure"];
|
|
|
|
if (!procedure.has_child("id"))
|
|
{
|
|
validationErrors << "Procedure missing required 'id' field";
|
|
}
|
|
if (!procedure.has_child("name"))
|
|
{
|
|
validationErrors << "Procedure missing required 'name' field";
|
|
}
|
|
if (!procedure.has_child("version"))
|
|
{
|
|
validationErrors << "Procedure missing required 'version' field";
|
|
}
|
|
if (!procedure.has_child("activitySequence"))
|
|
{
|
|
validationErrors << "Procedure missing required 'activitySequence' field";
|
|
}
|
|
else
|
|
{
|
|
if (!validateActivitySequence())
|
|
{
|
|
validationErrors << "Activity sequence validation failed";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate table definitions
|
|
if (!validateTableDefinitions())
|
|
{
|
|
validationErrors << "Table definitions validation failed";
|
|
}
|
|
|
|
return validationErrors.isEmpty();
|
|
}
|
|
|
|
bool ProcedureParser::validateActivitySequence()
|
|
{
|
|
// Placeholder for activity sequence validation
|
|
// In a full implementation, this would check:
|
|
// - All references are valid
|
|
// - Metadata inheritance rules are followed
|
|
// - Required fields are present
|
|
return true;
|
|
}
|
|
|
|
bool ProcedureParser::validateTableDefinitions()
|
|
{
|
|
// Placeholder for table definitions validation
|
|
// In a full implementation, this would check:
|
|
// - All table references are valid
|
|
// - Field definitions are complete
|
|
// - Upload strategies are valid
|
|
return true;
|
|
}
|
|
|
|
QStringList ProcedureParser::getValidationErrors() const
|
|
{
|
|
return validationErrors;
|
|
}
|
|
|
|
// Register custom types with Qt's meta-object system
|
|
namespace
|
|
{
|
|
struct TypeRegistrar
|
|
{
|
|
TypeRegistrar()
|
|
{
|
|
qRegisterMetaType<FieldDefinition>("FieldDefinition");
|
|
qRegisterMetaType<StaticCell>("StaticCell");
|
|
qRegisterMetaType<TableDefinition>("TableDefinition");
|
|
qRegisterMetaType<FieldSelector>("FieldSelector");
|
|
qRegisterMetaType<TestAction>("TestAction");
|
|
qRegisterMetaType<TestActivityGroup>("TestActivityGroup");
|
|
qRegisterMetaType<TestTaskGroup>("TestTaskGroup");
|
|
qRegisterMetaType<ResultDisplay>("ResultDisplay");
|
|
qRegisterMetaType<ProcedureConfig>("ProcedureConfig");
|
|
}
|
|
};
|
|
|
|
static TypeRegistrar typeRegistrar;
|
|
} // namespace
|