From 19088fed03e75e6f685c4e0dd518b1c6a37abe02 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:30:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现配置架构解析器,支持JSON架构到递归树的转换 - 添加YAML解析器,支持根映射、嵌套对象和数组结构 - 集成配置验证诊断系统,提供架构和YAML内容校验 - 实现批量编辑器字段提取,支持标量类型安全更新 - 添加YAML注释提取功能,映射到逻辑字段路径 - 创建示例配置YAML生成功能,包含架构描述作为注释 - 实现表单更新应用到YAML功能,重写YAML树结构 - 添加标量兼容性检查,支持整数、数字、布尔值和字符串类型 - 实现精确十进制算术运算,用于multipleOf约束验证 - 添加模式匹配验证,支持正则表达式编译和测试 - 实现常量值比较功能,保持与运行时一致的比较格式 - 集成多语言本地化支持,提供中英文验证消息 --- .../src/configValidation.js | 71 ++++++++++++-- .../test/configValidation.test.js | 94 +++++++++++++++++++ .../test/containsSummary.test.js | 49 ++++++++++ 3 files changed, 208 insertions(+), 6 deletions(-) diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js index 94e96d72..77b39183 100644 --- a/tools/gframework-config-tool/src/configValidation.js +++ b/tools/gframework-config-tool/src/configValidation.js @@ -1023,7 +1023,8 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) } const comparableItems = []; - let hasInvalidArrayItems = false; + const containsCandidateItems = []; + let hasStructurallyInvalidArrayItems = false; for (let index = 0; index < yamlNode.items.length; index += 1) { const diagnosticsBeforeValidation = diagnostics.length; validateNode( @@ -1033,12 +1034,16 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) diagnostics, localizer); + if (isStructurallyCompatibleWithSchemaNode(schemaNode.items, yamlNode.items[index])) { + containsCandidateItems.push({index, node: yamlNode.items[index]}); + } else { + hasStructurallyInvalidArrayItems = true; + } + // Keep uniqueItems focused on values that are otherwise valid so a - // shape/type error does not also surface as a misleading duplicate. + // shape/type or constraint error does not also surface as a misleading duplicate. if (diagnostics.length === diagnosticsBeforeValidation) { comparableItems.push({index, node: yamlNode.items[index]}); - } else { - hasInvalidArrayItems = true; } } @@ -1061,9 +1066,9 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) } } - if (!hasInvalidArrayItems && schemaNode.contains) { + if (!hasStructurallyInvalidArrayItems && schemaNode.contains) { let matchingContainsCount = 0; - for (const {node} of comparableItems) { + for (const {node} of containsCandidateItems) { if (matchesSchemaNode(schemaNode.contains, node)) { matchingContainsCount += 1; } @@ -1506,6 +1511,60 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode) { buildComparableNodeValue(schemaNode, yamlNode) === schemaNode.constComparableValue; } +/** + * Test whether one YAML node is structurally compatible with one schema node. + * This keeps array-level `contains` validation from producing noisy follow-on + * diagnostics when an item already has a shape or scalar-type mismatch, while + * still allowing value-level constraint failures to participate in contains counting. + * + * @param {SchemaNode} schemaNode Schema node. + * @param {YamlNode} yamlNode YAML node. + * @returns {boolean} True when the YAML node has the expected recursive shape. + */ +function isStructurallyCompatibleWithSchemaNode(schemaNode, yamlNode) { + if (schemaNode.type === "object") { + if (!yamlNode || yamlNode.kind !== "object") { + return false; + } + + for (const requiredProperty of schemaNode.required) { + if (!yamlNode.map.has(requiredProperty)) { + return false; + } + } + + for (const entry of yamlNode.entries) { + if (!Object.prototype.hasOwnProperty.call(schemaNode.properties, entry.key)) { + return false; + } + + if (!isStructurallyCompatibleWithSchemaNode(schemaNode.properties[entry.key], entry.node)) { + return false; + } + } + + return true; + } + + if (schemaNode.type === "array") { + if (!yamlNode || yamlNode.kind !== "array") { + return false; + } + + for (const item of yamlNode.items) { + if (!isStructurallyCompatibleWithSchemaNode(schemaNode.items, item)) { + return false; + } + } + + return true; + } + + return Boolean(yamlNode) && + yamlNode.kind === "scalar" && + isScalarCompatible(schemaNode.type, yamlNode.value); +} + /** * Validate one parsed YAML node against one normalized const comparable value. * The helper reuses the same comparable-key logic as uniqueItems so array order diff --git a/tools/gframework-config-tool/test/configValidation.test.js b/tools/gframework-config-tool/test/configValidation.test.js index 38fe05af..92ff0b1e 100644 --- a/tools/gframework-config-tool/test/configValidation.test.js +++ b/tools/gframework-config-tool/test/configValidation.test.js @@ -831,6 +831,100 @@ dropRates: assert.match(diagnostics[0].message, /at least 2 items matching the 'contains' schema|至少需要包含 2 个匹配 contains 条件的元素/u); }); +test("validateParsedConfig should skip contains match-count when items are structurally invalid", () => { + const schema = parseSchemaContent(` + { + "type": "object", + "required": ["dropRates"], + "properties": { + "dropRates": { + "type": "array", + "minContains": 2, + "contains": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "const": "RARE" + } + } + }, + "items": { + "type": "object", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + } + } + } + `); + const yaml = parseTopLevelYaml(` +dropRates: + - + type: RARE + value: "not-a-number" + - + type: RARE + value: 10 +`); + + const diagnostics = validateParsedConfig(schema, yaml); + + assert.ok(diagnostics.length > 0); + assert.match( + diagnostics[0].message, + /dropRates\[0\]\.value/u); + assert.match( + diagnostics[0].message, + /integer|整数/u); + assert.equal( + diagnostics.some((diagnostic) => /at least 2 items matching the 'contains' schema|至少需要包含 2 个匹配 contains 条件的元素/u.test(diagnostic.message)), + false); + assert.equal( + diagnostics.some((diagnostic) => /at most \d+ items matching the 'contains' schema|最多只能包含 \d+ 个匹配 contains 条件的元素/u.test(diagnostic.message)), + false); +}); + +test("validateParsedConfig should continue contains match-count when items only have value-level violations", () => { + const schema = parseSchemaContent(` + { + "type": "object", + "properties": { + "dropRates": { + "type": "array", + "minContains": 1, + "contains": { + "type": "integer", + "const": 7 + }, + "items": { + "type": "integer", + "minimum": 10 + } + } + } + } + `); + const yaml = parseTopLevelYaml(` +dropRates: + - 5 +`); + + const diagnostics = validateParsedConfig(schema, yaml); + + assert.equal(diagnostics.length, 2); + assert.match(diagnostics[0].message, /greater than or equal to 10|大于或等于 10/u); + assert.match(diagnostics[1].message, /at least 1 items matching the 'contains' schema|至少需要包含 1 个匹配 contains 条件的元素/u); +}); + test("validateParsedConfig should report maxContains violations", () => { const schema = parseSchemaContent(` { diff --git a/tools/gframework-config-tool/test/containsSummary.test.js b/tools/gframework-config-tool/test/containsSummary.test.js index 5d10e12e..8d29d7f0 100644 --- a/tools/gframework-config-tool/test/containsSummary.test.js +++ b/tools/gframework-config-tool/test/containsSummary.test.js @@ -44,3 +44,52 @@ test("buildContainsHintLines should include default minContains when schema omit "Min contains: 1" ]); }); + +test("buildContainsHintLines should use explicit minContains when provided", () => { + const localizer = createLocalizer("en"); + + const lines = buildContainsHintLines( + { + minContains: 2, + contains: { + type: "string", + constValue: "\"potion\"", + constDisplayValue: "\"potion\"", + refTable: "item" + } + }, + localizer); + + assert.deepEqual(lines, [ + "Contains: string, Const: \"potion\", Ref table: item", + "Min contains: 2" + ]); +}); + +test("describeContainsSchema should format enum-based contains schema in English", () => { + const localizer = createLocalizer("en"); + + const summary = describeContainsSchema( + { + type: "string", + enumValues: ["potion", "elixir"], + refTable: "item" + }, + localizer); + + assert.equal(summary, "string, Allowed: potion, elixir, Ref table: item"); +}); + +test("describeContainsSchema should format pattern-based contains schema in Chinese", () => { + const localizer = createLocalizer("zh-cn"); + + const summary = describeContainsSchema( + { + type: "string", + pattern: "^potion-", + refTable: "item" + }, + localizer); + + assert.equal(summary, "string, 正则模式:^potion-, 引用表:item"); +});