Files
CalibratorLauncher/procedure/procedureparser.cpp
2026-01-02 19:20:35 +09:00

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