mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
feat(extension): 添加GFramework配置工具扩展功能
- 实现配置文件浏览器树视图,支持工作区配置目录导航 - 集成轻量级验证系统,支持YAML配置文件语法检查 - 添加模式感知表单预览功能,支持结构化配置编辑 - 实现批量编辑功能,支持跨多个配置文件统一修改字段值 - 集成国际化支持,提供中英文本地化界面 - 添加实时配置文件保存验证,在文件保存时自动校验 - 实现引用导航功能,支持跳转到关联配置表和文件 - 添加工作区变更响应,支持动态刷新配置树视图
This commit is contained in:
parent
4ff5189da4
commit
039ef9817a
@ -1211,6 +1211,170 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证匹配数量刚好等于 <c>minContains</c> / <c>maxContains</c> 时会被视为合法边界。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task LoadAsync_Should_Accept_Array_When_Contains_Match_Count_Equals_Min_And_Max_Bounds()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
dropRates:
|
||||||
|
- 5
|
||||||
|
- 7
|
||||||
|
- 5
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "dropRates"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"dropRates": {
|
||||||
|
"type": "array",
|
||||||
|
"minContains": 2,
|
||||||
|
"maxContains": 2,
|
||||||
|
"contains": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 5
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var table = registry.GetTable<int, MonsterConfigIntegerArrayStub>("monster");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(table.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(table.Get(1).DropRates, Is.EqualTo(new[] { 5, 7, 5 }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证数组字段将 <c>contains</c> 声明为非对象 schema 时,会在 schema 解析阶段被拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Contains_Is_Not_Object_Schema()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
dropRates:
|
||||||
|
- 5
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "dropRates"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"dropRates": {
|
||||||
|
"type": "array",
|
||||||
|
"contains": 5,
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
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!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("'contains' as an object-valued schema"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证数组字段将 <c>contains</c> 声明为嵌套数组 schema 时,会在 schema 解析阶段被拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Contains_Uses_Nested_Array_Schema()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
dropRates:
|
||||||
|
- 5
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "dropRates"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"dropRates": {
|
||||||
|
"type": "array",
|
||||||
|
"contains": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigIntegerArrayStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
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!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("unsupported nested array 'contains' schemas"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证数组在未声明 <c>contains</c> 时不能单独使用 <c>minContains</c>。
|
/// 验证数组在未声明 <c>contains</c> 时不能单独使用 <c>minContains</c>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -2345,6 +2509,190 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证仅声明在 <c>contains</c> 子 schema 里的跨表引用也会参与整批加载校验。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Contains_Matched_Reference_Target_Is_Missing()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"item/potion.yaml",
|
||||||
|
"""
|
||||||
|
id: potion
|
||||||
|
name: Potion
|
||||||
|
""");
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
dropItemIds:
|
||||||
|
- potion
|
||||||
|
- missing_item
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/item.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "dropItemIds"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"minContains": 1,
|
||||||
|
"contains": {
|
||||||
|
"type": "string",
|
||||||
|
"x-gframework-ref-table": "item"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
|
||||||
|
static config => config.Id)
|
||||||
|
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
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!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ReferencedKeyNotFound));
|
||||||
|
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(exception.Diagnostic.ReferencedTableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropItemIds[1]"));
|
||||||
|
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("missing_item"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证依赖关系仅来自 <c>contains</c> 子 schema 时,热重载仍会追踪该依赖并在目标表破坏引用后回滚。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task EnableHotReload_Should_Keep_Previous_State_When_Contains_Reference_Dependency_Breaks()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"item/potion.yaml",
|
||||||
|
"""
|
||||||
|
id: potion
|
||||||
|
name: Potion
|
||||||
|
""");
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
dropItemIds:
|
||||||
|
- potion
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/item.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "dropItemIds"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"dropItemIds": {
|
||||||
|
"type": "array",
|
||||||
|
"minContains": 1,
|
||||||
|
"contains": {
|
||||||
|
"type": "string",
|
||||||
|
"x-gframework-ref-table": "item"
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
|
||||||
|
static config => config.Id)
|
||||||
|
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var reloadFailureTaskSource =
|
||||||
|
new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions
|
||||||
|
.RunContinuationsAsynchronously);
|
||||||
|
var hotReload = loader.EnableHotReload(
|
||||||
|
registry,
|
||||||
|
onTableReloadFailed: (tableName, exception) =>
|
||||||
|
reloadFailureTaskSource.TrySetResult((tableName, exception)),
|
||||||
|
debounceDelay: TimeSpan.FromMilliseconds(150));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"item/potion.yaml",
|
||||||
|
"""
|
||||||
|
id: elixir
|
||||||
|
name: Elixir
|
||||||
|
""");
|
||||||
|
|
||||||
|
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
||||||
|
var diagnosticException = failure.Exception as ConfigLoadException;
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(failure.TableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(diagnosticException, Is.Not.Null);
|
||||||
|
Assert.That(diagnosticException!.Diagnostic.FailureKind,
|
||||||
|
Is.EqualTo(ConfigLoadFailureKind.ReferencedKeyNotFound));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.ReferencedTableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.DisplayPath, Is.EqualTo("dropItemIds[0]"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.RawValue, Is.EqualTo("potion"));
|
||||||
|
Assert.That(registry.GetTable<string, ItemConfigStub>("item").ContainsKey("potion"), Is.True);
|
||||||
|
Assert.That(registry.GetTable<int, MonsterDropArrayConfigStub>("monster").Get(1).DropItemIds,
|
||||||
|
Is.EqualTo(new[] { "potion" }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
hotReload.UnRegister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证启用热重载后,配置文件内容变更会刷新已注册配置表。
|
/// 验证启用热重载后,配置文件内容变更会刷新已注册配置表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -2779,7 +3127,7 @@ public class YamlConfigLoaderTests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置掉落率列表。
|
/// 获取或设置掉落率列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<int> DropRates { get; set; } = Array.Empty<int>();
|
public List<int> DropRates { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -687,7 +687,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
|
||||||
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2153,12 +2153,14 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="displayPath">字段路径。</param>
|
/// <param name="displayPath">字段路径。</param>
|
||||||
/// <param name="sequenceNode">实际数组节点。</param>
|
/// <param name="sequenceNode">实际数组节点。</param>
|
||||||
/// <param name="schemaNode">数组 schema 节点。</param>
|
/// <param name="schemaNode">数组 schema 节点。</param>
|
||||||
|
/// <param name="references">匹配成功的 <c>contains</c> 子树所声明的跨表引用收集器。</param>
|
||||||
private static void ValidateArrayContainsConstraints(
|
private static void ValidateArrayContainsConstraints(
|
||||||
string tableName,
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlSequenceNode sequenceNode,
|
YamlSequenceNode sequenceNode,
|
||||||
YamlConfigSchemaNode schemaNode)
|
YamlConfigSchemaNode schemaNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references)
|
||||||
{
|
{
|
||||||
var containsConstraints = schemaNode.ArrayConstraints?.ContainsConstraints;
|
var containsConstraints = schemaNode.ArrayConstraints?.ContainsConstraints;
|
||||||
if (containsConstraints is null)
|
if (containsConstraints is null)
|
||||||
@ -2171,7 +2173,8 @@ internal static class YamlConfigSchemaValidator
|
|||||||
yamlPath,
|
yamlPath,
|
||||||
displayPath,
|
displayPath,
|
||||||
sequenceNode,
|
sequenceNode,
|
||||||
containsConstraints.ContainsNode);
|
containsConstraints.ContainsNode,
|
||||||
|
references);
|
||||||
var rawValue = matchingCount.ToString(CultureInfo.InvariantCulture);
|
var rawValue = matchingCount.ToString(CultureInfo.InvariantCulture);
|
||||||
var requiredMinContains = containsConstraints.MinContains ?? 1;
|
var requiredMinContains = containsConstraints.MinContains ?? 1;
|
||||||
if (matchingCount < requiredMinContains)
|
if (matchingCount < requiredMinContains)
|
||||||
@ -2211,13 +2214,15 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="displayPath">数组字段路径。</param>
|
/// <param name="displayPath">数组字段路径。</param>
|
||||||
/// <param name="sequenceNode">实际数组节点。</param>
|
/// <param name="sequenceNode">实际数组节点。</param>
|
||||||
/// <param name="containsNode">contains 子 schema。</param>
|
/// <param name="containsNode">contains 子 schema。</param>
|
||||||
|
/// <param name="references">匹配成功元素的可选跨表引用收集器。</param>
|
||||||
/// <returns>匹配 <c>contains</c> 子 schema 的元素数量。</returns>
|
/// <returns>匹配 <c>contains</c> 子 schema 的元素数量。</returns>
|
||||||
private static int CountMatchingContainsItems(
|
private static int CountMatchingContainsItems(
|
||||||
string tableName,
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlSequenceNode sequenceNode,
|
YamlSequenceNode sequenceNode,
|
||||||
YamlConfigSchemaNode containsNode)
|
YamlConfigSchemaNode containsNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references)
|
||||||
{
|
{
|
||||||
var matchingCount = 0;
|
var matchingCount = 0;
|
||||||
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
||||||
@ -2227,7 +2232,8 @@ internal static class YamlConfigSchemaValidator
|
|||||||
yamlPath,
|
yamlPath,
|
||||||
$"{displayPath}[{itemIndex}]",
|
$"{displayPath}[{itemIndex}]",
|
||||||
sequenceNode.Children[itemIndex],
|
sequenceNode.Children[itemIndex],
|
||||||
containsNode))
|
containsNode,
|
||||||
|
references))
|
||||||
{
|
{
|
||||||
matchingCount++;
|
matchingCount++;
|
||||||
}
|
}
|
||||||
@ -2245,17 +2251,33 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="displayPath">当前数组元素路径。</param>
|
/// <param name="displayPath">当前数组元素路径。</param>
|
||||||
/// <param name="itemNode">实际 YAML 元素。</param>
|
/// <param name="itemNode">实际 YAML 元素。</param>
|
||||||
/// <param name="containsNode">contains 子 schema。</param>
|
/// <param name="containsNode">contains 子 schema。</param>
|
||||||
|
/// <param name="references">当前元素匹配成功后要写回的可选跨表引用收集器。</param>
|
||||||
/// <returns>当前元素是否匹配 contains 子 schema。</returns>
|
/// <returns>当前元素是否匹配 contains 子 schema。</returns>
|
||||||
private static bool IsArrayItemMatchingContains(
|
private static bool IsArrayItemMatchingContains(
|
||||||
string tableName,
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlNode itemNode,
|
YamlNode itemNode,
|
||||||
YamlConfigSchemaNode containsNode)
|
YamlConfigSchemaNode containsNode,
|
||||||
|
ICollection<YamlConfigReferenceUsage>? references)
|
||||||
{
|
{
|
||||||
|
// contains 的“试匹配”不能把失败元素的引用泄漏给外层,但匹配成功的元素仍需要参与
|
||||||
|
// 跨表引用收集,否则仅声明在 contains 子 schema 里的 ref-table 会被运行时遗漏。
|
||||||
|
List<YamlConfigReferenceUsage>? matchedReferences = references is null ? null : new();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ValidateNode(tableName, yamlPath, displayPath, itemNode, containsNode, references: null);
|
ValidateNode(tableName, yamlPath, displayPath, itemNode, containsNode, matchedReferences);
|
||||||
|
|
||||||
|
if (references is not null &&
|
||||||
|
matchedReferences is not null)
|
||||||
|
{
|
||||||
|
foreach (var referenceUsage in matchedReferences)
|
||||||
|
{
|
||||||
|
references.Add(referenceUsage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind != ConfigLoadFailureKind.UnexpectedFailure)
|
catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind != ConfigLoadFailureKind.UnexpectedFailure)
|
||||||
@ -2627,6 +2649,12 @@ internal static class YamlConfigSchemaValidator
|
|||||||
{
|
{
|
||||||
CollectReferencedTableNames(node.ItemNode, referencedTableNames);
|
CollectReferencedTableNames(node.ItemNode, referencedTableNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containsNode = node.ArrayConstraints?.ContainsConstraints?.ContainsNode;
|
||||||
|
if (containsNode is not null)
|
||||||
|
{
|
||||||
|
CollectReferencedTableNames(containsNode, referencedTableNames);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
41
tools/gframework-config-tool/src/containsSummary.js
Normal file
41
tools/gframework-config-tool/src/containsSummary.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Build a compact contains-schema summary for array field hints.
|
||||||
|
* The summary reuses existing localized hint strings so Chinese UI surfaces
|
||||||
|
* do not fall back to mixed English tokens such as const/enum/pattern/ref.
|
||||||
|
*
|
||||||
|
* @param {{type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}} containsSchema Parsed contains schema metadata.
|
||||||
|
* @param {{t: (key: string, params?: Record<string, string | number>) => string}} localizer Runtime localizer.
|
||||||
|
* @returns {string} Human-facing summary.
|
||||||
|
*/
|
||||||
|
function describeContainsSchema(containsSchema, localizer) {
|
||||||
|
const parts = [];
|
||||||
|
if (containsSchema.type) {
|
||||||
|
parts.push(containsSchema.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsSchema.constValue !== undefined) {
|
||||||
|
parts.push(localizer.t("webview.hint.const", {
|
||||||
|
value: containsSchema.constDisplayValue ?? containsSchema.constValue
|
||||||
|
}));
|
||||||
|
} else if (Array.isArray(containsSchema.enumValues) && containsSchema.enumValues.length > 0) {
|
||||||
|
parts.push(localizer.t("webview.hint.allowed", {
|
||||||
|
values: containsSchema.enumValues.join(", ")
|
||||||
|
}));
|
||||||
|
} else if (containsSchema.pattern) {
|
||||||
|
parts.push(localizer.t("webview.hint.pattern", {
|
||||||
|
value: containsSchema.pattern
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsSchema.refTable) {
|
||||||
|
parts.push(localizer.t("webview.hint.refTable", {
|
||||||
|
refTable: containsSchema.refTable
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(", ") || localizer.t("webview.objectArray.item");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
describeContainsSchema
|
||||||
|
};
|
||||||
@ -18,6 +18,7 @@ const {
|
|||||||
joinArrayTemplatePath,
|
joinArrayTemplatePath,
|
||||||
joinPropertyPath
|
joinPropertyPath
|
||||||
} = require("./configPath");
|
} = require("./configPath");
|
||||||
|
const {describeContainsSchema} = require("./containsSummary");
|
||||||
const {createLocalizer} = require("./localization");
|
const {createLocalizer} = require("./localization");
|
||||||
|
|
||||||
const localizer = createLocalizer(vscode.env.language);
|
const localizer = createLocalizer(vscode.env.language);
|
||||||
@ -1658,7 +1659,7 @@ function renderFieldHint(propertySchema, isArrayField, includeDescription = true
|
|||||||
|
|
||||||
if (isArrayField && propertySchema.contains) {
|
if (isArrayField && propertySchema.contains) {
|
||||||
hints.push(escapeHtml(localizer.t("webview.hint.contains", {
|
hints.push(escapeHtml(localizer.t("webview.hint.contains", {
|
||||||
summary: describeContainsSchema(propertySchema.contains)
|
summary: describeContainsSchema(propertySchema.contains, localizer)
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1723,35 +1724,6 @@ function renderFieldHint(propertySchema, isArrayField, includeDescription = true
|
|||||||
return `<span class="hint">${hints.join(" · ")}</span>`;
|
return `<span class="hint">${hints.join(" · ")}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a compact contains-schema summary for array field hints.
|
|
||||||
* The hint intentionally stays short so the form preview can expose the rule
|
|
||||||
* without inlining a second full schema tree beside the field controls.
|
|
||||||
*
|
|
||||||
* @param {{type?: string, enumValues?: string[], constValue?: string, constDisplayValue?: string, pattern?: string, refTable?: string}} containsSchema Parsed contains schema metadata.
|
|
||||||
* @returns {string} Human-facing summary.
|
|
||||||
*/
|
|
||||||
function describeContainsSchema(containsSchema) {
|
|
||||||
const parts = [];
|
|
||||||
if (containsSchema.type) {
|
|
||||||
parts.push(containsSchema.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containsSchema.constValue !== undefined) {
|
|
||||||
parts.push(`const = ${containsSchema.constDisplayValue ?? containsSchema.constValue}`);
|
|
||||||
} else if (Array.isArray(containsSchema.enumValues) && containsSchema.enumValues.length > 0) {
|
|
||||||
parts.push(`enum = ${containsSchema.enumValues.join(", ")}`);
|
|
||||||
} else if (containsSchema.pattern) {
|
|
||||||
parts.push(`pattern = ${containsSchema.pattern}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containsSchema.refTable) {
|
|
||||||
parts.push(`ref = ${containsSchema.refTable}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(", ") || "item";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt for one batch-edit field value.
|
* Prompt for one batch-edit field value.
|
||||||
*
|
*
|
||||||
|
|||||||
27
tools/gframework-config-tool/test/containsSummary.test.js
Normal file
27
tools/gframework-config-tool/test/containsSummary.test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const {describeContainsSchema} = require("../src/containsSummary");
|
||||||
|
const {createLocalizer} = require("../src/localization");
|
||||||
|
|
||||||
|
test("describeContainsSchema should reuse localized Chinese hint strings", () => {
|
||||||
|
const localizer = createLocalizer("zh-cn");
|
||||||
|
|
||||||
|
const summary = describeContainsSchema(
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
constValue: "\"potion\"",
|
||||||
|
constDisplayValue: "\"potion\"",
|
||||||
|
refTable: "item"
|
||||||
|
},
|
||||||
|
localizer);
|
||||||
|
|
||||||
|
assert.equal(summary, "string, 固定值:\"potion\", 引用表:item");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("describeContainsSchema should fall back to localized item label", () => {
|
||||||
|
const localizer = createLocalizer("en");
|
||||||
|
|
||||||
|
const summary = describeContainsSchema({}, localizer);
|
||||||
|
|
||||||
|
assert.equal(summary, "Item");
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user