#include "procedureparser.h" #include #include #include #include #include ProcedureParser::ProcedureParser(QObject *parent) : QObject(parent), tree(std::make_unique()) {} 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"]) : ""; 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:" << activityVariant.canConvert() << "canConvert:" << activityVariant.canConvert(); if (activityVariant.canConvert()) { TestTaskGroup group = activityVariant.value(); config.taskGroups.append(group); qDebug() << "Added TestTaskGroup to direct storage:" << group.id << group.name; } else if (activityVariant.canConvert()) { ResultDisplay display = activityVariant.value(); 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 ProcedureParser::getAllTableDefinitions() { QMap 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"); qRegisterMetaType("StaticCell"); qRegisterMetaType("TableDefinition"); qRegisterMetaType("FieldSelector"); qRegisterMetaType("TestAction"); qRegisterMetaType("TestActivityGroup"); qRegisterMetaType("TestTaskGroup"); qRegisterMetaType("ResultDisplay"); qRegisterMetaType("ProcedureConfig"); } }; static TypeRegistrar typeRegistrar; } // namespace