mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 13:14:30 +08:00
docs(config): 添加游戏内容配置系统文档和验证工具
- 新增游戏内容配置系统完整文档,涵盖 YAML 配置、JSON Schema 结构、目录组织等 - 添加 Schema 示例和 YAML 示例,说明怪物、物品等静态数据配置方式 - 提供推荐接入模板,包括目录结构、csproj 配置和启动代码模板 - 实现官方启动帮助器 GameConfigBootstrap 与 GameConfigModule 集成 - 添加运行时读取模板,提供强类型配置访问入口 - 实现生成查询辅助功能,支持 FindBy* 和 TryFindFirstBy* 查询接口 - 提供 Architecture 推荐接入模板,支持模块化配置管理 - 添加热重载模板,支持开发期配置文件自动刷新 - 实现运行时接入方案,提供只读表形式的配置访问 - 添加运行时校验行为说明,支持跨表引用和数据完整性检查 - 实现开发期热重载功能,支持配置变更自动重载 - 添加生成器接入约定,自动生成配置类型和表包装代码 - 提供 VS Code 工具支持,包括配置浏览、表单编辑和批量更新功能 - 实现配置验证工具,支持 JSON Schema 子集解析和 YAML 校验功能
This commit is contained in:
parent
f5317eda01
commit
16686a0d97
@ -558,6 +558,47 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证科学计数法数值会按 <c>number</c> 类型被运行时接受。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task LoadAsync_Should_Accept_Scientific_Notation_Number()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
dropRate: 1.5e10
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "dropRate"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"dropRate": { "type": "number" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterNumberConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var table = registry.GetTable<int, MonsterNumberConfigStub>("monster");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(table.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(table.Get(1).DropRate, Is.EqualTo(1.5e10));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证字符串最小长度与最大长度约束会在运行时被统一拒绝。
|
/// 验证字符串最小长度与最大长度约束会在运行时被统一拒绝。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -864,6 +905,68 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 <c>uniqueItems</c> 的归一化键不会把带分隔符的不同对象值误判为重复项。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task LoadAsync_Should_Accept_Distinct_Object_Items_When_Comparable_Values_Contain_Separators()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
entries:
|
||||||
|
-
|
||||||
|
a: "x|1:b=string:yz"
|
||||||
|
-
|
||||||
|
a: x
|
||||||
|
b: yz
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "entries"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"a": { "type": "string" },
|
||||||
|
"b": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterComparableEntryArrayConfigStub>(
|
||||||
|
"monster",
|
||||||
|
"monster",
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var table = registry.GetTable<int, MonsterComparableEntryArrayConfigStub>("monster");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(table.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(table.Get(1).Entries.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(table.Get(1).Entries[0].A, Is.EqualTo("x|1:b=string:yz"));
|
||||||
|
Assert.That(table.Get(1).Entries[1].A, Is.EqualTo("x"));
|
||||||
|
Assert.That(table.Get(1).Entries[1].B, Is.EqualTo("yz"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1801,6 +1904,22 @@ public class YamlConfigLoaderTests
|
|||||||
public int Hp { get; set; }
|
public int Hp { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于浮点数 schema 校验测试的最小怪物配置类型。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MonsterNumberConfigStub
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置主键。
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置浮点掉落率。
|
||||||
|
/// </summary>
|
||||||
|
public double DropRate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用于数组 schema 校验测试的最小怪物配置类型。
|
/// 用于数组 schema 校验测试的最小怪物配置类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1880,6 +1999,22 @@ public class YamlConfigLoaderTests
|
|||||||
public IReadOnlyList<PhaseConfigStub> Phases { get; set; } = Array.Empty<PhaseConfigStub>();
|
public IReadOnlyList<PhaseConfigStub> Phases { get; set; } = Array.Empty<PhaseConfigStub>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于 <c>uniqueItems</c> 比较键碰撞回归测试的最小配置类型。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MonsterComparableEntryArrayConfigStub
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置主键。
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置待比较对象数组。
|
||||||
|
/// </summary>
|
||||||
|
public List<ComparableEntryConfigStub> Entries { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示对象数组中的阶段元素。
|
/// 表示对象数组中的阶段元素。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1896,6 +2031,22 @@ public class YamlConfigLoaderTests
|
|||||||
public string MonsterId { get; set; } = string.Empty;
|
public string MonsterId { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示用于比较键碰撞回归测试的对象数组元素。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ComparableEntryConfigStub
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置字段 A。
|
||||||
|
/// </summary>
|
||||||
|
public string A { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置字段 B。
|
||||||
|
/// </summary>
|
||||||
|
public string B { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用于深层跨表引用测试的怪物配置类型。
|
/// 用于深层跨表引用测试的怪物配置类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -708,7 +708,7 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
|
|||||||
- `exclusiveMinimum` / `exclusiveMaximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
- `exclusiveMinimum` / `exclusiveMaximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前按运行时与 JS 共用的浮点容差策略判断十进制步进
|
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前按运行时与 JS 共用的浮点容差策略判断十进制步进
|
||||||
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
- `pattern`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用;当前按 C# `CultureInvariant` 与 JS 默认分组语义解释,非法模式会在 schema 解析阶段直接报错
|
- `pattern`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用;当前按 C# `CultureInvariant` 与 JS Unicode `u` 模式解释,非法模式会在 schema 解析阶段直接报错
|
||||||
- `minItems` / `maxItems`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
- `minItems` / `maxItems`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
||||||
- `uniqueItems`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象数组会按 schema 归一化后的结构比较重复项,而不是依赖 YAML 字段顺序
|
- `uniqueItems`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象数组会按 schema 归一化后的结构比较重复项,而不是依赖 YAML 字段顺序
|
||||||
|
|
||||||
@ -807,7 +807,7 @@ var hotReload = loader.EnableHotReload(
|
|||||||
- 对带 `x-gframework-ref-table` 的字段提供引用 schema / 配置域 / 引用文件跳转入口
|
- 对带 `x-gframework-ref-table` 的字段提供引用 schema / 配置域 / 引用文件跳转入口
|
||||||
- 对空配置文件提供基于 schema 的示例 YAML 初始化入口
|
- 对空配置文件提供基于 schema 的示例 YAML 初始化入口
|
||||||
- 对同一配置域内的多份 YAML 文件执行批量字段更新
|
- 对同一配置域内的多份 YAML 文件执行批量字段更新
|
||||||
- 在表单和批量编辑入口中显示 `title / description / default / enum / ref-table / multipleOf / uniqueItems` 元数据
|
- 在表单入口中显示 `title / description / default / enum / ref-table / multipleOf / uniqueItems` 元数据;批量编辑入口当前只暴露顶层可批量改写字段所需的基础信息
|
||||||
|
|
||||||
当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。
|
当前表单入口适合编辑嵌套对象中的标量字段、标量数组,以及对象数组中的对象项。
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,10 @@ const {
|
|||||||
} = require("./configPath");
|
} = require("./configPath");
|
||||||
const {ValidationMessageKeys} = require("./localizationKeys");
|
const {ValidationMessageKeys} = require("./localizationKeys");
|
||||||
|
|
||||||
|
const IntegerScalarPattern = /^[+-]?\d+$/u;
|
||||||
|
const NumberScalarPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u;
|
||||||
|
const BooleanScalarPattern = /^(true|false)$/iu;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the repository's minimal config-schema subset into a recursive tree.
|
* Parse the repository's minimal config-schema subset into a recursive tree.
|
||||||
* The parser intentionally mirrors the same high-level contract used by the
|
* The parser intentionally mirrors the same high-level contract used by the
|
||||||
@ -262,11 +266,11 @@ function isScalarCompatible(expectedType, scalarValue) {
|
|||||||
const value = unquoteScalar(String(scalarValue));
|
const value = unquoteScalar(String(scalarValue));
|
||||||
switch (expectedType) {
|
switch (expectedType) {
|
||||||
case "integer":
|
case "integer":
|
||||||
return /^-?\d+$/u.test(value);
|
return IntegerScalarPattern.test(value);
|
||||||
case "number":
|
case "number":
|
||||||
return /^-?\d+(?:\.\d+)?$/u.test(value);
|
return NumberScalarPattern.test(value);
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return /^(true|false)$/iu.test(value);
|
return BooleanScalarPattern.test(value);
|
||||||
case "string":
|
case "string":
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
@ -407,7 +411,7 @@ function normalizeSchemaBoolean(value) {
|
|||||||
* @param {unknown} value Raw schema value.
|
* @param {unknown} value Raw schema value.
|
||||||
* @param {string} displayPath Logical property path used in diagnostics.
|
* @param {string} displayPath Logical property path used in diagnostics.
|
||||||
* @throws {Error} Thrown when the pattern string cannot be compiled.
|
* @throws {Error} Thrown when the pattern string cannot be compiled.
|
||||||
* @returns {string | undefined} Normalized pattern string.
|
* @returns {{source: string, regex: RegExp} | undefined} Normalized pattern metadata.
|
||||||
*/
|
*/
|
||||||
function normalizeSchemaPattern(value, displayPath) {
|
function normalizeSchemaPattern(value, displayPath) {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
@ -415,8 +419,10 @@ function normalizeSchemaPattern(value, displayPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
void new RegExp(value);
|
return {
|
||||||
return value;
|
source: value,
|
||||||
|
regex: new RegExp(value, "u")
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Schema property '${displayPath}' declares an invalid 'pattern' regular expression: ${error.message}`);
|
throw new Error(`Schema property '${displayPath}' declares an invalid 'pattern' regular expression: ${error.message}`);
|
||||||
}
|
}
|
||||||
@ -454,24 +460,18 @@ function formatSchemaDefaultValue(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test one scalar value against one schema pattern string.
|
* Test one scalar value against one compiled schema pattern.
|
||||||
*
|
*
|
||||||
* @param {string} scalarValue Scalar value from YAML.
|
* @param {string} scalarValue Scalar value from YAML.
|
||||||
* @param {string | undefined} pattern Schema pattern string.
|
* @param {RegExp | undefined} patternRegex Compiled schema pattern.
|
||||||
* @param {string} displayPath Logical property path used in diagnostics.
|
|
||||||
* @throws {Error} Thrown when the pattern string cannot be compiled.
|
|
||||||
* @returns {boolean} True when the value matches or no pattern is declared.
|
* @returns {boolean} True when the value matches or no pattern is declared.
|
||||||
*/
|
*/
|
||||||
function matchesSchemaPattern(scalarValue, pattern, displayPath) {
|
function matchesSchemaPattern(scalarValue, patternRegex) {
|
||||||
if (typeof pattern !== "string") {
|
if (!(patternRegex instanceof RegExp)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return patternRegex.test(scalarValue);
|
||||||
return new RegExp(pattern).test(scalarValue);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Schema property '${displayPath}' declares an invalid 'pattern' regular expression: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -500,7 +500,7 @@ function matchesSchemaMultipleOf(scalarValue, multipleOf) {
|
|||||||
* @returns {string} YAML-ready scalar.
|
* @returns {string} YAML-ready scalar.
|
||||||
*/
|
*/
|
||||||
function formatYamlScalar(value) {
|
function formatYamlScalar(value) {
|
||||||
if (/^-?\d+(?:\.\d+)?$/u.test(value) || /^(true|false)$/iu.test(value)) {
|
if (NumberScalarPattern.test(value) || BooleanScalarPattern.test(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,6 +536,7 @@ function unquoteScalar(value) {
|
|||||||
function parseSchemaNode(rawNode, displayPath) {
|
function parseSchemaNode(rawNode, displayPath) {
|
||||||
const value = rawNode && typeof rawNode === "object" ? rawNode : {};
|
const value = rawNode && typeof rawNode === "object" ? rawNode : {};
|
||||||
const type = typeof value.type === "string" ? value.type : "object";
|
const type = typeof value.type === "string" ? value.type : "object";
|
||||||
|
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
||||||
const metadata = {
|
const metadata = {
|
||||||
title: typeof value.title === "string" ? value.title : undefined,
|
title: typeof value.title === "string" ? value.title : undefined,
|
||||||
description: typeof value.description === "string" ? value.description : undefined,
|
description: typeof value.description === "string" ? value.description : undefined,
|
||||||
@ -547,7 +548,8 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
multipleOf: normalizeSchemaPositiveNumber(value.multipleOf),
|
multipleOf: normalizeSchemaPositiveNumber(value.multipleOf),
|
||||||
minLength: normalizeSchemaNonNegativeInteger(value.minLength),
|
minLength: normalizeSchemaNonNegativeInteger(value.minLength),
|
||||||
maxLength: normalizeSchemaNonNegativeInteger(value.maxLength),
|
maxLength: normalizeSchemaNonNegativeInteger(value.maxLength),
|
||||||
pattern: normalizeSchemaPattern(value.pattern, displayPath),
|
pattern: patternMetadata ? patternMetadata.source : undefined,
|
||||||
|
patternRegex: patternMetadata ? patternMetadata.regex : undefined,
|
||||||
minItems: normalizeSchemaNonNegativeInteger(value.minItems),
|
minItems: normalizeSchemaNonNegativeInteger(value.minItems),
|
||||||
maxItems: normalizeSchemaNonNegativeInteger(value.maxItems),
|
maxItems: normalizeSchemaNonNegativeInteger(value.maxItems),
|
||||||
uniqueItems: normalizeSchemaBoolean(value.uniqueItems),
|
uniqueItems: normalizeSchemaBoolean(value.uniqueItems),
|
||||||
@ -622,6 +624,9 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
pattern: type === "string"
|
pattern: type === "string"
|
||||||
? metadata.pattern
|
? metadata.pattern
|
||||||
: undefined,
|
: undefined,
|
||||||
|
patternRegex: type === "string"
|
||||||
|
? metadata.patternRegex
|
||||||
|
: undefined,
|
||||||
enumValues: normalizeSchemaEnumValues(value.enum),
|
enumValues: normalizeSchemaEnumValues(value.enum),
|
||||||
refTable: metadata.refTable
|
refTable: metadata.refTable
|
||||||
};
|
};
|
||||||
@ -675,19 +680,27 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const comparableItems = [];
|
||||||
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
||||||
|
const diagnosticsBeforeValidation = diagnostics.length;
|
||||||
validateNode(
|
validateNode(
|
||||||
schemaNode.items,
|
schemaNode.items,
|
||||||
yamlNode.items[index],
|
yamlNode.items[index],
|
||||||
joinArrayIndexPath(displayPath, index),
|
joinArrayIndexPath(displayPath, index),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
localizer);
|
localizer);
|
||||||
|
|
||||||
|
// Keep uniqueItems focused on values that are otherwise valid so a
|
||||||
|
// shape/type error does not also surface as a misleading duplicate.
|
||||||
|
if (diagnostics.length === diagnosticsBeforeValidation) {
|
||||||
|
comparableItems.push({index, node: yamlNode.items[index]});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.uniqueItems === true) {
|
if (schemaNode.uniqueItems === true) {
|
||||||
const seenItems = new Map();
|
const seenItems = new Map();
|
||||||
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
for (const {index, node} of comparableItems) {
|
||||||
const comparableValue = buildComparableNodeValue(schemaNode.items, yamlNode.items[index]);
|
const comparableValue = buildComparableNodeValue(schemaNode.items, node);
|
||||||
if (seenItems.has(comparableValue)) {
|
if (seenItems.has(comparableValue)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
@ -696,7 +709,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
duplicatePath: joinArrayIndexPath(displayPath, seenItems.get(comparableValue))
|
duplicatePath: joinArrayIndexPath(displayPath, seenItems.get(comparableValue))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
seenItems.set(comparableValue, index);
|
seenItems.set(comparableValue, index);
|
||||||
@ -830,7 +843,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (supportsPatternConstraints &&
|
if (supportsPatternConstraints &&
|
||||||
!matchesSchemaPattern(scalarValue, schemaNode.pattern, schemaNode.displayPath)) {
|
!matchesSchemaPattern(scalarValue, schemaNode.patternRegex)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage(ValidationMessageKeys.patternViolation, localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.patternViolation, localizer, {
|
||||||
@ -920,7 +933,10 @@ function buildComparableNodeValue(schemaNode, yamlNode) {
|
|||||||
return Object.keys(schemaNode.properties)
|
return Object.keys(schemaNode.properties)
|
||||||
.filter((key) => yamlNode.map.has(key))
|
.filter((key) => yamlNode.map.has(key))
|
||||||
.sort((left, right) => left.localeCompare(right))
|
.sort((left, right) => left.localeCompare(right))
|
||||||
.map((key) => `${key.length}:${key}=${buildComparableNodeValue(schemaNode.properties[key], yamlNode.map.get(key))}`)
|
.map((key) => {
|
||||||
|
const valueKey = buildComparableNodeValue(schemaNode.properties[key], yamlNode.map.get(key));
|
||||||
|
return `${key.length}:${key}=${valueKey.length}:${valueKey}`;
|
||||||
|
})
|
||||||
.join("|");
|
.join("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -929,7 +945,10 @@ function buildComparableNodeValue(schemaNode, yamlNode) {
|
|||||||
return yamlNode.kind;
|
return yamlNode.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `[${yamlNode.items.map((item) => buildComparableNodeValue(schemaNode.items, item)).join(",")}]`;
|
return `[${yamlNode.items.map((item) => {
|
||||||
|
const valueKey = buildComparableNodeValue(schemaNode.items, item);
|
||||||
|
return `${valueKey.length}:${valueKey}`;
|
||||||
|
}).join(",")}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yamlNode.kind !== "scalar") {
|
if (yamlNode.kind !== "scalar") {
|
||||||
@ -942,7 +961,7 @@ function buildComparableNodeValue(schemaNode, yamlNode) {
|
|||||||
: schemaNode.type === "boolean"
|
: schemaNode.type === "boolean"
|
||||||
? String(/^true$/iu.test(scalarValue))
|
? String(/^true$/iu.test(scalarValue))
|
||||||
: scalarValue;
|
: scalarValue;
|
||||||
return `${schemaNode.type}:${normalizedScalar}`;
|
return `${schemaNode.type}:${normalizedScalar.length}:${normalizedScalar}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1704,6 +1723,7 @@ module.exports = {
|
|||||||
* minLength?: number,
|
* minLength?: number,
|
||||||
* maxLength?: number,
|
* maxLength?: number,
|
||||||
* pattern?: string,
|
* pattern?: string,
|
||||||
|
* patternRegex?: RegExp,
|
||||||
* enumValues?: string[],
|
* enumValues?: string[],
|
||||||
* refTable?: string
|
* refTable?: string
|
||||||
* }} SchemaNode
|
* }} SchemaNode
|
||||||
|
|||||||
@ -349,6 +349,135 @@ phases:
|
|||||||
assert.match(diagnostics[1].message, /phases\[1\]|uniqueItems|元素唯一/u);
|
assert.match(diagnostics[1].message, /phases\[1\]|uniqueItems|元素唯一/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should accept scientific-notation numbers", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dropRate": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
dropRate: 1.5e10
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.deepEqual(validateParsedConfig(schema, yaml), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should apply schema patterns with Unicode semantics", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\\\p{L}+$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
name: 测试
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.deepEqual(validateParsedConfig(schema, yaml), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should skip uniqueItems checks for invalid array items", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"values": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
values:
|
||||||
|
-
|
||||||
|
id: 1
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 2);
|
||||||
|
assert.match(diagnostics[0].message, /values\[0\]/u);
|
||||||
|
assert.match(diagnostics[1].message, /values\[1\]/u);
|
||||||
|
assert.ok(diagnostics.every((diagnostic) => !/uniqueItems|元素唯一/u.test(diagnostic.message)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should report every uniqueItems duplicate in one pass", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
tags:
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 2);
|
||||||
|
assert.match(diagnostics[0].message, /tags\[2\]/u);
|
||||||
|
assert.match(diagnostics[1].message, /tags\[3\]/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should avoid uniqueItems comparable-key collisions for distinct objects", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"a": { "type": "string" },
|
||||||
|
"b": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
entries:
|
||||||
|
-
|
||||||
|
a: "x|1:b=string:yz"
|
||||||
|
-
|
||||||
|
a: x
|
||||||
|
b: yz
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.deepEqual(validateParsedConfig(schema, yaml), []);
|
||||||
|
});
|
||||||
|
|
||||||
test("parseSchemaContent should capture scalar range and length metadata", () => {
|
test("parseSchemaContent should capture scalar range and length metadata", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user