From faa01437992f733b30a47f05250e5db75ff282e8 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 17 Apr 2026 16:18:14 +0800
Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0JSON=20sch?=
=?UTF-8?q?ema=E9=85=8D=E7=BD=AE=E7=94=9F=E6=88=90=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了基于JSON schema自动生成配置类型和配置表包装的功能
- 支持嵌套对象、对象数组、标量数组的数据结构生成
- 添加了default/enum/const/ref-table元数据映射功能
- 实现了查找索引生成功能,支持唯一键快速检索
- 集成了诊断报告系统,提供详细的错误提示信息
- 生成配置类、表类和绑定类三种类型的源代码
- 支持日期、时间、邮箱等字符串格式验证功能
- 实现了依赖关系验证,确保schema间的引用正确性
---
.../Config/SchemaConfigGenerator.cs | 28 ++++-
.../Config/YamlConfigLoaderAllOfTests.cs | 80 ++++++++++++++
.../Config/YamlConfigSchemaValidator.cs | 26 ++++-
.../Config/SchemaConfigGeneratorTests.cs | 104 ++++++++++++++++++
4 files changed, 230 insertions(+), 8 deletions(-)
diff --git a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs
index 2f9f3fdd..54c5eca7 100644
--- a/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs
+++ b/GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs
@@ -1357,9 +1357,19 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
diagnostic = null;
var allOfEntryPath = BuildAllOfEntryPath(displayPath, allOfIndex);
- if (allOfSchema.TryGetProperty("properties", out var allOfPropertiesElement) &&
- allOfPropertiesElement.ValueKind == JsonValueKind.Object)
+ if (allOfSchema.TryGetProperty("properties", out var allOfPropertiesElement))
{
+ if (allOfPropertiesElement.ValueKind != JsonValueKind.Object)
+ {
+ diagnostic = Diagnostic.Create(
+ ConfigSchemaDiagnostics.InvalidAllOfMetadata,
+ CreateFileLocation(filePath),
+ Path.GetFileName(filePath),
+ allOfEntryPath,
+ $"Entry #{allOfIndex + 1} in 'allOf' must declare 'properties' as an object-valued map.");
+ return false;
+ }
+
foreach (var property in allOfPropertiesElement.EnumerateObject())
{
if (declaredProperties.Contains(property.Name))
@@ -1377,12 +1387,22 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
}
}
- if (!allOfSchema.TryGetProperty("required", out var requiredElement) ||
- requiredElement.ValueKind != JsonValueKind.Array)
+ if (!allOfSchema.TryGetProperty("required", out var requiredElement))
{
return true;
}
+ if (requiredElement.ValueKind != JsonValueKind.Array)
+ {
+ diagnostic = Diagnostic.Create(
+ ConfigSchemaDiagnostics.InvalidAllOfMetadata,
+ CreateFileLocation(filePath),
+ Path.GetFileName(filePath),
+ allOfEntryPath,
+ $"Entry #{allOfIndex + 1} in 'allOf' must declare 'required' as an array of parent property names.");
+ return false;
+ }
+
foreach (var requiredProperty in requiredElement.EnumerateArray())
{
if (requiredProperty.ValueKind != JsonValueKind.String)
diff --git a/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs b/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs
index ed2c5e79..2e4edde7 100644
--- a/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs
+++ b/GFramework.Game.Tests/Config/YamlConfigLoaderAllOfTests.cs
@@ -300,6 +300,86 @@ public sealed class YamlConfigLoaderAllOfTests
});
}
+ ///
+ /// 验证 allOf 条目的 properties 必须声明为对象映射。
+ ///
+ [Test]
+ public void LoadAsync_Should_Throw_When_AllOf_Entry_Properties_Is_Not_Object_Valued()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ BuildMonsterConfigYaml(
+ """
+ itemCount: 3
+ """));
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ BuildMonsterSchema(
+ DefaultRewardPropertiesJson,
+ """
+ [
+ {
+ "type": "object",
+ "properties": 1
+ }
+ ]
+ """));
+
+ var loader = CreateMonsterRewardLoader();
+ var registry = CreateRegistry();
+
+ var exception = Assert.ThrowsAsync(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("reward[allOf[0]]"));
+ Assert.That(exception.Message, Does.Contain("must declare 'properties' as an object-valued map"));
+ Assert.That(registry.Count, Is.EqualTo(0));
+ });
+ }
+
+ ///
+ /// 验证 allOf 条目的 required 必须声明为字段名数组。
+ ///
+ [Test]
+ public void LoadAsync_Should_Throw_When_AllOf_Entry_Required_Is_Not_An_Array()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ BuildMonsterConfigYaml(
+ """
+ itemCount: 3
+ """));
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ BuildMonsterSchema(
+ DefaultRewardPropertiesJson,
+ """
+ [
+ {
+ "type": "object",
+ "required": {}
+ }
+ ]
+ """));
+
+ var loader = CreateMonsterRewardLoader();
+ var registry = CreateRegistry();
+
+ var exception = Assert.ThrowsAsync(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("reward[allOf[0]]"));
+ Assert.That(exception.Message, Does.Contain("must declare 'required' as an array of property names"));
+ Assert.That(registry.Count, Is.EqualTo(0));
+ });
+ }
+
///
/// 验证 allOf 条目不能要求父对象未声明的字段。
///
diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
index 2f642d6d..08edbeb4 100644
--- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs
+++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
@@ -1975,9 +1975,18 @@ internal static class YamlConfigSchemaValidator
JsonElement allOfSchemaElement,
IReadOnlyDictionary properties)
{
- if (allOfSchemaElement.TryGetProperty("properties", out var allOfPropertiesElement) &&
- allOfPropertiesElement.ValueKind == JsonValueKind.Object)
+ if (allOfSchemaElement.TryGetProperty("properties", out var allOfPropertiesElement))
{
+ if (allOfPropertiesElement.ValueKind != JsonValueKind.Object)
+ {
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.SchemaUnsupported,
+ tableName,
+ $"Entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.",
+ schemaPath: schemaPath,
+ displayPath: GetDiagnosticPath(allOfSchemaPath));
+ }
+
foreach (var property in allOfPropertiesElement.EnumerateObject())
{
if (properties.ContainsKey(property.Name))
@@ -1994,12 +2003,21 @@ internal static class YamlConfigSchemaValidator
}
}
- if (!allOfSchemaElement.TryGetProperty("required", out var allOfRequiredElement) ||
- allOfRequiredElement.ValueKind != JsonValueKind.Array)
+ if (!allOfSchemaElement.TryGetProperty("required", out var allOfRequiredElement))
{
return;
}
+ if (allOfRequiredElement.ValueKind != JsonValueKind.Array)
+ {
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.SchemaUnsupported,
+ tableName,
+ $"Entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' as an array of property names.",
+ schemaPath: schemaPath,
+ displayPath: GetDiagnosticPath(allOfSchemaPath));
+ }
+
foreach (var requiredProperty in allOfRequiredElement.EnumerateArray())
{
if (requiredProperty.ValueKind != JsonValueKind.String)
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
index 314df325..030d12f1 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
@@ -949,6 +949,110 @@ public class SchemaConfigGeneratorTests
});
}
+ ///
+ /// 验证生成器会拒绝把 allOf.properties 声明为非对象映射。
+ ///
+ [Test]
+ public void Run_Should_Report_Diagnostic_When_AllOf_Entry_Properties_Is_Not_Object_Valued()
+ {
+ const string source = """
+ namespace TestApp
+ {
+ public sealed class Dummy
+ {
+ }
+ }
+ """;
+
+ const string schema = """
+ {
+ "type": "object",
+ "required": ["id", "reward"],
+ "properties": {
+ "id": { "type": "integer" },
+ "reward": {
+ "type": "object",
+ "properties": {
+ "itemCount": { "type": "integer" }
+ },
+ "allOf": [
+ {
+ "type": "object",
+ "properties": 1
+ }
+ ]
+ }
+ }
+ }
+ """;
+
+ var result = SchemaGeneratorTestDriver.Run(
+ source,
+ ("monster.schema.json", schema));
+
+ var diagnostic = result.Results.Single().Diagnostics.Single();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_012"));
+ Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
+ Assert.That(diagnostic.GetMessage(), Does.Contain("reward[allOf[0]]"));
+ Assert.That(diagnostic.GetMessage(), Does.Contain("must declare 'properties' as an object-valued map"));
+ });
+ }
+
+ ///
+ /// 验证生成器会拒绝把 allOf.required 声明为非数组。
+ ///
+ [Test]
+ public void Run_Should_Report_Diagnostic_When_AllOf_Entry_Required_Is_Not_An_Array()
+ {
+ const string source = """
+ namespace TestApp
+ {
+ public sealed class Dummy
+ {
+ }
+ }
+ """;
+
+ const string schema = """
+ {
+ "type": "object",
+ "required": ["id", "reward"],
+ "properties": {
+ "id": { "type": "integer" },
+ "reward": {
+ "type": "object",
+ "properties": {
+ "itemCount": { "type": "integer" }
+ },
+ "allOf": [
+ {
+ "type": "object",
+ "required": {}
+ }
+ ]
+ }
+ }
+ }
+ """;
+
+ var result = SchemaGeneratorTestDriver.Run(
+ source,
+ ("monster.schema.json", schema));
+
+ var diagnostic = result.Results.Single().Diagnostics.Single();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_012"));
+ Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
+ Assert.That(diagnostic.GetMessage(), Does.Contain("reward[allOf[0]]"));
+ Assert.That(diagnostic.GetMessage(), Does.Contain("must declare 'required' as an array of parent property names"));
+ });
+ }
+
///
/// 验证生成器会拒绝在 allOf 中引入父对象未声明的字段。
///