mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(config): 添加游戏内容配置系统完整文档
- 新增 YAML 配置与 JSON Schema 结构描述支持 - 添加一对象一文件的目录组织方式说明 - 实现运行时只读查询功能详细文档 - 添加 Source Generator 生成配置类型的完整指南 - 集成 VS Code 插件提供配置浏览和校验功能 - 添加 Godot 文本配置桥接的使用说明 - 实现热重载模板和 Architecture 接入示例 - 添加运行时校验行为和错误处理机制 - 提供开发期热重载功能的详细配置方法 - 添加生成器接入约定和工具使用说明
This commit is contained in:
parent
a3f5010247
commit
809e1f5ded
295
GFramework.Game.Tests/Config/YamlConfigLoaderEnumTests.cs
Normal file
295
GFramework.Game.Tests/Config/YamlConfigLoaderEnumTests.cs
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
using System.IO;
|
||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
using GFramework.Game.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 YAML 配置加载器对对象 / 数组 <c>enum</c> 约束的运行时行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class YamlConfigLoaderEnumTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
|
||||||
|
/// </summary>
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigTests", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清理测试期间创建的临时目录。
|
||||||
|
/// </summary>
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_rootPath))
|
||||||
|
{
|
||||||
|
Directory.Delete(_rootPath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _rootPath = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象 <c>enum</c> 会按字段名排序后的稳定比较键匹配,而不是依赖 schema 内的 JSON 字段顺序。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task LoadAsync_Should_Accept_Object_Value_Declared_In_Schema_Enum_When_Property_Order_Differs()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
itemId: potion
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "reward"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["gold", "itemId"],
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"itemId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"enum": [
|
||||||
|
{ "itemId": "potion", "gold": 10 },
|
||||||
|
{ "gold": 50, "itemId": "gem" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = CreateLoader<MonsterRewardConfigStub>();
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var table = registry.GetTable<int, MonsterRewardConfigStub>("monster");
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(table.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(table.Get(1).Reward.Gold, Is.EqualTo(10));
|
||||||
|
Assert.That(table.Get(1).Reward.ItemId, Is.EqualTo("potion"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象 <c>enum</c> 不匹配时,运行时会拒绝整个对象值并输出候选 JSON 文本。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Object_Value_Is_Not_Declared_In_Schema_Enum()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
itemId: elixir
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "reward"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["gold", "itemId"],
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"itemId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"enum": [
|
||||||
|
{ "gold": 10, "itemId": "potion" },
|
||||||
|
{ "gold": 50, "itemId": "gem" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = CreateLoader<MonsterRewardConfigStub>();
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Message, Does.Contain("reward"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("\"itemId\": \"potion\""));
|
||||||
|
Assert.That(exception.Message, Does.Contain("\"itemId\": \"gem\""));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证数组 <c>enum</c> 会保留元素顺序;同一批元素但顺序不同仍视为不同候选值。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Array_Value_Order_Does_Not_Match_Schema_Enum()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
dropItemIds:
|
||||||
|
- ice
|
||||||
|
- fire
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "dropItemIds"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"enum": [
|
||||||
|
["fire", "ice"],
|
||||||
|
["earth"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = CreateLoader<MonsterDropItemIdsConfigStub>();
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Message, Does.Contain("dropItemIds"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("[\"fire\", \"ice\"]"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("[\"earth\"]"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建带 schema 校验的测试加载器。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TConfig">配置类型。</typeparam>
|
||||||
|
/// <returns>已注册 monster 表的加载器。</returns>
|
||||||
|
private YamlConfigLoader CreateLoader<TConfig>()
|
||||||
|
where TConfig : IHasMonsterId
|
||||||
|
{
|
||||||
|
return new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, TConfig>("monster", "monster", "schemas/monster.schema.json", static config => config.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建测试配置文件。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="relativePath">相对路径。</param>
|
||||||
|
/// <param name="content">文件内容。</param>
|
||||||
|
private void CreateConfigFile(string relativePath, string content)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
var directoryPath = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建测试 schema 文件。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="relativePath">相对路径。</param>
|
||||||
|
/// <param name="content">文件内容。</param>
|
||||||
|
private void CreateSchemaFile(string relativePath, string content)
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
var directoryPath = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为通用测试加载器暴露统一主键访问约定。
|
||||||
|
/// </summary>
|
||||||
|
private interface IHasMonsterId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置主键。
|
||||||
|
/// </summary>
|
||||||
|
int Id { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 供对象 <c>enum</c> 测试使用的配置桩。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MonsterRewardConfigStub : IHasMonsterId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置主键。
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置奖励对象。
|
||||||
|
/// </summary>
|
||||||
|
public RewardConfigStub Reward { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 供数组 <c>enum</c> 测试使用的配置桩。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MonsterDropItemIdsConfigStub : IHasMonsterId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置主键。
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置掉落物数组。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> DropItemIds { get; set; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 供对象 <c>enum</c> 测试使用的奖励配置桩。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class RewardConfigStub
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置金币数量。
|
||||||
|
/// </summary>
|
||||||
|
public int Gold { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置道具标识。
|
||||||
|
/// </summary>
|
||||||
|
public string ItemId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -462,6 +462,8 @@ internal static class YamlConfigSchemaValidator
|
|||||||
requiredProperties,
|
requiredProperties,
|
||||||
ParseObjectConstraints(tableName, schemaPath, propertyPath, element),
|
ParseObjectConstraints(tableName, schemaPath, propertyPath, element),
|
||||||
schemaPath);
|
schemaPath);
|
||||||
|
objectNode = objectNode.WithAllowedValues(
|
||||||
|
ParseEnumValues(tableName, schemaPath, propertyPath, element, objectNode, "enum"));
|
||||||
return objectNode.WithConstantValue(
|
return objectNode.WithConstantValue(
|
||||||
ParseConstantValue(tableName, schemaPath, propertyPath, element, objectNode));
|
ParseConstantValue(tableName, schemaPath, propertyPath, element, objectNode));
|
||||||
}
|
}
|
||||||
@ -525,8 +527,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
var arrayNode = YamlConfigSchemaNode.CreateArray(
|
var arrayNode = YamlConfigSchemaNode.CreateArray(
|
||||||
itemNode,
|
itemNode,
|
||||||
|
allowedValues: null,
|
||||||
ParseArrayConstraints(tableName, schemaPath, propertyPath, element),
|
ParseArrayConstraints(tableName, schemaPath, propertyPath, element),
|
||||||
schemaPath);
|
schemaPath);
|
||||||
|
arrayNode = arrayNode.WithAllowedValues(
|
||||||
|
ParseEnumValues(tableName, schemaPath, propertyPath, element, arrayNode, "enum"));
|
||||||
return arrayNode.WithConstantValue(
|
return arrayNode.WithConstantValue(
|
||||||
ParseConstantValue(tableName, schemaPath, propertyPath, element, arrayNode));
|
ParseConstantValue(tableName, schemaPath, propertyPath, element, arrayNode));
|
||||||
}
|
}
|
||||||
@ -553,9 +558,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
var scalarNode = YamlConfigSchemaNode.CreateScalar(
|
var scalarNode = YamlConfigSchemaNode.CreateScalar(
|
||||||
nodeType,
|
nodeType,
|
||||||
referenceTableName,
|
referenceTableName,
|
||||||
ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"),
|
allowedValues: null,
|
||||||
ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
|
ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
|
||||||
schemaPath);
|
schemaPath);
|
||||||
|
scalarNode = scalarNode.WithAllowedValues(
|
||||||
|
ParseEnumValues(tableName, schemaPath, propertyPath, element, scalarNode, "enum"));
|
||||||
return scalarNode.WithConstantValue(
|
return scalarNode.WithConstantValue(
|
||||||
ParseConstantValue(tableName, schemaPath, propertyPath, element, scalarNode));
|
ParseConstantValue(tableName, schemaPath, propertyPath, element, scalarNode));
|
||||||
}
|
}
|
||||||
@ -794,6 +801,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
ValidateObjectConstraints(tableName, yamlPath, displayPath, seenProperties.Count, schemaNode);
|
ValidateObjectConstraints(tableName, yamlPath, displayPath, seenProperties.Count, schemaNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
|
||||||
}
|
}
|
||||||
@ -917,6 +925,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
|
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
|
||||||
|
ValidateAllowedValues(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
}
|
}
|
||||||
@ -992,19 +1001,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
||||||
if (schemaNode.AllowedValues is { Count: > 0 } &&
|
ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode);
|
||||||
!schemaNode.AllowedValues.Contains(normalizedValue, StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.EnumValueNotAllowed,
|
|
||||||
tableName,
|
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be one of [{string.Join(", ", schemaNode.AllowedValues)}], but the current YAML scalar value is '{value}'.",
|
|
||||||
yamlPath: yamlPath,
|
|
||||||
schemaPath: schemaNode.SchemaPathHint,
|
|
||||||
displayPath: GetDiagnosticPath(displayPath),
|
|
||||||
rawValue: value,
|
|
||||||
detail: $"Allowed values: {string.Join(", ", schemaNode.AllowedValues)}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schemaNode.Constraints is not null)
|
if (schemaNode.Constraints is not null)
|
||||||
{
|
{
|
||||||
@ -1029,21 +1026,22 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析 enum,并在读取阶段验证枚举值与字段类型的兼容性。
|
/// 解析 <c>enum</c>,并在读取阶段将每个候选值预归一化成与运行时 YAML 相同的稳定比较键。
|
||||||
|
/// 这样对象、数组和标量节点都可以复用同一套匹配逻辑,而不必在每次加载时重新解释 schema 字面量。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tableName">所属配置表名称。</param>
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">字段路径。</param>
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
/// <param name="element">Schema 节点。</param>
|
/// <param name="element">Schema 节点。</param>
|
||||||
/// <param name="expectedType">期望的标量类型。</param>
|
/// <param name="schemaNode">当前节点的已解析 schema 模型。</param>
|
||||||
/// <param name="keywordName">当前读取的关键字名称。</param>
|
/// <param name="keywordName">当前读取的关键字名称。</param>
|
||||||
/// <returns>归一化后的枚举值集合;未声明时返回空。</returns>
|
/// <returns>归一化后的允许值集合;未声明时返回空。</returns>
|
||||||
private static IReadOnlyCollection<string>? ParseEnumValues(
|
private static IReadOnlyCollection<YamlConfigAllowedValue>? ParseEnumValues(
|
||||||
string tableName,
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
YamlConfigSchemaPropertyType expectedType,
|
YamlConfigSchemaNode schemaNode,
|
||||||
string keywordName)
|
string keywordName)
|
||||||
{
|
{
|
||||||
if (!element.TryGetProperty("enum", out var enumElement))
|
if (!element.TryGetProperty("enum", out var enumElement))
|
||||||
@ -1061,16 +1059,29 @@ internal static class YamlConfigSchemaValidator
|
|||||||
displayPath: GetDiagnosticPath(propertyPath));
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedValues = new List<string>();
|
var allowedValues = new List<YamlConfigAllowedValue>();
|
||||||
foreach (var item in enumElement.EnumerateArray())
|
foreach (var item in enumElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
allowedValues.Add(
|
allowedValues.Add(
|
||||||
NormalizeKeywordScalarValue(tableName, schemaPath, propertyPath, keywordName, expectedType, item));
|
new YamlConfigAllowedValue(
|
||||||
|
BuildComparableConstantValue(tableName, schemaPath, propertyPath, keywordName, item, schemaNode),
|
||||||
|
BuildEnumDisplayValue(item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return allowedValues;
|
return allowedValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取一个 <c>enum</c> 候选值的展示文本。
|
||||||
|
/// 这里直接保留原始 JSON 字面量,避免字符串引号、对象字段顺序或数组结构在诊断中被二次格式化后丢失上下文。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Schema 中的单个枚举值。</param>
|
||||||
|
/// <returns>适合放入诊断消息的展示文本。</returns>
|
||||||
|
private static string BuildEnumDisplayValue(JsonElement element)
|
||||||
|
{
|
||||||
|
return element.GetRawText();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析 <c>const</c>,并把 schema 常量预归一化成与运行时 YAML 相同的稳定比较键。
|
/// 解析 <c>const</c>,并把 schema 常量预归一化成与运行时 YAML 相同的稳定比较键。
|
||||||
/// 这样运行时只需要复用现有递归比较逻辑,而不必在每次加载时重新解释 JSON 常量。
|
/// 这样运行时只需要复用现有递归比较逻辑,而不必在每次加载时重新解释 JSON 常量。
|
||||||
@ -2084,6 +2095,50 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验节点值是否命中声明的 <c>enum</c> 集合。
|
||||||
|
/// 该实现与 <c>const</c> 共享同一套可比较键,保证对象字段顺序、数组顺序和数值归一化语义稳定一致。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
|
/// <param name="displayPath">字段路径;根节点时为空。</param>
|
||||||
|
/// <param name="node">当前 YAML 节点。</param>
|
||||||
|
/// <param name="schemaNode">对应的 schema 节点。</param>
|
||||||
|
private static void ValidateAllowedValues(
|
||||||
|
string tableName,
|
||||||
|
string yamlPath,
|
||||||
|
string displayPath,
|
||||||
|
YamlNode node,
|
||||||
|
YamlConfigSchemaNode schemaNode)
|
||||||
|
{
|
||||||
|
var allowedValues = schemaNode.AllowedValues;
|
||||||
|
if (allowedValues is null || allowedValues.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparableValue = BuildComparableNodeValue(node, schemaNode);
|
||||||
|
if (allowedValues.Any(allowedValue =>
|
||||||
|
string.Equals(allowedValue.ComparableValue, comparableValue, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subject = string.IsNullOrWhiteSpace(displayPath)
|
||||||
|
? "Root object"
|
||||||
|
: $"Property '{displayPath}'";
|
||||||
|
var displayValues = string.Join(", ", allowedValues.Select(static allowedValue => allowedValue.DisplayValue));
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.EnumValueNotAllowed,
|
||||||
|
tableName,
|
||||||
|
$"{subject} in config file '{yamlPath}' must be one of [{displayValues}].",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode),
|
||||||
|
detail: $"Allowed values: {displayValues}.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验节点值是否满足 <c>const</c> 约束。
|
/// 校验节点值是否满足 <c>const</c> 约束。
|
||||||
/// 该检查复用与 <c>uniqueItems</c> 相同的稳定比较键,保证对象字段顺序、数字字面量和布尔大小写不会造成伪差异。
|
/// 该检查复用与 <c>uniqueItems</c> 相同的稳定比较键,保证对象字段顺序、数字字面量和布尔大小写不会造成伪差异。
|
||||||
@ -3482,9 +3537,9 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
public string? ReferenceTableName { get; }
|
public string? ReferenceTableName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取标量允许值集合;未声明 enum 时返回空。
|
/// 获取节点允许值集合;未声明 <c>enum</c> 时返回空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<string>? AllowedValues { get; }
|
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取标量范围与长度约束;未声明时返回空。
|
/// 获取标量范围与长度约束;未声明时返回空。
|
||||||
@ -3549,11 +3604,13 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
/// 创建数组节点描述。
|
/// 创建数组节点描述。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemNode">数组元素节点。</param>
|
/// <param name="itemNode">数组元素节点。</param>
|
||||||
|
/// <param name="allowedValues">数组节点允许值集合。</param>
|
||||||
/// <param name="arrayConstraints">数组元素数量约束。</param>
|
/// <param name="arrayConstraints">数组元素数量约束。</param>
|
||||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||||
/// <returns>数组节点模型。</returns>
|
/// <returns>数组节点模型。</returns>
|
||||||
public static YamlConfigSchemaNode CreateArray(
|
public static YamlConfigSchemaNode CreateArray(
|
||||||
YamlConfigSchemaNode itemNode,
|
YamlConfigSchemaNode itemNode,
|
||||||
|
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||||
YamlConfigArrayConstraints? arrayConstraints,
|
YamlConfigArrayConstraints? arrayConstraints,
|
||||||
string schemaPathHint)
|
string schemaPathHint)
|
||||||
{
|
{
|
||||||
@ -3562,7 +3619,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
new NodeChildren(properties: null, requiredProperties: null, itemNode),
|
new NodeChildren(properties: null, requiredProperties: null, itemNode),
|
||||||
new NodeValidation(
|
new NodeValidation(
|
||||||
referenceTableName: null,
|
referenceTableName: null,
|
||||||
allowedValues: null,
|
allowedValues,
|
||||||
constraints: null,
|
constraints: null,
|
||||||
arrayConstraints,
|
arrayConstraints,
|
||||||
objectConstraints: null,
|
objectConstraints: null,
|
||||||
@ -3583,7 +3640,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
public static YamlConfigSchemaNode CreateScalar(
|
public static YamlConfigSchemaNode CreateScalar(
|
||||||
YamlConfigSchemaPropertyType nodeType,
|
YamlConfigSchemaPropertyType nodeType,
|
||||||
string? referenceTableName,
|
string? referenceTableName,
|
||||||
IReadOnlyCollection<string>? allowedValues,
|
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||||
YamlConfigScalarConstraints? constraints,
|
YamlConfigScalarConstraints? constraints,
|
||||||
string schemaPathHint)
|
string schemaPathHint)
|
||||||
{
|
{
|
||||||
@ -3616,6 +3673,20 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
SchemaPathHint);
|
SchemaPathHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于当前节点复制一个只替换 <c>enum</c> 允许值集合的新节点。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allowedValues">新的允许值集合。</param>
|
||||||
|
/// <returns>复制后的节点。</returns>
|
||||||
|
public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||||
|
{
|
||||||
|
return new YamlConfigSchemaNode(
|
||||||
|
NodeType,
|
||||||
|
_children,
|
||||||
|
_validation.WithAllowedValues(allowedValues),
|
||||||
|
SchemaPathHint);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 基于当前节点复制一个只替换常量约束的新节点。
|
/// 基于当前节点复制一个只替换常量约束的新节点。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -3669,7 +3740,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
{
|
{
|
||||||
public NodeValidation(
|
public NodeValidation(
|
||||||
string? referenceTableName,
|
string? referenceTableName,
|
||||||
IReadOnlyCollection<string>? allowedValues,
|
IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues,
|
||||||
YamlConfigScalarConstraints? constraints,
|
YamlConfigScalarConstraints? constraints,
|
||||||
YamlConfigArrayConstraints? arrayConstraints,
|
YamlConfigArrayConstraints? arrayConstraints,
|
||||||
YamlConfigObjectConstraints? objectConstraints,
|
YamlConfigObjectConstraints? objectConstraints,
|
||||||
@ -3696,7 +3767,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
|
|
||||||
public string? ReferenceTableName { get; }
|
public string? ReferenceTableName { get; }
|
||||||
|
|
||||||
public IReadOnlyCollection<string>? AllowedValues { get; }
|
public IReadOnlyCollection<YamlConfigAllowedValue>? AllowedValues { get; }
|
||||||
|
|
||||||
public YamlConfigScalarConstraints? Constraints { get; }
|
public YamlConfigScalarConstraints? Constraints { get; }
|
||||||
|
|
||||||
@ -3714,6 +3785,12 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NodeValidation WithAllowedValues(IReadOnlyCollection<YamlConfigAllowedValue>? allowedValues)
|
||||||
|
{
|
||||||
|
return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints,
|
||||||
|
ObjectConstraints, ConstantValue, NegatedSchemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue)
|
public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue)
|
||||||
{
|
{
|
||||||
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints,
|
||||||
@ -3759,6 +3836,37 @@ internal sealed class YamlConfigConstantValue
|
|||||||
public string DisplayValue { get; }
|
public string DisplayValue { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一个节点上声明的单个 <c>enum</c> 候选值。
|
||||||
|
/// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class YamlConfigAllowedValue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化一个枚举候选值模型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comparableValue">用于与 YAML 节点比较的稳定键。</param>
|
||||||
|
/// <param name="displayValue">用于诊断输出的原始 JSON 文本。</param>
|
||||||
|
public YamlConfigAllowedValue(string comparableValue, string displayValue)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(comparableValue);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(displayValue);
|
||||||
|
|
||||||
|
ComparableValue = comparableValue;
|
||||||
|
DisplayValue = displayValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用于运行时比较的稳定键。
|
||||||
|
/// </summary>
|
||||||
|
public string ComparableValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用于诊断输出的原始 JSON 文本。
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayValue { get; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示一个对象节点上声明的属性数量约束。
|
/// 表示一个对象节点上声明的属性数量约束。
|
||||||
/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。
|
/// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 schema 配置生成器对对象 / 数组 <c>enum</c> 文档输出的行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class SchemaConfigGeneratorEnumTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象 <c>enum</c> 会以原始 JSON 文本写入生成代码 XML 文档。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Write_Object_Enum_Into_Generated_Documentation()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class Dummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "reward"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["gold", "itemId"],
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"itemId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"enum": [
|
||||||
|
{ "gold": 10, "itemId": "potion" },
|
||||||
|
{ "gold": 50, "itemId": "gem" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", schema));
|
||||||
|
|
||||||
|
var generatedSources = result.Results
|
||||||
|
.Single()
|
||||||
|
.GeneratedSources
|
||||||
|
.ToDictionary(
|
||||||
|
static sourceResult => sourceResult.HintName,
|
||||||
|
static sourceResult => sourceResult.SourceText.ToString(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
|
Assert.That(generatedSources["MonsterConfig.g.cs"],
|
||||||
|
Does.Contain("Allowed values: { \"gold\": 10, \"itemId\": \"potion\" }, { \"gold\": 50, \"itemId\": \"gem\" }."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证数组 <c>enum</c> 会以保留顺序的 JSON 数组文本写入生成代码 XML 文档。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Write_Array_Enum_Into_Generated_Documentation()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class Dummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "dropItemIds"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"enum": [
|
||||||
|
["fire", "ice"],
|
||||||
|
["earth"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", schema));
|
||||||
|
|
||||||
|
var generatedSources = result.Results
|
||||||
|
.Single()
|
||||||
|
.GeneratedSources
|
||||||
|
.ToDictionary(
|
||||||
|
static sourceResult => sourceResult.HintName,
|
||||||
|
static sourceResult => sourceResult.SourceText.ToString(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
|
Assert.That(generatedSources["MonsterConfig.g.cs"],
|
||||||
|
Does.Contain("Allowed values: [\"fire\", \"ice\"], [\"earth\"]."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -478,7 +478,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"object",
|
"object",
|
||||||
isRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?",
|
isRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?",
|
||||||
isRequired ? " = new();" : null,
|
isRequired ? " = new();" : null,
|
||||||
null,
|
TryBuildEnumDocumentation(property.Value, "object"),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
objectSpec,
|
objectSpec,
|
||||||
@ -805,7 +805,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
$"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>",
|
$"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>",
|
||||||
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
|
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
|
||||||
$" = global::System.Array.Empty<{itemClrType}>();",
|
$" = global::System.Array.Empty<{itemClrType}>();",
|
||||||
TryBuildEnumDocumentation(itemsElement, itemType),
|
TryBuildEnumDocumentation(property.Value, "array"),
|
||||||
TryBuildConstraintDocumentation(property.Value, "array"),
|
TryBuildConstraintDocumentation(property.Value, "array"),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
@ -856,7 +856,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"array",
|
"array",
|
||||||
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
|
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
|
||||||
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
|
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
|
||||||
null,
|
TryBuildEnumDocumentation(property.Value, "array"),
|
||||||
TryBuildConstraintDocumentation(property.Value, "array"),
|
TryBuildConstraintDocumentation(property.Value, "array"),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -2760,7 +2760,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
/// 将 enum 值整理成 XML 文档可读字符串。
|
/// 将 enum 值整理成 XML 文档可读字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="element">Schema 节点。</param>
|
/// <param name="element">Schema 节点。</param>
|
||||||
/// <param name="schemaType">标量类型。</param>
|
/// <param name="schemaType">当前 schema 类型。</param>
|
||||||
/// <returns>格式化后的枚举说明。</returns>
|
/// <returns>格式化后的枚举说明。</returns>
|
||||||
private static string? TryBuildEnumDocumentation(JsonElement element, string schemaType)
|
private static string? TryBuildEnumDocumentation(JsonElement element, string schemaType)
|
||||||
{
|
{
|
||||||
@ -2782,6 +2782,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"boolean" when item.ValueKind == JsonValueKind.True => "true",
|
"boolean" when item.ValueKind == JsonValueKind.True => "true",
|
||||||
"boolean" when item.ValueKind == JsonValueKind.False => "false",
|
"boolean" when item.ValueKind == JsonValueKind.False => "false",
|
||||||
"string" when item.ValueKind == JsonValueKind.String => item.GetString(),
|
"string" when item.ValueKind == JsonValueKind.String => item.GetString(),
|
||||||
|
"array" when item.ValueKind == JsonValueKind.Array => item.GetRawText(),
|
||||||
|
"object" when item.ValueKind == JsonValueKind.Object => item.GetRawText(),
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
- JSON Schema 作为结构描述
|
- JSON Schema 作为结构描述
|
||||||
- 一对象一文件的目录组织
|
- 一对象一文件的目录组织
|
||||||
- 运行时只读查询
|
- 运行时只读查询
|
||||||
- Runtime / Generator / Tooling 共享支持 `const`、`not`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`
|
- Runtime / Generator / Tooling 共享支持 `enum`、`const`、`not`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`duration`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`
|
||||||
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
||||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||||
|
|
||||||
@ -721,7 +721,7 @@ var loader = new YamlConfigLoader("config-root")
|
|||||||
- 对象字段违反 `minProperties` / `maxProperties`
|
- 对象字段违反 `minProperties` / `maxProperties`
|
||||||
- 标量 / 对象 / 数组字段违反 `const`
|
- 标量 / 对象 / 数组字段违反 `const`
|
||||||
- 标量 / 对象 / 数组字段命中 `not`
|
- 标量 / 对象 / 数组字段命中 `not`
|
||||||
- 标量 `enum` 不匹配
|
- 标量 / 对象 / 数组字段违反 `enum`
|
||||||
- 标量数组元素 `enum` 不匹配
|
- 标量数组元素 `enum` 不匹配
|
||||||
- 通过 `x-gframework-ref-table` 声明的跨表引用缺失目标行
|
- 通过 `x-gframework-ref-table` 声明的跨表引用缺失目标行
|
||||||
|
|
||||||
@ -769,7 +769,7 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
|
|||||||
- `default`:供生成类型属性初始值和工具提示复用
|
- `default`:供生成类型属性初始值和工具提示复用
|
||||||
- `const`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象会忽略字段顺序比较,数组保留元素顺序,标量按运行时同一套类型归一化规则比较
|
- `const`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象会忽略字段顺序比较,数组保留元素顺序,标量按运行时同一套类型归一化规则比较
|
||||||
- `not`:供运行时校验、VS Code 校验和生成代码 XML 文档复用;`not` 子 schema 会复用同一套递归校验规则,但对象匹配保持主校验链的严格语义,不会像 `contains` 那样把“声明属性子集”视为命中
|
- `not`:供运行时校验、VS Code 校验和生成代码 XML 文档复用;`not` 子 schema 会复用同一套递归校验规则,但对象匹配保持主校验链的严格语义,不会像 `contains` 那样把“声明属性子集”视为命中
|
||||||
- `enum`:供运行时校验、VS Code 校验和表单枚举选择复用
|
- `enum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用;当前标量、对象和数组节点都支持 `enum`,其中标量 `enum` 继续驱动表单枚举选择,对象 / 数组 `enum` 当前主要提供校验与文档约束
|
||||||
- `minimum` / `maximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
- `minimum` / `maximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
- `exclusiveMinimum` / `exclusiveMaximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
- `exclusiveMinimum` / `exclusiveMaximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前优先按运行时与 JS 共用的十进制精确整倍数判定处理常见十进制步进,并在必要时退回浮点容差兜底
|
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前优先按运行时与 JS 共用的十进制精确整倍数判定处理常见十进制步进,并在必要时退回浮点容差兜底
|
||||||
|
|||||||
@ -373,25 +373,6 @@ function parseBatchArrayValue(value) {
|
|||||||
.filter((item) => item.length > 0);
|
.filter((item) => item.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a schema enum array into string values that can be shown in UI
|
|
||||||
* hints and compared against parsed YAML scalar content.
|
|
||||||
*
|
|
||||||
* @param {unknown} value Raw schema enum value.
|
|
||||||
* @returns {string[] | undefined} Normalized enum values.
|
|
||||||
*/
|
|
||||||
function normalizeSchemaEnumValues(value) {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = value
|
|
||||||
.filter((item) => ["string", "number", "boolean"].includes(typeof item))
|
|
||||||
.map((item) => String(item));
|
|
||||||
|
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize one finite schema number for tooling metadata and comparisons.
|
* Normalize one finite schema number for tooling metadata and comparisons.
|
||||||
*
|
*
|
||||||
@ -598,6 +579,47 @@ function applyConstMetadata(schemaNode, rawConst, displayPath) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach parsed enum metadata to one schema node.
|
||||||
|
*
|
||||||
|
* @param {SchemaNode} schemaNode Parsed schema node.
|
||||||
|
* @param {unknown} rawEnum Raw schema enum value.
|
||||||
|
* @param {string} displayPath Logical property path.
|
||||||
|
* @returns {SchemaNode} Schema node with optional enum metadata.
|
||||||
|
*/
|
||||||
|
function applyEnumMetadata(schemaNode, rawEnum, displayPath) {
|
||||||
|
if (!Array.isArray(rawEnum)) {
|
||||||
|
return schemaNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enumComparableValues = [];
|
||||||
|
const enumDisplayValues = [];
|
||||||
|
const enumValues = [];
|
||||||
|
|
||||||
|
for (const item of rawEnum) {
|
||||||
|
enumComparableValues.push(buildSchemaConstComparableValue(schemaNode, item, displayPath));
|
||||||
|
|
||||||
|
const displayValue = formatSchemaConstDisplayValue(item);
|
||||||
|
if (displayValue !== undefined) {
|
||||||
|
enumDisplayValues.push(displayValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaNode.type !== "object" && schemaNode.type !== "array") {
|
||||||
|
const editableValue = formatSchemaConstEditableValue(schemaNode, item);
|
||||||
|
if (editableValue !== undefined) {
|
||||||
|
enumValues.push(editableValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...schemaNode,
|
||||||
|
enumValues: enumValues.length > 0 ? enumValues : undefined,
|
||||||
|
enumDisplayValues: enumDisplayValues.length > 0 ? enumDisplayValues : undefined,
|
||||||
|
enumComparableValues: enumComparableValues.length > 0 ? enumComparableValues : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test one scalar value against one compiled schema pattern.
|
* Test one scalar value against one compiled schema pattern.
|
||||||
*
|
*
|
||||||
@ -1107,7 +1129,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
properties[key] = parseSchemaNode(propertyNode, joinPropertyPath(displayPath, key));
|
properties[key] = parseSchemaNode(propertyNode, joinPropertyPath(displayPath, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyConstMetadata({
|
return applyEnumMetadata(applyConstMetadata({
|
||||||
type: "object",
|
type: "object",
|
||||||
displayPath,
|
displayPath,
|
||||||
required,
|
required,
|
||||||
@ -1118,7 +1140,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
defaultValue: metadata.defaultValue,
|
defaultValue: metadata.defaultValue,
|
||||||
not: negatedSchemaNode
|
not: negatedSchemaNode
|
||||||
}, value.const, displayPath);
|
}, value.const, displayPath), value.enum, displayPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "array") {
|
if (type === "array") {
|
||||||
@ -1144,7 +1166,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
throw new Error(`Schema property '${displayPath}' declares 'minContains' greater than 'maxContains'.`);
|
throw new Error(`Schema property '${displayPath}' declares 'minContains' greater than 'maxContains'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyConstMetadata({
|
return applyEnumMetadata(applyConstMetadata({
|
||||||
type: "array",
|
type: "array",
|
||||||
displayPath,
|
displayPath,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
@ -1163,10 +1185,10 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
contains: containsNode,
|
contains: containsNode,
|
||||||
items: itemNode,
|
items: itemNode,
|
||||||
not: negatedSchemaNode
|
not: negatedSchemaNode
|
||||||
}, value.const, displayPath);
|
}, value.const, displayPath), value.enum, displayPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyConstMetadata({
|
return applyEnumMetadata(applyConstMetadata({
|
||||||
type,
|
type,
|
||||||
displayPath,
|
displayPath,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
@ -1202,10 +1224,9 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
format: type === "string"
|
format: type === "string"
|
||||||
? metadata.format
|
? metadata.format
|
||||||
: undefined,
|
: undefined,
|
||||||
enumValues: normalizeSchemaEnumValues(value.enum),
|
|
||||||
refTable: metadata.refTable,
|
refTable: metadata.refTable,
|
||||||
not: negatedSchemaNode
|
not: negatedSchemaNode
|
||||||
}, value.const, displayPath);
|
}, value.const, displayPath), value.enum, displayPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1353,6 +1374,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateEnumComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
validateConstComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
validateConstComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
|
|
||||||
@ -1382,17 +1404,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(schemaNode.enumValues) &&
|
validateEnumComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
schemaNode.enumValues.length > 0 &&
|
|
||||||
!schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) {
|
|
||||||
diagnostics.push({
|
|
||||||
severity: "error",
|
|
||||||
message: localizeValidationMessage(ValidationMessageKeys.enumMismatch, localizer, {
|
|
||||||
displayPath,
|
|
||||||
values: schemaNode.enumValues.join(", ")
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const scalarValue = unquoteScalar(yamlNode.value);
|
const scalarValue = unquoteScalar(yamlNode.value);
|
||||||
const supportsNumericConstraints = schemaNode.type === "integer" || schemaNode.type === "number";
|
const supportsNumericConstraints = schemaNode.type === "integer" || schemaNode.type === "number";
|
||||||
@ -1586,6 +1598,7 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateEnumComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
validateConstComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
validateConstComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
validateNotSchemaMatch(schemaNode, yamlNode, displayPath, diagnostics, localizer);
|
||||||
}
|
}
|
||||||
@ -1660,6 +1673,12 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode, allowUnknownObjectPrope
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(schemaNode.enumComparableValues) &&
|
||||||
|
schemaNode.enumComparableValues.length > 0 &&
|
||||||
|
!schemaNode.enumComparableValues.includes(buildComparableNodeValue(schemaNode, yamlNode))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof schemaNode.constComparableValue === "string" &&
|
if (typeof schemaNode.constComparableValue === "string" &&
|
||||||
buildComparableNodeValue(schemaNode, yamlNode) !== schemaNode.constComparableValue) {
|
buildComparableNodeValue(schemaNode, yamlNode) !== schemaNode.constComparableValue) {
|
||||||
return false;
|
return false;
|
||||||
@ -1722,6 +1741,12 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode, allowUnknownObjectPrope
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(schemaNode.enumComparableValues) &&
|
||||||
|
schemaNode.enumComparableValues.length > 0 &&
|
||||||
|
!schemaNode.enumComparableValues.includes(buildComparableNodeValue(schemaNode, yamlNode))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof schemaNode.constComparableValue === "string" &&
|
if (typeof schemaNode.constComparableValue === "string" &&
|
||||||
buildComparableNodeValue(schemaNode, yamlNode) !== schemaNode.constComparableValue) {
|
buildComparableNodeValue(schemaNode, yamlNode) !== schemaNode.constComparableValue) {
|
||||||
return false;
|
return false;
|
||||||
@ -1738,9 +1763,9 @@ function matchesSchemaNodeInternal(schemaNode, yamlNode, allowUnknownObjectPrope
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(schemaNode.enumValues) &&
|
if (Array.isArray(schemaNode.enumComparableValues) &&
|
||||||
schemaNode.enumValues.length > 0 &&
|
schemaNode.enumComparableValues.length > 0 &&
|
||||||
!schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) {
|
!schemaNode.enumComparableValues.includes(buildComparableNodeValue(schemaNode, yamlNode))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1886,6 +1911,39 @@ function isStructurallyCompatibleWithSchemaNode(schemaNode, yamlNode) {
|
|||||||
isScalarCompatible(schemaNode.type, yamlNode.value);
|
isScalarCompatible(schemaNode.type, yamlNode.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate one parsed YAML node against one normalized enum comparable set.
|
||||||
|
*
|
||||||
|
* @param {SchemaNode} schemaNode Schema node.
|
||||||
|
* @param {YamlNode} yamlNode YAML node.
|
||||||
|
* @param {string} displayPath Current logical path.
|
||||||
|
* @param {Array<{severity: "error" | "warning", message: string}>} diagnostics Diagnostic sink.
|
||||||
|
* @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer.
|
||||||
|
*/
|
||||||
|
function validateEnumComparableValue(schemaNode, yamlNode, displayPath, diagnostics, localizer) {
|
||||||
|
if (!Array.isArray(schemaNode.enumComparableValues) || schemaNode.enumComparableValues.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparableValue = buildComparableNodeValue(schemaNode, yamlNode);
|
||||||
|
if (schemaNode.enumComparableValues.includes(comparableValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayValues = Array.isArray(schemaNode.enumDisplayValues) && schemaNode.enumDisplayValues.length > 0
|
||||||
|
? schemaNode.enumDisplayValues
|
||||||
|
: Array.isArray(schemaNode.enumValues)
|
||||||
|
? schemaNode.enumValues
|
||||||
|
: [];
|
||||||
|
diagnostics.push({
|
||||||
|
severity: "error",
|
||||||
|
message: localizeValidationMessage(ValidationMessageKeys.enumMismatch, localizer, {
|
||||||
|
displayPath,
|
||||||
|
values: displayValues.join(", ")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate one parsed YAML node against one normalized const comparable 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
|
* The helper reuses the same comparable-key logic as uniqueItems so array order
|
||||||
@ -2804,6 +2862,8 @@ module.exports = {
|
|||||||
* title?: string,
|
* title?: string,
|
||||||
* description?: string,
|
* description?: string,
|
||||||
* defaultValue?: string,
|
* defaultValue?: string,
|
||||||
|
* enumDisplayValues?: string[],
|
||||||
|
* enumComparableValues?: string[],
|
||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
* constDisplayValue?: string,
|
* constDisplayValue?: string,
|
||||||
* constComparableValue?: string,
|
* constComparableValue?: string,
|
||||||
@ -2814,6 +2874,8 @@ module.exports = {
|
|||||||
* title?: string,
|
* title?: string,
|
||||||
* description?: string,
|
* description?: string,
|
||||||
* defaultValue?: string,
|
* defaultValue?: string,
|
||||||
|
* enumDisplayValues?: string[],
|
||||||
|
* enumComparableValues?: string[],
|
||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
* constDisplayValue?: string,
|
* constDisplayValue?: string,
|
||||||
* constComparableValue?: string,
|
* constComparableValue?: string,
|
||||||
@ -2835,6 +2897,8 @@ module.exports = {
|
|||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
* constDisplayValue?: string,
|
* constDisplayValue?: string,
|
||||||
* constComparableValue?: string,
|
* constComparableValue?: string,
|
||||||
|
* enumDisplayValues?: string[],
|
||||||
|
* enumComparableValues?: string[],
|
||||||
* minimum?: number,
|
* minimum?: number,
|
||||||
* exclusiveMinimum?: number,
|
* exclusiveMinimum?: number,
|
||||||
* maximum?: number,
|
* maximum?: number,
|
||||||
|
|||||||
101
tools/gframework-config-tool/test/configValidation.enum.test.js
Normal file
101
tools/gframework-config-tool/test/configValidation.enum.test.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const {
|
||||||
|
parseSchemaContent,
|
||||||
|
parseTopLevelYaml,
|
||||||
|
validateParsedConfig
|
||||||
|
} = require("../src/configValidation");
|
||||||
|
|
||||||
|
test("parseSchemaContent should capture object and array enum comparable metadata", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"itemId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"enum": [
|
||||||
|
{ "gold": 10, "itemId": "potion" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"enum": [
|
||||||
|
["fire", "ice"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.deepEqual(schema.properties.reward.enumDisplayValues, ["{\"gold\":10,\"itemId\":\"potion\"}"]);
|
||||||
|
assert.match(schema.properties.reward.enumComparableValues[0], /^4:gold=/u);
|
||||||
|
assert.deepEqual(schema.properties.dropItemIds.enumDisplayValues, ["[\"fire\",\"ice\"]"]);
|
||||||
|
assert.equal(schema.properties.dropItemIds.enumComparableValues[0], "[13:string:4:fire,12:string:3:ice]");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should reject object values not declared in object enum", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["reward"],
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["gold", "itemId"],
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"itemId": { "type": "string" }
|
||||||
|
},
|
||||||
|
"enum": [
|
||||||
|
{ "gold": 10, "itemId": "potion" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
itemId: elixir
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 1);
|
||||||
|
assert.match(diagnostics[0].message, /reward/u);
|
||||||
|
assert.match(diagnostics[0].message, /"itemId":"potion"/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should treat array enum candidates as order-sensitive", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["dropItemIds"],
|
||||||
|
"properties": {
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"enum": [
|
||||||
|
["fire", "ice"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
dropItemIds:
|
||||||
|
- ice
|
||||||
|
- fire
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 1);
|
||||||
|
assert.match(diagnostics[0].message, /dropItemIds/u);
|
||||||
|
assert.match(diagnostics[0].message, /\["fire","ice"\]/u);
|
||||||
|
});
|
||||||
@ -323,7 +323,7 @@ reward:
|
|||||||
const diagnostics = validateParsedConfig(schema, yaml);
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
assert.equal(diagnostics.length, 1);
|
assert.equal(diagnostics.length, 1);
|
||||||
assert.match(diagnostics[0].message, /coin, gem/u);
|
assert.match(diagnostics[0].message, /"coin", "gem"/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should report scalar const mismatches", () => {
|
test("validateParsedConfig should report scalar const mismatches", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user