mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(config): 添加配置验证功能模块
- 实现配置模式解析器,支持对象、数组和标量类型的递归验证 - 添加 YAML 配置文件解析和注释提取功能 - 实现配置值的类型兼容性检查和约束验证 - 添加批量编辑器字段收集和表单更新应用功能 - 实现配置样本生成和多语言本地化支持 - 添加精确十进制算术用于数值约束验证 - 实现配置枚举值和默认值的标准化处理 - 添加配置常量值的可比较键构建功能
This commit is contained in:
parent
dca304afeb
commit
b0e8b6ecc5
@ -1319,17 +1319,191 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether one YAML node satisfies one schema node without emitting user-facing diagnostics.
|
* Test whether one YAML node satisfies one schema node without emitting user-facing diagnostics.
|
||||||
* This is used by array `contains` so the tooling can reuse the same recursive validator
|
* This is used by array `contains`, where object sub-schemas must behave like
|
||||||
* while treating regular validation failures as a simple "does not match" result.
|
* partial matchers: declared properties, required members, and constraints must
|
||||||
|
* match, but additional object members outside the sub-schema must not block a hit.
|
||||||
*
|
*
|
||||||
* @param {SchemaNode} schemaNode Schema node.
|
* @param {SchemaNode} schemaNode Schema node.
|
||||||
* @param {YamlNode} yamlNode YAML node.
|
* @param {YamlNode} yamlNode YAML node.
|
||||||
* @returns {boolean} True when the YAML node matches the schema node.
|
* @returns {boolean} True when the YAML node matches the schema node.
|
||||||
*/
|
*/
|
||||||
function matchesSchemaNode(schemaNode, yamlNode) {
|
function matchesSchemaNode(schemaNode, yamlNode) {
|
||||||
const diagnostics = [];
|
return matchesSchemaNodeInternal(schemaNode, yamlNode);
|
||||||
validateNode(schemaNode, yamlNode, schemaNode.displayPath, diagnostics, undefined);
|
}
|
||||||
return diagnostics.length === 0;
|
|
||||||
|
/**
|
||||||
|
* Match one YAML node against one schema node using JSON-Schema-style subset semantics.
|
||||||
|
* The helper mirrors validation rules closely, but it intentionally skips unknown-property
|
||||||
|
* rejection for objects so `contains` can test whether one item satisfies a sub-schema.
|
||||||
|
*
|
||||||
|
* @param {SchemaNode} schemaNode Schema node.
|
||||||
|
* @param {YamlNode} yamlNode YAML node.
|
||||||
|
* @returns {boolean} True when the YAML node satisfies the schema node.
|
||||||
|
*/
|
||||||
|
function matchesSchemaNodeInternal(schemaNode, yamlNode) {
|
||||||
|
if (schemaNode.type === "object") {
|
||||||
|
if (!yamlNode || yamlNode.kind !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyCount = yamlNode.map instanceof Map
|
||||||
|
? yamlNode.map.size
|
||||||
|
: Array.isArray(yamlNode.entries)
|
||||||
|
? new Set(yamlNode.entries.map((entry) => entry.key)).size
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
for (const requiredProperty of schemaNode.required) {
|
||||||
|
if (!yamlNode.map.has(requiredProperty)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, childSchema] of Object.entries(schemaNode.properties)) {
|
||||||
|
if (yamlNode.map.has(key) &&
|
||||||
|
!matchesSchemaNodeInternal(childSchema, yamlNode.map.get(key))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.minProperties === "number" &&
|
||||||
|
propertyCount < schemaNode.minProperties) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.maxProperties === "number" &&
|
||||||
|
propertyCount > schemaNode.maxProperties) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof schemaNode.constComparableValue !== "string" ||
|
||||||
|
buildComparableNodeValue(schemaNode, yamlNode) === schemaNode.constComparableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaNode.type === "array") {
|
||||||
|
if (!yamlNode || yamlNode.kind !== "array") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.minItems === "number" &&
|
||||||
|
yamlNode.items.length < schemaNode.minItems) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.maxItems === "number" &&
|
||||||
|
yamlNode.items.length > schemaNode.maxItems) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of yamlNode.items) {
|
||||||
|
if (!matchesSchemaNodeInternal(schemaNode.items, item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaNode.uniqueItems === true) {
|
||||||
|
const seenItems = new Set();
|
||||||
|
for (const item of yamlNode.items) {
|
||||||
|
const comparableValue = buildComparableNodeValue(schemaNode.items, item);
|
||||||
|
if (seenItems.has(comparableValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenItems.add(comparableValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaNode.contains) {
|
||||||
|
let matchingContainsCount = 0;
|
||||||
|
for (const item of yamlNode.items) {
|
||||||
|
if (matchesSchemaNodeInternal(schemaNode.contains, item)) {
|
||||||
|
matchingContainsCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredMinContains = typeof schemaNode.minContains === "number"
|
||||||
|
? schemaNode.minContains
|
||||||
|
: 1;
|
||||||
|
if (matchingContainsCount < requiredMinContains) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.maxContains === "number" &&
|
||||||
|
matchingContainsCount > schemaNode.maxContains) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof schemaNode.constComparableValue !== "string" ||
|
||||||
|
buildComparableNodeValue(schemaNode, yamlNode) === schemaNode.constComparableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yamlNode || yamlNode.kind !== "scalar") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isScalarCompatible(schemaNode.type, yamlNode.value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(schemaNode.enumValues) &&
|
||||||
|
schemaNode.enumValues.length > 0 &&
|
||||||
|
!schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scalarValue = unquoteScalar(yamlNode.value);
|
||||||
|
const supportsNumericConstraints = schemaNode.type === "integer" || schemaNode.type === "number";
|
||||||
|
const supportsLengthConstraints = schemaNode.type === "string";
|
||||||
|
const supportsPatternConstraints = schemaNode.type === "string";
|
||||||
|
|
||||||
|
if (supportsNumericConstraints &&
|
||||||
|
typeof schemaNode.minimum === "number" &&
|
||||||
|
Number(scalarValue) < schemaNode.minimum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsNumericConstraints &&
|
||||||
|
typeof schemaNode.exclusiveMinimum === "number" &&
|
||||||
|
Number(scalarValue) <= schemaNode.exclusiveMinimum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsNumericConstraints &&
|
||||||
|
typeof schemaNode.maximum === "number" &&
|
||||||
|
Number(scalarValue) > schemaNode.maximum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsNumericConstraints &&
|
||||||
|
typeof schemaNode.exclusiveMaximum === "number" &&
|
||||||
|
Number(scalarValue) >= schemaNode.exclusiveMaximum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsNumericConstraints &&
|
||||||
|
!matchesSchemaMultipleOf(scalarValue, schemaNode.multipleOf)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsLengthConstraints &&
|
||||||
|
typeof schemaNode.minLength === "number" &&
|
||||||
|
scalarValue.length < schemaNode.minLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsLengthConstraints &&
|
||||||
|
typeof schemaNode.maxLength === "number" &&
|
||||||
|
scalarValue.length > schemaNode.maxLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsPatternConstraints &&
|
||||||
|
!matchesSchemaPattern(scalarValue, schemaNode.patternRegex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof schemaNode.constComparableValue !== "string" ||
|
||||||
|
buildComparableNodeValue(schemaNode, yamlNode) === schemaNode.constComparableValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -921,6 +921,53 @@ dropRates:
|
|||||||
assert.deepEqual(validateParsedConfig(schemaWithDefaultMinContains, yamlSatisfyingDefaultMinContains), []);
|
assert.deepEqual(validateParsedConfig(schemaWithDefaultMinContains, yamlSatisfyingDefaultMinContains), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should allow object contains matches with additional declared item fields", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"minContains": 1,
|
||||||
|
"contains": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "boss"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "weight"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
entries:
|
||||||
|
-
|
||||||
|
id: boss
|
||||||
|
weight: 10
|
||||||
|
-
|
||||||
|
id: slime
|
||||||
|
weight: 3
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.deepEqual(validateParsedConfig(schema, yaml), []);
|
||||||
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should accept large decimal multiples without floating-point drift", () => {
|
test("validateParsedConfig should accept large decimal multiples without floating-point drift", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user