|
- #include "json_project_storage.h"
-
- #include <QFile>
- #include <QJsonArray>
- #include <QJsonDocument>
- #include <QJsonObject>
- #include <QJsonParseError>
- #include <QSaveFile>
-
- #include <cmath>
- #include <limits>
- #include <string>
- #include <utility>
-
- namespace {
-
- // 当前读写实现支持的工程文件格式版本
- constexpr const char *kCurrentFormatVersion = "1.0";
-
- // 保存解析过程中遇到的第一个错误,避免后续失败覆盖根因
- struct ParseState
- {
- ProjectStorageError error = ProjectStorageError::None;
- std::string message;
-
- // 记录首次解析错误并返回 false,便于解析函数直接向上传播失败
- bool fail(ProjectStorageError new_error, const std::string &new_message)
- {
- if (error == ProjectStorageError::None)
- {
- error = new_error;
- message = new_message;
- }
- return false;
- }
- };
-
- // 将领域层使用的 UTF-8 字符串转换为 Qt 字符串
- QString fromUtf8(const std::string &value)
- {
- return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
- }
-
- // 将 Qt 字符串转换为领域层使用的 UTF-8 字符串
- std::string toUtf8(const QString &value)
- {
- const QByteArray bytes = value.toUtf8();
- return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
- }
-
- // 拼接带上下文的字段路径,用于生成可定位的错误信息
- std::string fieldPath(const std::string &context, const char *field)
- {
- return context + '.' + field;
- }
-
- // 读取必填 JSON 字段,字段不存在时记录 MissingField 错误
- bool readValue(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- QJsonValue *value,
- ParseState *state)
- {
- const QString key = QString::fromLatin1(field);
- if (!object.contains(key))
- {
- return state->fail(
- ProjectStorageError::MissingField,
- "missing required field " + fieldPath(context, field));
- }
- *value = object.value(key);
- return true;
- }
-
- // 读取必填字符串字段并转换为 UTF-8 标准字符串
- bool readString(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- std::string *value,
- ParseState *state)
- {
- QJsonValue json_value;
- if (!readValue(object, field, context, &json_value, state))
- {
- return false;
- }
- if (!json_value.isString())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " must be a string");
- }
- *value = toUtf8(json_value.toString());
- return true;
- }
-
- // 读取必填布尔字段并校验 JSON 类型
- bool readBool(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- bool *value,
- ParseState *state)
- {
- QJsonValue json_value;
- if (!readValue(object, field, context, &json_value, state))
- {
- return false;
- }
- if (!json_value.isBool())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " must be a boolean");
- }
- *value = json_value.toBool();
- return true;
- }
-
- // 读取指定闭区间内的整数,拒绝小数、非有限值和越界值
- bool readInt(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- int minimum,
- int maximum,
- int *value,
- ParseState *state)
- {
- QJsonValue json_value;
- if (!readValue(object, field, context, &json_value, state))
- {
- return false;
- }
- if (!json_value.isDouble())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " must be an integer");
- }
-
- // Qt JSON 统一用 double 表示数值,需要额外确认它能无损转换为 int
- const double number = json_value.toDouble();
- if (!std::isfinite(number) || std::floor(number) != number
- || number < minimum || number > maximum)
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " is outside the supported integer range");
- }
- *value = static_cast<int>(number);
- return true;
- }
-
- // 读取必填对象字段并校验 JSON 类型
- bool readObject(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- QJsonObject *value,
- ParseState *state)
- {
- QJsonValue json_value;
- if (!readValue(object, field, context, &json_value, state))
- {
- return false;
- }
- if (!json_value.isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " must be an object");
- }
- *value = json_value.toObject();
- return true;
- }
-
- // 读取必填数组字段并校验 JSON 类型
- bool readArray(
- const QJsonObject &object,
- const char *field,
- const std::string &context,
- QJsonArray *value,
- ParseState *state)
- {
- QJsonValue json_value;
- if (!readValue(object, field, context, &json_value, state))
- {
- return false;
- }
- if (!json_value.isArray())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- fieldPath(context, field) + " must be an array");
- }
- *value = json_value.toArray();
- return true;
- }
-
- // 将寄存器地址序列化为包含区域和原始索引的 JSON 对象
- QJsonObject serializeAddress(const RegisterAddress &address)
- {
- QJsonObject object;
- object.insert(QStringLiteral("area"),
- address.area() == RegisterArea::M ? QStringLiteral("M")
- : QStringLiteral("D"));
- object.insert(QStringLiteral("index"), address.index());
- return object;
- }
-
- // 解析寄存器地址并校验区域名称及项目允许的索引范围
- bool parseAddress(
- const QJsonObject &object,
- const std::string &context,
- RegisterAddress *address,
- ParseState *state)
- {
- std::string area_text;
- int index = 0;
- if (!readString(object, "area", context, &area_text, state)
- || !readInt(
- object,
- "index",
- context,
- RegisterAddress::kMinimumIndex,
- RegisterAddress::kMaximumIndex,
- &index,
- state))
- {
- return false;
- }
-
- RegisterArea area = RegisterArea::M;
- if (area_text == "M")
- {
- area = RegisterArea::M;
- }
- else if (area_text == "D")
- {
- area = RegisterArea::D;
- }
- else
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".area must be M or D");
- }
-
- *address = RegisterAddress{area, index};
- return true;
- }
-
- // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串
- QString hmiControlTypeName(HmiControlType type)
- {
- switch (type)
- {
- case HmiControlType::Button:
- {
- return QStringLiteral("button");
- }
- case HmiControlType::Indicator:
- {
- return QStringLiteral("indicator");
- }
- case HmiControlType::NumericDisplay:
- {
- return QStringLiteral("numericDisplay");
- }
- case HmiControlType::NumericInput:
- {
- return QStringLiteral("numericInput");
- }
- case HmiControlType::Label:
- {
- return QStringLiteral("label");
- }
- default:
- {
- return {};
- }
- }
- }
-
- // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举
- bool parseHmiControlType(
- const std::string &value, HmiControlType *type, ParseState *state)
- {
- if (value == "button")
- {
- *type = HmiControlType::Button;
- }
- else if (value == "indicator")
- {
- *type = HmiControlType::Indicator;
- }
- else if (value == "numericDisplay")
- {
- *type = HmiControlType::NumericDisplay;
- }
- else if (value == "numericInput")
- {
- *type = HmiControlType::NumericInput;
- }
- else if (value == "label")
- {
- *type = HmiControlType::Label;
- }
- else
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "unsupported HMI control type " + value);
- }
- return true;
- }
-
- // 将 HMI 控件矩形区域序列化为 JSON 对象
- QJsonObject serializeBounds(const HmiRect &bounds)
- {
- QJsonObject object;
- object.insert(QStringLiteral("x"), bounds.x);
- object.insert(QStringLiteral("y"), bounds.y);
- object.insert(QStringLiteral("width"), bounds.width);
- object.insert(QStringLiteral("height"), bounds.height);
- return object;
- }
-
- // 解析控件矩形区域,允许任意坐标但要求宽高为正数
- bool parseBounds(
- const QJsonObject &object,
- const std::string &context,
- HmiRect *bounds,
- ParseState *state)
- {
- return readInt(
- object,
- "x",
- context,
- std::numeric_limits<int>::min(),
- std::numeric_limits<int>::max(),
- &bounds->x,
- state)
- && readInt(
- object,
- "y",
- context,
- std::numeric_limits<int>::min(),
- std::numeric_limits<int>::max(),
- &bounds->y,
- state)
- && readInt(
- object,
- "width",
- context,
- 1,
- std::numeric_limits<int>::max(),
- &bounds->width,
- state)
- && readInt(
- object,
- "height",
- context,
- 1,
- std::numeric_limits<int>::max(),
- &bounds->height,
- state);
- }
-
- // 将 HMI 控件的字符串扩展属性序列化为 JSON 对象
- QJsonObject serializeProperties(const std::map<std::string, std::string> &properties)
- {
- QJsonObject object;
- for (const auto &property : properties)
- {
- object.insert(fromUtf8(property.first), fromUtf8(property.second));
- }
- return object;
- }
-
- // 解析 HMI 控件扩展属性,并确保所有属性值都是字符串
- bool parseProperties(
- const QJsonObject &object,
- std::map<std::string, std::string> *properties,
- ParseState *state)
- {
- for (auto current = object.constBegin(); current != object.constEnd(); ++current)
- {
- if (!current.value().isString())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "HMI control property values must be strings");
- }
- properties->emplace(toUtf8(current.key()), toUtf8(current.value().toString()));
- }
- return true;
- }
-
- // 将单个 HMI 控件及其可选寄存器绑定序列化为 JSON 对象
- QJsonObject serializeHmiControl(const HmiControl &control)
- {
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(control.id));
- object.insert(QStringLiteral("type"), hmiControlTypeName(control.type));
- object.insert(QStringLiteral("bounds"), serializeBounds(control.bounds));
- object.insert(QStringLiteral("text"), fromUtf8(control.text));
- // 使用 JSON null 明确表示控件没有寄存器绑定
- if (control.binding.has_value())
- {
- object.insert(QStringLiteral("binding"), serializeAddress(*control.binding));
- }
- else
- {
- object.insert(QStringLiteral("binding"), QJsonValue::Null);
- }
- object.insert(QStringLiteral("properties"), serializeProperties(control.properties));
- return object;
- }
-
- // 解析单个 HMI 控件,并逐层校验类型、区域、绑定和扩展属性
- bool parseHmiControl(
- const QJsonObject &object,
- const std::string &context,
- HmiControl *control,
- ParseState *state)
- {
- std::string type_text;
- QJsonObject bounds;
- QJsonObject properties;
- QJsonValue binding;
- if (!readString(object, "id", context, &control->id, state)
- || !readString(object, "type", context, &type_text, state)
- || !readObject(object, "bounds", context, &bounds, state)
- || !readString(object, "text", context, &control->text, state)
- || !readValue(object, "binding", context, &binding, state)
- || !readObject(object, "properties", context, &properties, state))
- {
- return false;
- }
- if (!parseHmiControlType(type_text, &control->type, state)
- || !parseBounds(bounds, context + ".bounds", &control->bounds, state)
- || !parseProperties(properties, &control->properties, state))
- {
- return false;
- }
-
- // binding 允许为 null,其余非空值必须是合法的寄存器地址对象
- if (binding.isNull())
- {
- control->binding.reset();
- return true;
- }
- if (!binding.isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".binding must be an object or null");
- }
-
- RegisterAddress address{RegisterArea::M, 0};
- if (!parseAddress(binding.toObject(), context + ".binding", &address, state))
- {
- return false;
- }
- control->binding = address;
- return true;
- }
-
- // 将 HMI 页面及其全部控件序列化为 JSON 对象
- QJsonObject serializeHmiPage(const HmiPage &page)
- {
- QJsonArray controls;
- for (const HmiControl &control : page.controls)
- {
- controls.append(serializeHmiControl(control));
- }
-
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(page.id));
- object.insert(QStringLiteral("name"), fromUtf8(page.name));
- object.insert(QStringLiteral("width"), page.width);
- object.insert(QStringLiteral("height"), page.height);
- object.insert(QStringLiteral("controls"), controls);
- return object;
- }
-
- // 解析 HMI 页面基础信息,并按数组顺序构造页面控件
- bool parseHmiPage(
- const QJsonObject &object,
- const std::string &context,
- HmiPage *page,
- ParseState *state)
- {
- QJsonArray controls;
- if (!readString(object, "id", context, &page->id, state)
- || !readString(object, "name", context, &page->name, state)
- || !readInt(object, "width", context, 1, std::numeric_limits<int>::max(),
- &page->width, state)
- || !readInt(object, "height", context, 1, std::numeric_limits<int>::max(),
- &page->height, state)
- || !readArray(object, "controls", context, &controls, state))
- {
- return false;
- }
-
- // 预留准确容量,避免逐项加入控件时重复扩容
- page->controls.reserve(static_cast<std::size_t>(controls.size()));
- for (int index = 0; index < controls.size(); ++index)
- {
- if (!controls.at(index).isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".controls items must be objects");
- }
- HmiControl control;
- if (!parseHmiControl(
- controls.at(index).toObject(),
- context + ".controls[" + std::to_string(index) + ']',
- &control,
- state))
- {
- return false;
- }
- page->controls.push_back(std::move(control));
- }
- return true;
- }
-
- // 将触点模式枚举转换为工程文件中的稳定字符串
- QString contactModeName(ContactMode mode)
- {
- return mode == ContactMode::NormallyOpen
- ? QStringLiteral("normallyOpen")
- : QStringLiteral("normallyClosed");
- }
-
- // 将工程文件中的触点模式字符串转换为枚举
- bool parseContactMode(
- const std::string &value, ContactMode *mode, ParseState *state)
- {
- if (value == "normallyOpen")
- {
- *mode = ContactMode::NormallyOpen;
- }
- else if (value == "normallyClosed")
- {
- *mode = ContactMode::NormallyClosed;
- }
- else
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "unsupported contact mode " + value);
- }
- return true;
- }
-
- // 将线圈模式枚举转换为工程文件中的稳定字符串
- QString coilModeName(CoilMode mode)
- {
- switch (mode)
- {
- case CoilMode::Normal:
- {
- return QStringLiteral("normal");
- }
- case CoilMode::Set:
- {
- return QStringLiteral("set");
- }
- case CoilMode::Reset:
- {
- return QStringLiteral("reset");
- }
- default:
- {
- return {};
- }
- }
- }
-
- // 将工程文件中的线圈模式字符串转换为枚举
- bool parseCoilMode(const std::string &value, CoilMode *mode, ParseState *state)
- {
- if (value == "normal")
- {
- *mode = CoilMode::Normal;
- }
- else if (value == "set")
- {
- *mode = CoilMode::Set;
- }
- else if (value == "reset")
- {
- *mode = CoilMode::Reset;
- }
- else
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "unsupported coil mode " + value);
- }
- return true;
- }
-
- // 将比较运算符枚举转换为工程文件中的稳定字符串
- QString comparisonName(ComparisonOperator comparison)
- {
- switch (comparison)
- {
- case ComparisonOperator::Equal:
- {
- return QStringLiteral("equal");
- }
- case ComparisonOperator::NotEqual:
- {
- return QStringLiteral("notEqual");
- }
- case ComparisonOperator::LessThan:
- {
- return QStringLiteral("lessThan");
- }
- case ComparisonOperator::LessThanOrEqual:
- {
- return QStringLiteral("lessThanOrEqual");
- }
- case ComparisonOperator::GreaterThan:
- {
- return QStringLiteral("greaterThan");
- }
- case ComparisonOperator::GreaterThanOrEqual:
- {
- return QStringLiteral("greaterThanOrEqual");
- }
- default:
- {
- return {};
- }
- }
- }
-
- // 将工程文件中的比较运算符字符串转换为枚举
- bool parseComparison(
- const std::string &value,
- ComparisonOperator *comparison,
- ParseState *state)
- {
- if (value == "equal")
- {
- *comparison = ComparisonOperator::Equal;
- }
- else if (value == "notEqual")
- {
- *comparison = ComparisonOperator::NotEqual;
- }
- else if (value == "lessThan")
- {
- *comparison = ComparisonOperator::LessThan;
- }
- else if (value == "lessThanOrEqual")
- {
- *comparison = ComparisonOperator::LessThanOrEqual;
- }
- else if (value == "greaterThan")
- {
- *comparison = ComparisonOperator::GreaterThan;
- }
- else if (value == "greaterThanOrEqual")
- {
- *comparison = ComparisonOperator::GreaterThanOrEqual;
- }
- else
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "unsupported comparison operator " + value);
- }
- return true;
- }
-
- // 将触点节点配置序列化,并写入用于反序列化分派的类型标记
- QJsonObject serializeNodeConfig(const ContactNodeConfig &config)
- {
- QJsonObject object;
- object.insert(QStringLiteral("type"), QStringLiteral("contact"));
- object.insert(QStringLiteral("address"), serializeAddress(config.address));
- object.insert(QStringLiteral("mode"), contactModeName(config.mode));
- return object;
- }
-
- // 将线圈节点配置序列化,并写入用于反序列化分派的类型标记
- QJsonObject serializeNodeConfig(const CoilNodeConfig &config)
- {
- QJsonObject object;
- object.insert(QStringLiteral("type"), QStringLiteral("coil"));
- object.insert(QStringLiteral("address"), serializeAddress(config.address));
- object.insert(QStringLiteral("mode"), coilModeName(config.mode));
- return object;
- }
-
- // 将数值比较节点配置序列化,并保留有符号 16 位比较常量
- QJsonObject serializeNodeConfig(const CompareNodeConfig &config)
- {
- QJsonObject object;
- object.insert(QStringLiteral("type"), QStringLiteral("compare"));
- object.insert(QStringLiteral("address"), serializeAddress(config.address));
- object.insert(QStringLiteral("comparison"), comparisonName(config.comparison));
- object.insert(QStringLiteral("value"), config.value);
- return object;
- }
-
- // 将逻辑节点序列化,并根据 variant 中的实际配置类型选择对应重载
- QJsonObject serializeLogicNode(const LogicNode &node)
- {
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(node.id));
- object.insert(QStringLiteral("configured"), node.configured);
- // std::visit 将不同节点配置统一转换为 config JSON 对象
- object.insert(
- QStringLiteral("config"),
- std::visit(
- [](const auto &config)
- {
- return serializeNodeConfig(config);
- },
- node.config));
- return object;
- }
-
- // 根据 type 字段解析具体节点配置,并写入 LogicNodeConfig 变体
- bool parseNodeConfig(
- const QJsonObject &object,
- const std::string &context,
- LogicNodeConfig *config,
- ParseState *state)
- {
- std::string type;
- QJsonObject address_object;
- if (!readString(object, "type", context, &type, state)
- || !readObject(object, "address", context, &address_object, state))
- {
- return false;
- }
-
- RegisterAddress address{RegisterArea::M, 0};
- if (!parseAddress(address_object, context + ".address", &address, state))
- {
- return false;
- }
-
- // 每种节点只读取自身需要的字段,避免无关配置进入领域模型
- if (type == "contact")
- {
- std::string mode_text;
- ContactMode mode = ContactMode::NormallyOpen;
- if (!readString(object, "mode", context, &mode_text, state)
- || !parseContactMode(mode_text, &mode, state))
- {
- return false;
- }
- *config = ContactNodeConfig{address, mode};
- return true;
- }
- if (type == "coil")
- {
- std::string mode_text;
- CoilMode mode = CoilMode::Normal;
- if (!readString(object, "mode", context, &mode_text, state)
- || !parseCoilMode(mode_text, &mode, state))
- {
- return false;
- }
- *config = CoilNodeConfig{address, mode};
- return true;
- }
- if (type == "compare")
- {
- std::string comparison_text;
- int value = 0;
- ComparisonOperator comparison = ComparisonOperator::Equal;
- if (!readString(object, "comparison", context, &comparison_text, state)
- || !readInt(
- object,
- "value",
- context,
- std::numeric_limits<std::int16_t>::min(),
- std::numeric_limits<std::int16_t>::max(),
- &value,
- state)
- || !parseComparison(comparison_text, &comparison, state))
- {
- return false;
- }
- // 先按 int 校验范围,再安全收窄为领域模型要求的 int16_t
- *config = CompareNodeConfig{
- address, comparison, static_cast<std::int16_t>(value)};
- return true;
- }
-
- return state->fail(
- ProjectStorageError::InvalidField,
- "unsupported logic node type " + type);
- }
-
- // 解析逻辑节点标识及其多态配置
- bool parseLogicNode(
- const QJsonObject &object,
- const std::string &context,
- LogicNode *node,
- ParseState *state)
- {
- QJsonObject config;
- if (!readString(object, "id", context, &node->id, state)
- || !readBool(object, "configured", context, &node->configured, state)
- || !readObject(object, "config", context, &config, state)
- || !parseNodeConfig(config, context + ".config", &node->config, state))
- {
- return false;
- }
- return true;
- }
-
- QString expressionKindText(ConditionExpressionKind kind)
- {
- switch (kind)
- {
- case ConditionExpressionKind::Node:
- return QStringLiteral("node");
- case ConditionExpressionKind::Series:
- return QStringLiteral("series");
- case ConditionExpressionKind::Parallel:
- return QStringLiteral("parallel");
- }
- return {};
- }
-
- QJsonObject serializeConditionExpression(const ConditionExpression &expression)
- {
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(expression.id));
- object.insert(QStringLiteral("kind"), expressionKindText(expression.kind));
- if (expression.kind == ConditionExpressionKind::Node)
- {
- object.insert(QStringLiteral("node"), serializeLogicNode(*expression.node));
- }
- else
- {
- QJsonArray children;
- for (const ConditionExpression &child : expression.children)
- {
- children.append(serializeConditionExpression(child));
- }
- object.insert(QStringLiteral("children"), children);
- }
- return object;
- }
-
- bool parseConditionExpression(
- const QJsonObject &object,
- const std::string &context,
- ConditionExpression *expression,
- ParseState *state)
- {
- std::string kind;
- if (!readString(object, "id", context, &expression->id, state)
- || !readString(object, "kind", context, &kind, state))
- {
- return false;
- }
- if (kind == "node")
- {
- QJsonObject node;
- if (!readObject(object, "node", context, &node, state))
- {
- return false;
- }
- LogicNode parsed_node;
- if (!parseLogicNode(node, context + ".node", &parsed_node, state))
- {
- return false;
- }
- expression->kind = ConditionExpressionKind::Node;
- expression->node = std::move(parsed_node);
- return true;
- }
- if (kind != "series" && kind != "parallel")
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".kind must be node, series or parallel");
- }
- QJsonArray children;
- if (!readArray(object, "children", context, &children, state))
- {
- return false;
- }
- expression->kind = kind == "series"
- ? ConditionExpressionKind::Series : ConditionExpressionKind::Parallel;
- expression->children.reserve(static_cast<std::size_t>(children.size()));
- for (int index = 0; index < children.size(); ++index)
- {
- if (!children.at(index).isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".children items must be objects");
- }
- ConditionExpression child;
- if (!parseConditionExpression(
- children.at(index).toObject(),
- context + ".children[" + std::to_string(index) + ']',
- &child,
- state))
- {
- return false;
- }
- expression->children.push_back(std::move(child));
- }
- return true;
- }
-
- QJsonObject serializeLadderRung(const LadderRung &rung)
- {
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(rung.id));
- object.insert(QStringLiteral("name"), fromUtf8(rung.name));
- object.insert(
- QStringLiteral("condition"),
- rung.condition.has_value()
- ? QJsonValue(serializeConditionExpression(*rung.condition))
- : QJsonValue(QJsonValue::Null));
- object.insert(
- QStringLiteral("output"),
- rung.output.has_value() ? QJsonValue(serializeLogicNode(*rung.output))
- : QJsonValue(QJsonValue::Null));
- return object;
- }
-
- bool parseLadderRung(
- const QJsonObject &object,
- const std::string &context,
- LadderRung *rung,
- ParseState *state)
- {
- QJsonValue condition;
- QJsonValue output;
- if (!readString(object, "id", context, &rung->id, state)
- || !readString(object, "name", context, &rung->name, state)
- || !readValue(object, "output", context, &output, state))
- {
- return false;
- }
- if (!readValue(object, "condition", context, &condition, state))
- {
- return false;
- }
- if (condition.isNull())
- {
- rung->condition.reset();
- }
- else if (!condition.isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".condition must be an object or null");
- }
- else
- {
- ConditionExpression parsed_condition;
- if (!parseConditionExpression(
- condition.toObject(), context + ".condition", &parsed_condition, state))
- {
- return false;
- }
- rung->condition = std::move(parsed_condition);
- }
- if (output.isNull())
- {
- rung->output.reset();
- return true;
- }
- if (!output.isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".output must be an object or null");
- }
- LogicNode node;
- if (!parseLogicNode(output.toObject(), context + ".output", &node, state))
- {
- return false;
- }
- rung->output = std::move(node);
- return true;
- }
-
- QJsonObject serializeControlLogic(const ControlLogic &logic)
- {
- QJsonArray rungs;
- for (const LadderRung &rung : logic.rungs)
- {
- rungs.append(serializeLadderRung(rung));
- }
- QJsonObject object;
- object.insert(QStringLiteral("id"), fromUtf8(logic.id));
- object.insert(QStringLiteral("name"), fromUtf8(logic.name));
- object.insert(QStringLiteral("enabled"), logic.enabled);
- object.insert(QStringLiteral("rungs"), rungs);
- return object;
- }
-
- bool parseControlLogic(
- const QJsonObject &object,
- const std::string &context,
- ControlLogic *logic,
- ParseState *state)
- {
- QJsonArray rungs;
- if (!readString(object, "id", context, &logic->id, state)
- || !readString(object, "name", context, &logic->name, state)
- || !readBool(object, "enabled", context, &logic->enabled, state)
- || !readArray(object, "rungs", context, &rungs, state))
- {
- return false;
- }
- logic->rungs.reserve(static_cast<std::size_t>(rungs.size()));
- for (int index = 0; index < rungs.size(); ++index)
- {
- if (!rungs.at(index).isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- context + ".rungs items must be objects");
- }
- LadderRung rung;
- if (!parseLadderRung(
- rungs.at(index).toObject(),
- context + ".rungs[" + std::to_string(index) + ']',
- &rung,
- state))
- {
- return false;
- }
- logic->rungs.push_back(std::move(rung));
- }
- return true;
- }
-
- // 将完整领域工程聚合为工程文件的顶层 JSON 对象
- QJsonObject serializeProject(const Project &project)
- {
- QJsonArray pages;
- for (const HmiPage &page : project.hmiPages)
- {
- pages.append(serializeHmiPage(page));
- }
-
- QJsonArray logics;
- for (const ControlLogic &logic : project.controlLogics)
- {
- logics.append(serializeControlLogic(logic));
- }
-
- QJsonObject object;
- object.insert(QStringLiteral("formatVersion"), fromUtf8(project.metadata.formatVersion));
- object.insert(QStringLiteral("id"), fromUtf8(project.metadata.id));
- object.insert(QStringLiteral("name"), fromUtf8(project.metadata.name));
- object.insert(QStringLiteral("hmiPages"), pages);
- object.insert(QStringLiteral("controlLogics"), logics);
- return object;
- }
-
- // 从顶层 JSON 对象解析完整工程,并在解析子对象前检查格式版本
- bool parseProject(
- const QJsonObject &object, Project *project, ParseState *state)
- {
- QJsonArray pages;
- QJsonArray logics;
- if (!readString(
- object,
- "formatVersion",
- "project",
- &project->metadata.formatVersion,
- state))
- {
- return false;
- }
- // 不尝试猜测其他版本的结构,避免按当前格式错误解释数据
- if (project->metadata.formatVersion != kCurrentFormatVersion)
- {
- return state->fail(
- ProjectStorageError::UnsupportedVersion,
- "unsupported project format version " + project->metadata.formatVersion);
- }
-
- if (!readString(object, "id", "project", &project->metadata.id, state)
- || !readString(object, "name", "project", &project->metadata.name, state)
- || !readArray(object, "hmiPages", "project", &pages, state)
- || !readArray(object, "controlLogics", "project", &logics, state))
- {
- return false;
- }
-
- // 逐层解析时持续传递字段路径,任何失败都保留最初的具体位置
- project->hmiPages.reserve(static_cast<std::size_t>(pages.size()));
- for (int index = 0; index < pages.size(); ++index)
- {
- if (!pages.at(index).isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "project.hmiPages items must be objects");
- }
- HmiPage page;
- if (!parseHmiPage(
- pages.at(index).toObject(),
- "project.hmiPages[" + std::to_string(index) + ']',
- &page,
- state))
- {
- return false;
- }
- project->hmiPages.push_back(std::move(page));
- }
-
- project->controlLogics.reserve(static_cast<std::size_t>(logics.size()));
- for (int index = 0; index < logics.size(); ++index)
- {
- if (!logics.at(index).isObject())
- {
- return state->fail(
- ProjectStorageError::InvalidField,
- "project.controlLogics items must be objects");
- }
- ControlLogic logic;
- if (!parseControlLogic(
- logics.at(index).toObject(),
- "project.controlLogics[" + std::to_string(index) + ']',
- &logic,
- state))
- {
- return false;
- }
- project->controlLogics.push_back(std::move(logic));
- }
- return true;
- }
-
- // 将 Qt 文件错误转换为领域层统一的工程保存失败结果
- ProjectSaveResult saveFailure(
- ProjectStorageError error, const QString &message)
- {
- return {false, error, toUtf8(message)};
- }
-
- } // namespace
-
- // 校验工程后将其序列化,并通过 QSaveFile 原子写入目标文件
- ProjectSaveResult JsonProjectStorage::save(
- const Project &project, const std::string &file_path)
- {
- // 写文件前先执行领域校验,防止持久化内部关系不合法的工程
- std::string validation_error;
- if (!project.validate(&validation_error))
- {
- return {false, ProjectStorageError::InvalidProject, validation_error};
- }
- if (project.metadata.formatVersion != kCurrentFormatVersion)
- {
- return {false,
- ProjectStorageError::UnsupportedVersion,
- "unsupported project format version " + project.metadata.formatVersion};
- }
-
- // QSaveFile 先写临时文件,仅在 commit 成功后替换目标文件
- QSaveFile file(fromUtf8(file_path));
- if (!file.open(QIODevice::WriteOnly))
- {
- return saveFailure(ProjectStorageError::FileOpenFailed, file.errorString());
- }
-
- const QByteArray data = QJsonDocument(serializeProject(project)).toJson(
- QJsonDocument::Indented);
- // 短写入也视为失败,并取消临时文件提交
- if (file.write(data) != data.size())
- {
- file.cancelWriting();
- return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString());
- }
- if (!file.commit())
- {
- return saveFailure(ProjectStorageError::FileWriteFailed, file.errorString());
- }
- return {true, ProjectStorageError::None, {}};
- }
-
- // 读取并解析工程文件,全部校验通过后才返回新的领域工程
- ProjectLoadResult JsonProjectStorage::load(const std::string &file_path)
- {
- QFile file(fromUtf8(file_path));
- if (!file.open(QIODevice::ReadOnly))
- {
- return {false,
- {},
- ProjectStorageError::FileOpenFailed,
- toUtf8(file.errorString())};
- }
-
- const QByteArray data = file.readAll();
- if (file.error() != QFileDevice::NoError)
- {
- return {false,
- {},
- ProjectStorageError::FileReadFailed,
- toUtf8(file.errorString())};
- }
-
- // 顶层必须是 JSON 对象,数组或标量不能表示完整工程
- QJsonParseError parse_error;
- const QJsonDocument document = QJsonDocument::fromJson(data, &parse_error);
- if (parse_error.error != QJsonParseError::NoError || !document.isObject())
- {
- const QString message = parse_error.error == QJsonParseError::NoError
- ? QStringLiteral("project JSON root must be an object")
- : parse_error.errorString();
- return {false,
- {},
- ProjectStorageError::InvalidJson,
- toUtf8(message)};
- }
-
- // 先在局部对象中完成结构解析,失败时不会暴露半成品工程
- Project project;
- ParseState state;
- if (!parseProject(document.object(), &project, &state))
- {
- return {false, {}, state.error, state.message};
- }
-
- // JSON 字段合法不代表业务关系合法,还需执行领域层整体校验
- std::string validation_error;
- if (!project.validate(&validation_error))
- {
- return {false,
- {},
- ProjectStorageError::InvalidProject,
- validation_error};
- }
- return {true, std::move(project), ProjectStorageError::None, {}};
- }
|