mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(config): 添加配置系统集成测试和文档
- 添加 ArchitectureConfigIntegrationTests 验证架构初始化流程中配置加载 - 添加 GeneratedConfigConsumerIntegrationTests 测试消费者项目配置绑定功能 - 添加完整的游戏内容配置系统中文文档 - 添加生成配置目录和注册选项支持批量表注册与筛选 - 实现配置架构集成模板和热重载功能 - 添加跨表引用校验和运行时诊断功能 - 实现 VS Code 工具支持配置浏览和表单编辑 - 添加查询辅助方法支持按字段快速检索配置数据
This commit is contained in:
parent
e9e04d9792
commit
92eb365dc7
@ -42,6 +42,7 @@ public class ArchitectureConfigIntegrationTests
|
|||||||
Assert.That(architecture.Registry.TryGetMonsterTable(out var retrieved), Is.True);
|
Assert.That(architecture.Registry.TryGetMonsterTable(out var retrieved), Is.True);
|
||||||
Assert.That(retrieved, Is.Not.Null);
|
Assert.That(retrieved, Is.Not.Null);
|
||||||
Assert.That(retrieved!.Get(1).Name, Is.EqualTo("Slime"));
|
Assert.That(retrieved!.Get(1).Name, Is.EqualTo("Slime"));
|
||||||
|
Assert.That(architecture.Registry.TryGetItemTable(out _), Is.False);
|
||||||
Assert.That(architecture.Context.GetUtility<ConfigRegistry>(), Is.SameAs(architecture.Registry));
|
Assert.That(architecture.Context.GetUtility<ConfigRegistry>(), Is.SameAs(architecture.Registry));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -147,7 +148,11 @@ public class ArchitectureConfigIntegrationTests
|
|||||||
RegisterUtility(Registry);
|
RegisterUtility(Registry);
|
||||||
|
|
||||||
var loader = new YamlConfigLoader(_configRoot)
|
var loader = new YamlConfigLoader(_configRoot)
|
||||||
.RegisterAllGeneratedConfigTables();
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
|
||||||
|
});
|
||||||
loader.LoadAsync(Registry).GetAwaiter().GetResult();
|
loader.LoadAsync(Registry).GetAwaiter().GetResult();
|
||||||
MonsterTable = Registry.GetMonsterTable();
|
MonsterTable = Registry.GetMonsterTable();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,152 @@ public class GeneratedConfigConsumerIntegrationTests
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
|
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
|
||||||
|
{
|
||||||
|
CreateMonsterFiles();
|
||||||
|
CreateItemFiles();
|
||||||
|
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterAllGeneratedConfigTables();
|
||||||
|
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
var monsterTable = registry.GetMonsterTable();
|
||||||
|
var dungeonMonsters = monsterTable.FindByFaction("dungeon");
|
||||||
|
var itemTable = registry.GetItemTable();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(
|
||||||
|
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
|
||||||
|
Is.SupersetOf(new[] { "item", "monster" }));
|
||||||
|
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemCatalogEntry), Is.True);
|
||||||
|
Assert.That(itemCatalogEntry.ConfigDomain, Is.EqualTo("item"));
|
||||||
|
Assert.That(itemCatalogEntry.ConfigRelativePath, Is.EqualTo("item"));
|
||||||
|
Assert.That(itemCatalogEntry.SchemaRelativePath, Is.EqualTo("schemas/item.schema.json"));
|
||||||
|
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
|
||||||
|
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
|
||||||
|
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
|
||||||
|
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
|
||||||
|
Assert.That(ItemConfigBindings.ConfigDomain, Is.EqualTo("item"));
|
||||||
|
Assert.That(ItemConfigBindings.Metadata.TableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
|
||||||
|
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
|
||||||
|
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
|
||||||
|
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
|
||||||
|
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
|
||||||
|
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
|
||||||
|
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
|
||||||
|
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
|
||||||
|
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
|
||||||
|
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
|
||||||
|
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
|
||||||
|
Assert.That(monsterTable.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
|
||||||
|
Assert.That(monsterTable.Get(2).Hp, Is.EqualTo(30));
|
||||||
|
Assert.That(monsterTable.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
|
||||||
|
Assert.That(dungeonMonsters.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
|
||||||
|
Assert.That(monsterTable.TryFindFirstByName("Goblin", out var goblin), Is.True);
|
||||||
|
Assert.That(goblin, Is.Not.Null);
|
||||||
|
Assert.That(goblin!.Id, Is.EqualTo(2));
|
||||||
|
Assert.That(monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
|
||||||
|
Assert.That(firstDungeonMonster, Is.Not.Null);
|
||||||
|
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
|
||||||
|
Assert.That(monsterTable.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
|
||||||
|
Assert.That(missingMonster, Is.Null);
|
||||||
|
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
|
||||||
|
Assert.That(generatedTable, Is.Not.Null);
|
||||||
|
Assert.That(generatedTable!.All().Select(static config => config.Name),
|
||||||
|
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
|
||||||
|
Assert.That(itemTable.Count, Is.EqualTo(2));
|
||||||
|
Assert.That(itemTable.Get("potion").Name, Is.EqualTo("Potion"));
|
||||||
|
Assert.That(itemTable.FindByCategory("consumable").Select(static config => config.Id),
|
||||||
|
Is.EquivalentTo(new[] { "potion", "ether" }));
|
||||||
|
Assert.That(registry.TryGetItemTable(out var generatedItemTable), Is.True);
|
||||||
|
Assert.That(generatedItemTable, Is.Not.Null);
|
||||||
|
Assert.That(generatedItemTable!.Get("ether").Name, Is.EqualTo("Ether"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task RegisterAllGeneratedConfigTables_Should_Support_Filtering_By_Domain_Table_Name_And_Predicate()
|
||||||
|
{
|
||||||
|
CreateMonsterFiles();
|
||||||
|
CreateItemFiles();
|
||||||
|
|
||||||
|
var domainRegistry = new ConfigRegistry();
|
||||||
|
var domainLoader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
|
||||||
|
});
|
||||||
|
await domainLoader.LoadAsync(domainRegistry);
|
||||||
|
|
||||||
|
var tableNameRegistry = new ConfigRegistry();
|
||||||
|
var tableNameLoader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
IncludedTableNames = new[] { ItemConfigBindings.TableName }
|
||||||
|
});
|
||||||
|
await tableNameLoader.LoadAsync(tableNameRegistry);
|
||||||
|
|
||||||
|
var monsterDomain = MonsterConfigBindings.ConfigDomain;
|
||||||
|
var predicateRegistry = new ConfigRegistry();
|
||||||
|
var predicateLoader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
TableFilter = metadata =>
|
||||||
|
string.Equals(metadata.ConfigDomain, monsterDomain, StringComparison.Ordinal)
|
||||||
|
});
|
||||||
|
await predicateLoader.LoadAsync(predicateRegistry);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(domainRegistry.TryGetMonsterTable(out var domainMonsterTable), Is.True);
|
||||||
|
Assert.That(domainMonsterTable, Is.Not.Null);
|
||||||
|
Assert.That(domainRegistry.TryGetItemTable(out _), Is.False);
|
||||||
|
|
||||||
|
Assert.That(tableNameRegistry.TryGetMonsterTable(out _), Is.False);
|
||||||
|
Assert.That(tableNameRegistry.TryGetItemTable(out var tableNameItemTable), Is.True);
|
||||||
|
Assert.That(tableNameItemTable, Is.Not.Null);
|
||||||
|
Assert.That(tableNameItemTable!.Get("potion").Name, Is.EqualTo("Potion"));
|
||||||
|
|
||||||
|
Assert.That(predicateRegistry.TryGetMonsterTable(out var predicateMonsterTable), Is.True);
|
||||||
|
Assert.That(predicateMonsterTable, Is.Not.Null);
|
||||||
|
Assert.That(predicateRegistry.TryGetItemTable(out _), Is.False);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在临时消费者根目录中创建测试文件。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="relativePath">相对根目录的文件路径。</param>
|
||||||
|
/// <param name="content">要写入的文件内容。</param>
|
||||||
|
private void CreateFile(
|
||||||
|
string relativePath,
|
||||||
|
string content)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
var directoryPath = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(directoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在临时消费者目录中创建 monster schema 与 YAML 测试数据。
|
||||||
|
/// </summary>
|
||||||
|
private void CreateMonsterFiles()
|
||||||
{
|
{
|
||||||
CreateFile(
|
CreateFile(
|
||||||
"schemas/monster.schema.json",
|
"schemas/monster.schema.json",
|
||||||
@ -88,73 +234,50 @@ public class GeneratedConfigConsumerIntegrationTests
|
|||||||
hp: 30
|
hp: 30
|
||||||
faction: dungeon
|
faction: dungeon
|
||||||
""");
|
""");
|
||||||
|
|
||||||
var registry = new ConfigRegistry();
|
|
||||||
var loader = new YamlConfigLoader(_rootPath)
|
|
||||||
.RegisterAllGeneratedConfigTables();
|
|
||||||
|
|
||||||
await loader.LoadAsync(registry);
|
|
||||||
|
|
||||||
var table = registry.GetMonsterTable();
|
|
||||||
var dungeonMonsters = table.FindByFaction("dungeon");
|
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
|
||||||
{
|
|
||||||
Assert.That(
|
|
||||||
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
|
|
||||||
Does.Contain("monster"));
|
|
||||||
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
|
|
||||||
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
|
|
||||||
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
|
|
||||||
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
|
|
||||||
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
|
|
||||||
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
|
|
||||||
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
|
|
||||||
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
|
|
||||||
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
|
|
||||||
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
|
|
||||||
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
|
|
||||||
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
|
|
||||||
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
|
|
||||||
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
|
|
||||||
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
|
|
||||||
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
|
|
||||||
Assert.That(table.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
|
|
||||||
Assert.That(table.Get(2).Hp, Is.EqualTo(30));
|
|
||||||
Assert.That(table.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
|
|
||||||
Assert.That(dungeonMonsters.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
|
|
||||||
Assert.That(table.TryFindFirstByName("Goblin", out var goblin), Is.True);
|
|
||||||
Assert.That(goblin, Is.Not.Null);
|
|
||||||
Assert.That(goblin!.Id, Is.EqualTo(2));
|
|
||||||
Assert.That(table.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
|
|
||||||
Assert.That(firstDungeonMonster, Is.Not.Null);
|
|
||||||
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
|
|
||||||
Assert.That(table.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
|
|
||||||
Assert.That(missingMonster, Is.Null);
|
|
||||||
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
|
|
||||||
Assert.That(generatedTable, Is.Not.Null);
|
|
||||||
Assert.That(generatedTable!.All().Select(static config => config.Name),
|
|
||||||
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在临时消费者根目录中创建测试文件。
|
/// 在临时消费者目录中创建 item schema 与 YAML 测试数据,用于验证多表聚合注册和筛选行为。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="relativePath">相对根目录的文件路径。</param>
|
private void CreateItemFiles()
|
||||||
/// <param name="content">要写入的文件内容。</param>
|
|
||||||
private void CreateFile(
|
|
||||||
string relativePath,
|
|
||||||
string content)
|
|
||||||
{
|
{
|
||||||
var path = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
CreateFile(
|
||||||
var directoryPath = Path.GetDirectoryName(path);
|
"schemas/item.schema.json",
|
||||||
if (!string.IsNullOrEmpty(directoryPath))
|
"""
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(directoryPath);
|
"title": "Item Config",
|
||||||
}
|
"description": "Defines one item entry for aggregate registration filtering integration tests.",
|
||||||
|
"type": "object",
|
||||||
File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
|
"required": ["id", "name", "category"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item identifier."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item display name."
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Used by integration tests to validate generated non-unique queries."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
CreateFile(
|
||||||
|
"item/potion.yaml",
|
||||||
|
"""
|
||||||
|
id: potion
|
||||||
|
name: Potion
|
||||||
|
category: consumable
|
||||||
|
""");
|
||||||
|
CreateFile(
|
||||||
|
"item/ether.yaml",
|
||||||
|
"""
|
||||||
|
id: ether
|
||||||
|
name: Ether
|
||||||
|
category: consumable
|
||||||
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
GFramework.Game.Tests/schemas/item.schema.json
Normal file
24
GFramework.Game.Tests/schemas/item.schema.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"title": "Item Config",
|
||||||
|
"description": "Defines one item entry for aggregate registration filtering integration tests.",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"category"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item identifier."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Item display name."
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Used by integration tests to validate generated non-unique queries."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -452,15 +452,22 @@ public class SchemaConfigGeneratorTests
|
|||||||
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
|
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
|
||||||
Assert.That(catalogSource, Does.Contain("public sealed class GeneratedConfigRegistrationOptions"));
|
Assert.That(catalogSource, Does.Contain("public sealed class GeneratedConfigRegistrationOptions"));
|
||||||
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigRegistrationExtensions"));
|
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigRegistrationExtensions"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }"));
|
||||||
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
|
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
|
||||||
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
|
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
|
||||||
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
|
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
|
||||||
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
|
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))"));
|
||||||
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);"));
|
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
|
||||||
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
|
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
|
||||||
Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
|
Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
|
||||||
Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
|
Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
|
||||||
Assert.That(catalogSource, Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
|
Assert.That(catalogSource, Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
|
||||||
|
Assert.That(catalogSource, Does.Contain("private static bool ShouldRegisterTable("));
|
||||||
|
Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList("));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,21 @@ public static class GeneratedConfigCatalog
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GeneratedConfigRegistrationOptions
|
public sealed class GeneratedConfigRegistrationOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional allow-list of generated config domains that aggregate registration should include. When null or empty, every generated domain remains eligible.
|
||||||
|
/// </summary>
|
||||||
|
public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional allow-list of runtime table names that aggregate registration should include. When null or empty, every generated table remains eligible.
|
||||||
|
/// </summary>
|
||||||
|
public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional predicate that can reject individual generated table metadata entries after allow-list filtering has passed.
|
||||||
|
/// </summary>
|
||||||
|
public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
|
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -141,7 +156,62 @@ public static class GeneratedConfigRegistrationExtensions
|
|||||||
|
|
||||||
options ??= new GeneratedConfigRegistrationOptions();
|
options ??= new GeneratedConfigRegistrationOptions();
|
||||||
|
|
||||||
loader.RegisterMonsterTable(options.MonsterComparer);
|
if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))
|
||||||
|
{
|
||||||
|
loader.RegisterMonsterTable(options.MonsterComparer);
|
||||||
|
}
|
||||||
|
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">Generated table metadata under consideration.</param>
|
||||||
|
/// <param name="options">Aggregate registration options supplied by the caller.</param>
|
||||||
|
/// <returns><see langword="true" /> when the generated table should be registered; otherwise <see langword="false" />.</returns>
|
||||||
|
private static bool ShouldRegisterTable(
|
||||||
|
GeneratedConfigCatalog.TableMetadata metadata,
|
||||||
|
GeneratedConfigRegistrationOptions options)
|
||||||
|
{
|
||||||
|
// Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.
|
||||||
|
if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.TableFilter?.Invoke(metadata) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allowedValues">Optional caller-supplied allow-list.</param>
|
||||||
|
/// <param name="candidate">Generated metadata value being evaluated.</param>
|
||||||
|
/// <returns><see langword="true" /> when the value should remain eligible for registration; otherwise <see langword="false" />.</returns>
|
||||||
|
private static bool MatchesOptionalAllowList(
|
||||||
|
global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,
|
||||||
|
string candidate)
|
||||||
|
{
|
||||||
|
if (allowedValues is null || allowedValues.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var allowedValue in allowedValues)
|
||||||
|
{
|
||||||
|
if (allowedValue is not null &&
|
||||||
|
string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1090,6 +1090,31 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
builder.AppendLine("/// </summary>");
|
builder.AppendLine("/// </summary>");
|
||||||
builder.AppendLine("public sealed class GeneratedConfigRegistrationOptions");
|
builder.AppendLine("public sealed class GeneratedConfigRegistrationOptions");
|
||||||
builder.AppendLine("{");
|
builder.AppendLine("{");
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Gets or sets the optional allow-list of generated config domains that aggregate registration should include. When null or empty, every generated domain remains eligible.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Gets or sets the optional allow-list of runtime table names that aggregate registration should include. When null or empty, every generated table remains eligible.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Gets or sets the optional predicate that can reject individual generated table metadata entries after allow-list filtering has passed.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }");
|
||||||
|
|
||||||
|
if (schemas.Count > 0)
|
||||||
|
{
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
for (var index = 0; index < schemas.Count; index++)
|
for (var index = 0; index < schemas.Count; index++)
|
||||||
{
|
{
|
||||||
@ -1158,13 +1183,77 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();");
|
builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();");
|
||||||
builder.AppendLine();
|
builder.AppendLine();
|
||||||
|
|
||||||
foreach (var schema in schemas)
|
for (var index = 0; index < schemas.Count; index++)
|
||||||
{
|
{
|
||||||
builder.AppendLine($" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
|
var schema = schemas[index];
|
||||||
|
builder.AppendLine(
|
||||||
|
$" if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], options))");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine($" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AppendLine(" return loader;");
|
builder.AppendLine(" return loader;");
|
||||||
builder.AppendLine(" }");
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"metadata\">Generated table metadata under consideration.</param>");
|
||||||
|
builder.AppendLine(" /// <param name=\"options\">Aggregate registration options supplied by the caller.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <returns><see langword=\"true\" /> when the generated table should be registered; otherwise <see langword=\"false\" />.</returns>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" private static bool ShouldRegisterTable(");
|
||||||
|
builder.AppendLine(" GeneratedConfigCatalog.TableMetadata metadata,");
|
||||||
|
builder.AppendLine(" GeneratedConfigRegistrationOptions options)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(
|
||||||
|
" // Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.");
|
||||||
|
builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return false;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return false;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" return options.TableFilter?.Invoke(metadata) ?? true;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"allowedValues\">Optional caller-supplied allow-list.</param>");
|
||||||
|
builder.AppendLine(" /// <param name=\"candidate\">Generated metadata value being evaluated.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <returns><see langword=\"true\" /> when the value should remain eligible for registration; otherwise <see langword=\"false\" />.</returns>");
|
||||||
|
builder.AppendLine(" private static bool MatchesOptionalAllowList(");
|
||||||
|
builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,");
|
||||||
|
builder.AppendLine(" string candidate)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" if (allowedValues is null || allowedValues.Count == 0)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return true;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" foreach (var allowedValue in allowedValues)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" if (allowedValue is not null &&");
|
||||||
|
builder.AppendLine(
|
||||||
|
" string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return true;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" return false;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
builder.AppendLine("}");
|
builder.AppendLine("}");
|
||||||
return builder.ToString().TrimEnd();
|
return builder.ToString().TrimEnd();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -465,10 +465,36 @@ var loader = new YamlConfigLoader("config-root")
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果项目已经生成了多张表,但当前场景只想注册其中一部分,也可以直接在聚合入口上加筛选,而不必退回手写逐表注册:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var loader = new YamlConfigLoader("config-root")
|
||||||
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你更习惯按表名白名单或自定义谓词裁剪启动集,也可以继续在同一个 options 对象里完成:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var loader = new YamlConfigLoader("config-root")
|
||||||
|
.RegisterAllGeneratedConfigTables(
|
||||||
|
new GeneratedConfigRegistrationOptions
|
||||||
|
{
|
||||||
|
IncludedTableNames = new[] { MonsterConfigBindings.TableName, ItemConfigBindings.TableName },
|
||||||
|
TableFilter = static metadata => metadata.SchemaRelativePath.EndsWith(".schema.json", StringComparison.Ordinal)
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
这里的规则是:
|
这里的规则是:
|
||||||
|
|
||||||
|
- `IncludedConfigDomains` 与 `IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制”
|
||||||
|
- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细的启动裁剪
|
||||||
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
|
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
|
||||||
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖
|
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖
|
||||||
|
- 当前 `ConfigDomain` 约定仍与生成表名保持一致,但建议优先引用 `*ConfigBindings.ConfigDomain`,为后续更细的分组策略保留稳定入口
|
||||||
- 如果项目希望继续完全手写某张表的注册流程,逐表 `Register*Table(...)` 入口仍然保留,作为兼容逃生通道
|
- 如果项目希望继续完全手写某张表的注册流程,逐表 `Register*Table(...)` 入口仍然保留,作为兼容逃生通道
|
||||||
|
|
||||||
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
|
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user