mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(config): 添加配置验证功能模块
- 实现配置架构解析器,支持JSON架构到递归树的转换 - 添加YAML解析器,支持根映射、嵌套对象和数组结构 - 集成配置验证诊断系统,提供架构和YAML内容校验 - 实现批量编辑器字段提取,支持标量类型安全更新 - 添加YAML注释提取功能,映射到逻辑字段路径 - 创建示例配置YAML生成功能,包含架构描述作为注释 - 实现表单更新应用到YAML功能,重写YAML树结构 - 添加标量兼容性检查,支持整数、数字、布尔值和字符串类型 - 实现精确十进制算术运算,用于multipleOf约束验证 - 添加模式匹配验证,支持正则表达式编译和测试 - 实现常量值比较功能,保持与运行时一致的比较格式 - 集成多语言本地化支持,提供中英文验证消息
This commit is contained in:
parent
b0e8b6ecc5
commit
19088fed03
@ -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
|
||||
|
||||
@ -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(`
|
||||
{
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user