Pre-walks a v2 'root' subtree to validate structural shape and collect per-window ids + referenced group labels. Throws on missing required field, type mismatch on a known field, or duplicate window id. Does not mutate engine state. 'seedingOrder' captures (bareWindowId, groupName) pairs in pre-order traversal order (splits' children walked in array order), used by ApplyLayout's group-seeding pas
| 91 | // the cell that appears first in document order whose links.selection |
| 92 | // names that group. |
| 93 | void PrewalkValidate(const nlohmann::json& node, |
| 94 | std::set<std::string>& seenIds, |
| 95 | std::set<std::string>& referencedGroups, |
| 96 | std::vector<std::pair<std::string, std::string>>& seedingOrder) |
| 97 | { |
| 98 | if (!node.is_object() || !node.contains("type") || !node["type"].is_string()) |
| 99 | { |
| 100 | mitkThrow() << "Layout node is missing the 'type' string field."; |
| 101 | } |
| 102 | const auto type = node["type"].get<std::string>(); |
| 103 | |
| 104 | if (type == "split") |
| 105 | { |
| 106 | if (!node.contains("orientation") || !node["orientation"].is_string()) |
| 107 | { |
| 108 | mitkThrow() << "Layout split node is missing the 'orientation' field."; |
| 109 | } |
| 110 | const auto orientation = node["orientation"].get<std::string>(); |
| 111 | if (orientation != "horizontal" && orientation != "vertical") |
| 112 | { |
| 113 | mitkThrow() << "Layout split node has invalid orientation '" << orientation |
| 114 | << "' (expected 'horizontal' or 'vertical')."; |
| 115 | } |
| 116 | if (!node.contains("children") || !node["children"].is_array() || node["children"].empty()) |
| 117 | { |
| 118 | mitkThrow() << "Layout split node is missing a non-empty 'children' array."; |
| 119 | } |
| 120 | for (const auto& child : node["children"]) |
| 121 | { |
| 122 | PrewalkValidate(child, seenIds, referencedGroups, seedingOrder); |
| 123 | } |
| 124 | } |
| 125 | else if (type == "window") |
| 126 | { |
| 127 | if (!node.contains("id") || !node["id"].is_string()) |
| 128 | { |
| 129 | mitkThrow() << "Layout window node is missing the 'id' string field."; |
| 130 | } |
| 131 | const auto id = node["id"].get<std::string>(); |
| 132 | if (id.empty()) |
| 133 | { |
| 134 | mitkThrow() << "Layout window node has an empty 'id'."; |
| 135 | } |
| 136 | if (!WINDOW_ID_PATTERN.match(QString::fromStdString(id)).hasMatch()) |
| 137 | { |
| 138 | mitkThrow() << "Layout window id '" << id |
| 139 | << "' does not match the required pattern '" |
| 140 | << WINDOW_ID_PATTERN.pattern().toStdString() |
| 141 | << "' (URL-segment-safe, qualified `<editor_name>__<bare_id>`)."; |
| 142 | } |
| 143 | if (!seenIds.insert(id).second) |
| 144 | { |
| 145 | mitkThrow() << "Layout document contains duplicate window id '" << id << "'."; |
| 146 | } |
| 147 | // Optional display label: free-form, not unique. If present, must be |
| 148 | // a non-empty string. Anything else (wrong type, empty string) throws. |
| 149 | if (node.contains("name")) |
| 150 | { |
no test coverage detected