mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(generator): 添加JSON schema配置生成器
- 实现了基于JSON schema自动生成配置类型和配置表包装的功能 - 支持嵌套对象、对象数组、标量数组的数据结构生成 - 添加了default/enum/const/ref-table元数据映射功能 - 实现了查找索引生成功能,支持唯一键快速检索 - 集成了诊断报告系统,提供详细的错误提示信息 - 生成配置类、表类和绑定类三种类型的源代码 - 支持日期、时间、邮箱等字符串格式验证功能 - 实现了依赖关系验证,确保schema间的引用正确性
This commit is contained in:
parent
66e1f06f2f
commit
faa0143799
@ -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)
|
||||
|
||||
@ -300,6 +300,86 @@ public sealed class YamlConfigLoaderAllOfTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 allOf 条目的 <c>properties</c> 必须声明为对象映射。
|
||||
/// </summary>
|
||||
[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<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("reward[allOf[0]]"));
|
||||
Assert.That(exception.Message, Does.Contain("must declare 'properties' as an object-valued map"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 allOf 条目的 <c>required</c> 必须声明为字段名数组。
|
||||
/// </summary>
|
||||
[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<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("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));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 allOf 条目不能要求父对象未声明的字段。
|
||||
/// </summary>
|
||||
|
||||
@ -1975,9 +1975,18 @@ internal static class YamlConfigSchemaValidator
|
||||
JsonElement allOfSchemaElement,
|
||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> 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)
|
||||
|
||||
@ -949,6 +949,110 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会拒绝把 <c>allOf.properties</c> 声明为非对象映射。
|
||||
/// </summary>
|
||||
[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"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会拒绝把 <c>allOf.required</c> 声明为非数组。
|
||||
/// </summary>
|
||||
[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"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会拒绝在 <c>allOf</c> 中引入父对象未声明的字段。
|
||||
/// </summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user