mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +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;
|
diagnostic = null;
|
||||||
var allOfEntryPath = BuildAllOfEntryPath(displayPath, allOfIndex);
|
var allOfEntryPath = BuildAllOfEntryPath(displayPath, allOfIndex);
|
||||||
|
|
||||||
if (allOfSchema.TryGetProperty("properties", out var allOfPropertiesElement) &&
|
if (allOfSchema.TryGetProperty("properties", out var allOfPropertiesElement))
|
||||||
allOfPropertiesElement.ValueKind == JsonValueKind.Object)
|
|
||||||
{
|
{
|
||||||
|
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())
|
foreach (var property in allOfPropertiesElement.EnumerateObject())
|
||||||
{
|
{
|
||||||
if (declaredProperties.Contains(property.Name))
|
if (declaredProperties.Contains(property.Name))
|
||||||
@ -1377,12 +1387,22 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allOfSchema.TryGetProperty("required", out var requiredElement) ||
|
if (!allOfSchema.TryGetProperty("required", out var requiredElement))
|
||||||
requiredElement.ValueKind != JsonValueKind.Array)
|
|
||||||
{
|
{
|
||||||
return true;
|
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())
|
foreach (var requiredProperty in requiredElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
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>
|
/// <summary>
|
||||||
/// 验证 allOf 条目不能要求父对象未声明的字段。
|
/// 验证 allOf 条目不能要求父对象未声明的字段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1975,9 +1975,18 @@ internal static class YamlConfigSchemaValidator
|
|||||||
JsonElement allOfSchemaElement,
|
JsonElement allOfSchemaElement,
|
||||||
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
IReadOnlyDictionary<string, YamlConfigSchemaNode> properties)
|
||||||
{
|
{
|
||||||
if (allOfSchemaElement.TryGetProperty("properties", out var allOfPropertiesElement) &&
|
if (allOfSchemaElement.TryGetProperty("properties", out var allOfPropertiesElement))
|
||||||
allOfPropertiesElement.ValueKind == JsonValueKind.Object)
|
|
||||||
{
|
{
|
||||||
|
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())
|
foreach (var property in allOfPropertiesElement.EnumerateObject())
|
||||||
{
|
{
|
||||||
if (properties.ContainsKey(property.Name))
|
if (properties.ContainsKey(property.Name))
|
||||||
@ -1994,12 +2003,21 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allOfSchemaElement.TryGetProperty("required", out var allOfRequiredElement) ||
|
if (!allOfSchemaElement.TryGetProperty("required", out var allOfRequiredElement))
|
||||||
allOfRequiredElement.ValueKind != JsonValueKind.Array)
|
|
||||||
{
|
{
|
||||||
return;
|
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())
|
foreach (var requiredProperty in allOfRequiredElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (requiredProperty.ValueKind != JsonValueKind.String)
|
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>
|
/// <summary>
|
||||||
/// 验证生成器会拒绝在 <c>allOf</c> 中引入父对象未声明的字段。
|
/// 验证生成器会拒绝在 <c>allOf</c> 中引入父对象未声明的字段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user