mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-04-02 20:09:00 +08:00
feat(generator): 添加JSON schema配置代码生成功能
- 实现SchemaConfigGenerator源代码生成器 - 支持从JSON schema文件生成配置类型和表包装类 - 添加ConfigSchemaDiagnostics诊断系统 - 集成System.Text.Json包依赖 - 生成强类型的配置访问接口 - 支持多种数据类型包括整数、浮点数、布尔值、字符串和数组 - 实现id字段作为表主键的约束验证 - 添加完整的单元测试和快照验证
This commit is contained in:
parent
5fa12dcd37
commit
c9d2306295
@ -0,0 +1,125 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 schema 配置生成器的生成快照。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class SchemaConfigGeneratorSnapshotTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证一个最小 monster schema 能生成配置类型和表包装。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task Snapshot_SchemaConfigGenerator()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.Config
|
||||||
|
{
|
||||||
|
public interface IConfigTable
|
||||||
|
{
|
||||||
|
Type KeyType { get; }
|
||||||
|
Type ValueType { get; }
|
||||||
|
int Count { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IConfigTable<TKey, TValue> : IConfigTable
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
TValue Get(TKey key);
|
||||||
|
bool TryGet(TKey key, out TValue? value);
|
||||||
|
bool ContainsKey(TKey key);
|
||||||
|
IReadOnlyCollection<TValue> All();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"hp": { "type": "integer" },
|
||||||
|
"dropItems": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", schema));
|
||||||
|
|
||||||
|
var generatedSources = result.Results
|
||||||
|
.Single()
|
||||||
|
.GeneratedSources
|
||||||
|
.ToDictionary(
|
||||||
|
static sourceResult => sourceResult.HintName,
|
||||||
|
static sourceResult => sourceResult.SourceText.ToString(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var snapshotFolder = Path.Combine(
|
||||||
|
TestContext.CurrentContext.TestDirectory,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"Config",
|
||||||
|
"snapshots",
|
||||||
|
"SchemaConfigGenerator");
|
||||||
|
snapshotFolder = Path.GetFullPath(snapshotFolder);
|
||||||
|
|
||||||
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt");
|
||||||
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对单个生成文件执行快照断言。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="generatedSources">生成结果字典。</param>
|
||||||
|
/// <param name="snapshotFolder">快照目录。</param>
|
||||||
|
/// <param name="fileName">快照文件名。</param>
|
||||||
|
private static async Task AssertSnapshotAsync(
|
||||||
|
IReadOnlyDictionary<string, string> generatedSources,
|
||||||
|
string snapshotFolder,
|
||||||
|
string generatedFileName,
|
||||||
|
string snapshotFileName)
|
||||||
|
{
|
||||||
|
if (!generatedSources.TryGetValue(generatedFileName, out var actual))
|
||||||
|
{
|
||||||
|
Assert.Fail($"Generated source '{generatedFileName}' was not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.Combine(snapshotFolder, snapshotFileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(snapshotFolder);
|
||||||
|
await File.WriteAllTextAsync(path, actual);
|
||||||
|
Assert.Fail($"Snapshot not found. Generated new snapshot at:\n{path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = await File.ReadAllTextAsync(path);
|
||||||
|
Assert.That(
|
||||||
|
Normalize(expected),
|
||||||
|
Is.EqualTo(Normalize(actual)),
|
||||||
|
$"Snapshot mismatch: {generatedFileName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准化快照文本以避免平台换行差异。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">原始文本。</param>
|
||||||
|
/// <returns>标准化后的文本。</returns>
|
||||||
|
private static string Normalize(string text)
|
||||||
|
{
|
||||||
|
return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
namespace GFramework.SourceGenerators.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 schema 配置生成器的错误诊断行为。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class SchemaConfigGeneratorTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证缺失必填 id 字段时会产生命名明确的诊断。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Report_Diagnostic_When_Id_Property_Is_Missing()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class Dummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", schema));
|
||||||
|
|
||||||
|
var diagnostics = result.Results.Single().Diagnostics;
|
||||||
|
var diagnostic = diagnostics.Single();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_003"));
|
||||||
|
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||||
|
Assert.That(diagnostic.GetMessage(), Does.Contain("monster.schema.json"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
using GFramework.SourceGenerators.Config;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Tests.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 schema 配置生成器提供测试驱动。
|
||||||
|
/// 该驱动直接使用 Roslyn GeneratorDriver 运行 AdditionalFiles 场景,
|
||||||
|
/// 以便测试基于 schema 文件的代码生成行为。
|
||||||
|
/// </summary>
|
||||||
|
public static class SchemaGeneratorTestDriver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 运行 schema 配置生成器,并返回生成结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">测试用源码。</param>
|
||||||
|
/// <param name="additionalFiles">AdditionalFiles 集合。</param>
|
||||||
|
/// <returns>生成器运行结果。</returns>
|
||||||
|
public static GeneratorDriverRunResult Run(
|
||||||
|
string source,
|
||||||
|
params (string path, string content)[] additionalFiles)
|
||||||
|
{
|
||||||
|
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||||
|
var compilation = CSharpCompilation.Create(
|
||||||
|
"SchemaConfigGeneratorTests",
|
||||||
|
new[] { syntaxTree },
|
||||||
|
GetMetadataReferences(),
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||||
|
|
||||||
|
var additionalTexts = additionalFiles
|
||||||
|
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
GeneratorDriver driver = CSharpGeneratorDriver.Create(
|
||||||
|
generators: new[] { new SchemaConfigGenerator().AsSourceGenerator() },
|
||||||
|
additionalTexts: additionalTexts,
|
||||||
|
parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||||
|
|
||||||
|
driver = driver.RunGenerators(compilation);
|
||||||
|
return driver.GetRunResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取测试编译所需的运行时元数据引用。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>元数据引用集合。</returns>
|
||||||
|
private static IEnumerable<MetadataReference> GetMetadataReferences()
|
||||||
|
{
|
||||||
|
var trustedPlatformAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))?
|
||||||
|
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
?? Array.Empty<string>();
|
||||||
|
|
||||||
|
return trustedPlatformAssemblies
|
||||||
|
.Select(static path => MetadataReference.CreateFromFile(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于测试 AdditionalFiles 的内存实现。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class InMemoryAdditionalText : AdditionalText
|
||||||
|
{
|
||||||
|
private readonly SourceText _text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建内存 AdditionalText。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">虚拟文件路径。</param>
|
||||||
|
/// <param name="content">文件内容。</param>
|
||||||
|
public InMemoryAdditionalText(
|
||||||
|
string path,
|
||||||
|
string content)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
_text = SourceText.From(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Path { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override SourceText GetText(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-generated config type for schema file 'monster.schema.json'.
|
||||||
|
/// This type is generated from JSON schema so runtime loading and editor tooling can share the same contract.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MonsterConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property 'id'.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property 'name'.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property 'hp'.
|
||||||
|
/// </summary>
|
||||||
|
public int? Hp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property 'dropItems'.
|
||||||
|
/// </summary>
|
||||||
|
public global::System.Collections.Generic.IReadOnlyList<string> DropItems { get; set; } = global::System.Array.Empty<string>();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-generated table wrapper for schema file 'monster.schema.json'.
|
||||||
|
/// The wrapper keeps generated call sites strongly typed while delegating actual storage to the runtime config table implementation.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MonsterTable : global::GFramework.Game.Abstractions.Config.IConfigTable<int, MonsterConfig>
|
||||||
|
{
|
||||||
|
private readonly global::GFramework.Game.Abstractions.Config.IConfigTable<int, MonsterConfig> _inner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a generated table wrapper around the runtime config table instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inner">The runtime config table instance.</param>
|
||||||
|
public MonsterTable(global::GFramework.Game.Abstractions.Config.IConfigTable<int, MonsterConfig> inner)
|
||||||
|
{
|
||||||
|
_inner = inner ?? throw new global::System.ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public global::System.Type KeyType => _inner.KeyType;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public global::System.Type ValueType => _inner.ValueType;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Count => _inner.Count;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MonsterConfig Get(int key)
|
||||||
|
{
|
||||||
|
return _inner.Get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGet(int key, out MonsterConfig? value)
|
||||||
|
{
|
||||||
|
return _inner.TryGet(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool ContainsKey(int key)
|
||||||
|
{
|
||||||
|
return _inner.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public global::System.Collections.Generic.IReadOnlyCollection<MonsterConfig> All()
|
||||||
|
{
|
||||||
|
return _inner.All();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
### New Rules
|
### New Rules
|
||||||
|
|
||||||
Rule ID | Category | Severity | Notes
|
Rule ID | Category | Severity | Notes
|
||||||
-----------------------|----------------------------------|----------|------------------------
|
-----------------------|------------------------------------|----------|-------------------------
|
||||||
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
|
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
|
||||||
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
|
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
|
||||||
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
@ -15,6 +15,11 @@
|
|||||||
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||||
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||||
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||||
|
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
|
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||||
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
||||||
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
||||||
|
|||||||
526
GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
Normal file
526
GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using GFramework.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 AdditionalFiles 中的 JSON schema 生成配置类型和配置表包装。
|
||||||
|
/// 当前实现聚焦 Runtime MVP 需要的最小能力:单 schema 对应单配置类型,并约定使用必填的 id 字段作为表主键。
|
||||||
|
/// </summary>
|
||||||
|
[Generator]
|
||||||
|
public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
private const string GeneratedNamespace = "GFramework.Game.Config.Generated";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
var schemaFiles = context.AdditionalTextsProvider
|
||||||
|
.Where(static file => file.Path.EndsWith(".schema.json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(static (file, cancellationToken) => ParseSchema(file, cancellationToken));
|
||||||
|
|
||||||
|
context.RegisterSourceOutput(schemaFiles, static (productionContext, result) =>
|
||||||
|
{
|
||||||
|
foreach (var diagnostic in result.Diagnostics)
|
||||||
|
{
|
||||||
|
productionContext.ReportDiagnostic(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Schema is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
productionContext.AddSource(
|
||||||
|
$"{result.Schema.ClassName}.g.cs",
|
||||||
|
SourceText.From(GenerateConfigClass(result.Schema), Encoding.UTF8));
|
||||||
|
productionContext.AddSource(
|
||||||
|
$"{result.Schema.TableName}.g.cs",
|
||||||
|
SourceText.From(GenerateTableClass(result.Schema), Encoding.UTF8));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析单个 schema 文件。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file">AdditionalFiles 中的 schema 文件。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>解析结果,包含 schema 模型或诊断。</returns>
|
||||||
|
private static SchemaParseResult ParseSchema(
|
||||||
|
AdditionalText file,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SourceText? text;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
text = file.GetText(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.InvalidSchemaJson,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path),
|
||||||
|
exception.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text is null)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.InvalidSchemaJson,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path),
|
||||||
|
"File content could not be read."));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(text.ToString());
|
||||||
|
var root = document.RootElement;
|
||||||
|
if (!root.TryGetProperty("type", out var rootTypeElement) ||
|
||||||
|
!string.Equals(rootTypeElement.GetString(), "object", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.RootObjectSchemaRequired,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("properties", out var propertiesElement) ||
|
||||||
|
propertiesElement.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.RootObjectSchemaRequired,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (root.TryGetProperty("required", out var requiredElement) &&
|
||||||
|
requiredElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in requiredElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var value = item.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
requiredProperties.Add(value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = new List<SchemaPropertySpec>();
|
||||||
|
foreach (var property in propertiesElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
var parsedProperty = ParseProperty(file.Path, property, requiredProperties.Contains(property.Name));
|
||||||
|
if (parsedProperty.Diagnostic is not null)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(parsedProperty.Diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.Add(parsedProperty.Property!);
|
||||||
|
}
|
||||||
|
|
||||||
|
var idProperty = properties.FirstOrDefault(static property =>
|
||||||
|
string.Equals(property.SchemaName, "id", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (idProperty is null || !idProperty.IsRequired)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.IdPropertyRequired,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(idProperty.SchemaType, "integer", StringComparison.Ordinal) &&
|
||||||
|
!string.Equals(idProperty.SchemaType, "string", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.UnsupportedKeyType,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path),
|
||||||
|
idProperty.SchemaType));
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityName = ToPascalCase(GetSchemaBaseName(file.Path));
|
||||||
|
var schema = new SchemaFileSpec(
|
||||||
|
Path.GetFileName(file.Path),
|
||||||
|
entityName,
|
||||||
|
$"{entityName}Config",
|
||||||
|
$"{entityName}Table",
|
||||||
|
GeneratedNamespace,
|
||||||
|
idProperty.ClrType,
|
||||||
|
properties);
|
||||||
|
|
||||||
|
return SchemaParseResult.FromSchema(schema);
|
||||||
|
}
|
||||||
|
catch (JsonException exception)
|
||||||
|
{
|
||||||
|
return SchemaParseResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.InvalidSchemaJson,
|
||||||
|
CreateFileLocation(file.Path),
|
||||||
|
Path.GetFileName(file.Path),
|
||||||
|
exception.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析单个 schema 属性定义。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">schema 文件路径。</param>
|
||||||
|
/// <param name="property">属性 JSON 节点。</param>
|
||||||
|
/// <param name="isRequired">属性是否必填。</param>
|
||||||
|
/// <returns>解析后的属性信息或诊断。</returns>
|
||||||
|
private static ParsedPropertyResult ParseProperty(
|
||||||
|
string filePath,
|
||||||
|
JsonProperty property,
|
||||||
|
bool isRequired)
|
||||||
|
{
|
||||||
|
if (!property.Value.TryGetProperty("type", out var typeElement) ||
|
||||||
|
typeElement.ValueKind != JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return ParsedPropertyResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.UnsupportedPropertyType,
|
||||||
|
CreateFileLocation(filePath),
|
||||||
|
Path.GetFileName(filePath),
|
||||||
|
property.Name,
|
||||||
|
"<missing>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemaType = typeElement.GetString() ?? string.Empty;
|
||||||
|
switch (schemaType)
|
||||||
|
{
|
||||||
|
case "integer":
|
||||||
|
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
|
||||||
|
property.Name,
|
||||||
|
ToPascalCase(property.Name),
|
||||||
|
"integer",
|
||||||
|
isRequired ? "int" : "int?",
|
||||||
|
isRequired,
|
||||||
|
null));
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
|
||||||
|
property.Name,
|
||||||
|
ToPascalCase(property.Name),
|
||||||
|
"number",
|
||||||
|
isRequired ? "double" : "double?",
|
||||||
|
isRequired,
|
||||||
|
null));
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
|
||||||
|
property.Name,
|
||||||
|
ToPascalCase(property.Name),
|
||||||
|
"boolean",
|
||||||
|
isRequired ? "bool" : "bool?",
|
||||||
|
isRequired,
|
||||||
|
null));
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
|
||||||
|
property.Name,
|
||||||
|
ToPascalCase(property.Name),
|
||||||
|
"string",
|
||||||
|
isRequired ? "string" : "string?",
|
||||||
|
isRequired,
|
||||||
|
isRequired ? " = string.Empty;" : null));
|
||||||
|
|
||||||
|
case "array":
|
||||||
|
if (!property.Value.TryGetProperty("items", out var itemsElement) ||
|
||||||
|
!itemsElement.TryGetProperty("type", out var itemTypeElement) ||
|
||||||
|
itemTypeElement.ValueKind != JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return ParsedPropertyResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.UnsupportedPropertyType,
|
||||||
|
CreateFileLocation(filePath),
|
||||||
|
Path.GetFileName(filePath),
|
||||||
|
property.Name,
|
||||||
|
"array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemType = itemTypeElement.GetString() ?? string.Empty;
|
||||||
|
var itemClrType = itemType switch
|
||||||
|
{
|
||||||
|
"integer" => "int",
|
||||||
|
"number" => "double",
|
||||||
|
"boolean" => "bool",
|
||||||
|
"string" => "string",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(itemClrType))
|
||||||
|
{
|
||||||
|
return ParsedPropertyResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.UnsupportedPropertyType,
|
||||||
|
CreateFileLocation(filePath),
|
||||||
|
Path.GetFileName(filePath),
|
||||||
|
property.Name,
|
||||||
|
$"array<{itemType}>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
|
||||||
|
property.Name,
|
||||||
|
ToPascalCase(property.Name),
|
||||||
|
"array",
|
||||||
|
$"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>",
|
||||||
|
isRequired,
|
||||||
|
" = global::System.Array.Empty<" + itemClrType + ">();"));
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ParsedPropertyResult.FromDiagnostic(
|
||||||
|
Diagnostic.Create(
|
||||||
|
ConfigSchemaDiagnostics.UnsupportedPropertyType,
|
||||||
|
CreateFileLocation(filePath),
|
||||||
|
Path.GetFileName(filePath),
|
||||||
|
property.Name,
|
||||||
|
schemaType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成配置类型源码。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schema">已解析的 schema 模型。</param>
|
||||||
|
/// <returns>配置类型源码。</returns>
|
||||||
|
private static string GenerateConfigClass(SchemaFileSpec schema)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine($"namespace {schema.Namespace};");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("/// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$"/// Auto-generated config type for schema file '{schema.FileName}'.");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// This type is generated from JSON schema so runtime loading and editor tooling can share the same contract.");
|
||||||
|
builder.AppendLine("/// </summary>");
|
||||||
|
builder.AppendLine($"public sealed partial class {schema.ClassName}");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
|
||||||
|
foreach (var property in schema.Properties)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" /// Gets or sets the value mapped from schema property '{property.SchemaName}'.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.Append($" public {property.ClrType} {property.PropertyName} {{ get; set; }}");
|
||||||
|
if (!string.IsNullOrEmpty(property.Initializer))
|
||||||
|
{
|
||||||
|
builder.Append(property.Initializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成配置表包装源码。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schema">已解析的 schema 模型。</param>
|
||||||
|
/// <returns>配置表包装源码。</returns>
|
||||||
|
private static string GenerateTableClass(SchemaFileSpec schema)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine($"namespace {schema.Namespace};");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("/// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$"/// Auto-generated table wrapper for schema file '{schema.FileName}'.");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// The wrapper keeps generated call sites strongly typed while delegating actual storage to the runtime config table implementation.");
|
||||||
|
builder.AppendLine("/// </summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$"public sealed partial class {schema.TableName} : global::GFramework.Game.Abstractions.Config.IConfigTable<{schema.KeyClrType}, {schema.ClassName}>");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" private readonly global::GFramework.Game.Abstractions.Config.IConfigTable<{schema.KeyClrType}, {schema.ClassName}> _inner;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Creates a generated table wrapper around the runtime config table instance.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"inner\">The runtime config table instance.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" public {schema.TableName}(global::GFramework.Game.Abstractions.Config.IConfigTable<{schema.KeyClrType}, {schema.ClassName}> inner)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" _inner = inner ?? throw new global::System.ArgumentNullException(nameof(inner));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine(" public global::System.Type KeyType => _inner.KeyType;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine(" public global::System.Type ValueType => _inner.ValueType;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine(" public int Count => _inner.Count;");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine($" public {schema.ClassName} Get({schema.KeyClrType} key)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return _inner.Get(key);");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine($" public bool TryGet({schema.KeyClrType} key, out {schema.ClassName}? value)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return _inner.TryGet(key, out value);");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine($" public bool ContainsKey({schema.KeyClrType} key)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return _inner.ContainsKey(key);");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <inheritdoc />");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" public global::System.Collections.Generic.IReadOnlyCollection<{schema.ClassName}> All()");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" return _inner.All();");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 schema 文件路径提取实体基础名。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">schema 文件路径。</param>
|
||||||
|
/// <returns>去掉扩展名和 `.schema` 后缀的实体基础名。</returns>
|
||||||
|
private static string GetSchemaBaseName(string path)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(path);
|
||||||
|
if (fileName.EndsWith(".schema.json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return fileName.Substring(0, fileName.Length - ".schema.json".Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFileNameWithoutExtension(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 schema 名称转换为 PascalCase 标识符。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">原始名称。</param>
|
||||||
|
/// <returns>PascalCase 标识符。</returns>
|
||||||
|
private static string ToPascalCase(string value)
|
||||||
|
{
|
||||||
|
var tokens = value
|
||||||
|
.Split(new[] { '-', '_', '.', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(static token =>
|
||||||
|
char.ToUpperInvariant(token[0]) + token.Substring(1))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return tokens.Length == 0 ? "Config" : string.Concat(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为 AdditionalFiles 诊断创建文件位置。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">文件路径。</param>
|
||||||
|
/// <returns>指向文件开头的位置。</returns>
|
||||||
|
private static Location CreateFileLocation(string path)
|
||||||
|
{
|
||||||
|
return Location.Create(
|
||||||
|
path,
|
||||||
|
TextSpan.FromBounds(0, 0),
|
||||||
|
new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示单个 schema 文件的解析结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Schema">成功解析出的 schema 模型。</param>
|
||||||
|
/// <param name="Diagnostics">解析阶段产生的诊断。</param>
|
||||||
|
private sealed record SchemaParseResult(
|
||||||
|
SchemaFileSpec? Schema,
|
||||||
|
ImmutableArray<Diagnostic> Diagnostics)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从成功解析的 schema 模型创建结果。
|
||||||
|
/// </summary>
|
||||||
|
public static SchemaParseResult FromSchema(SchemaFileSpec schema)
|
||||||
|
{
|
||||||
|
return new SchemaParseResult(schema, ImmutableArray<Diagnostic>.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从单个诊断创建结果。
|
||||||
|
/// </summary>
|
||||||
|
public static SchemaParseResult FromDiagnostic(Diagnostic diagnostic)
|
||||||
|
{
|
||||||
|
return new SchemaParseResult(null, ImmutableArray.Create(diagnostic));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示已解析的 schema 文件模型。
|
||||||
|
/// </summary>
|
||||||
|
private sealed record SchemaFileSpec(
|
||||||
|
string FileName,
|
||||||
|
string EntityName,
|
||||||
|
string ClassName,
|
||||||
|
string TableName,
|
||||||
|
string Namespace,
|
||||||
|
string KeyClrType,
|
||||||
|
IReadOnlyList<SchemaPropertySpec> Properties);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示已解析的 schema 属性。
|
||||||
|
/// </summary>
|
||||||
|
private sealed record SchemaPropertySpec(
|
||||||
|
string SchemaName,
|
||||||
|
string PropertyName,
|
||||||
|
string SchemaType,
|
||||||
|
string ClrType,
|
||||||
|
bool IsRequired,
|
||||||
|
string? Initializer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示单个属性的解析结果。
|
||||||
|
/// </summary>
|
||||||
|
private sealed record ParsedPropertyResult(
|
||||||
|
SchemaPropertySpec? Property,
|
||||||
|
Diagnostic? Diagnostic)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从属性模型创建成功结果。
|
||||||
|
/// </summary>
|
||||||
|
public static ParsedPropertyResult FromProperty(SchemaPropertySpec property)
|
||||||
|
{
|
||||||
|
return new ParsedPropertyResult(property, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从诊断创建失败结果。
|
||||||
|
/// </summary>
|
||||||
|
public static ParsedPropertyResult FromDiagnostic(Diagnostic diagnostic)
|
||||||
|
{
|
||||||
|
return new ParsedPropertyResult(null, diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
|
|
||||||
|
namespace GFramework.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供配置 schema 代码生成相关诊断。
|
||||||
|
/// </summary>
|
||||||
|
public static class ConfigSchemaDiagnostics
|
||||||
|
{
|
||||||
|
private const string SourceGeneratorsConfigCategory = $"{PathContests.SourceGeneratorsPath}.Config";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema JSON 无法解析。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor InvalidSchemaJson = new(
|
||||||
|
"GF_ConfigSchema_001",
|
||||||
|
"Config schema JSON is invalid",
|
||||||
|
"Schema file '{0}' could not be parsed: {1}",
|
||||||
|
SourceGeneratorsConfigCategory,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 顶层必须是 object。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor RootObjectSchemaRequired = new(
|
||||||
|
"GF_ConfigSchema_002",
|
||||||
|
"Config schema root must describe an object",
|
||||||
|
"Schema file '{0}' must declare a root object schema",
|
||||||
|
SourceGeneratorsConfigCategory,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 必须声明 id 字段作为主键。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor IdPropertyRequired = new(
|
||||||
|
"GF_ConfigSchema_003",
|
||||||
|
"Config schema must declare an id property",
|
||||||
|
"Schema file '{0}' must declare a required 'id' property for table generation",
|
||||||
|
SourceGeneratorsConfigCategory,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 包含暂不支持的字段类型。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor UnsupportedPropertyType = new(
|
||||||
|
"GF_ConfigSchema_004",
|
||||||
|
"Config schema contains an unsupported property type",
|
||||||
|
"Property '{1}' in schema file '{0}' uses unsupported type '{2}'",
|
||||||
|
SourceGeneratorsConfigCategory,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 的 id 字段类型不支持作为主键。
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DiagnosticDescriptor UnsupportedKeyType = new(
|
||||||
|
"GF_ConfigSchema_005",
|
||||||
|
"Config schema uses an unsupported key type",
|
||||||
|
"Schema file '{0}' uses unsupported id type '{1}'. Supported key types are 'integer' and 'string'.",
|
||||||
|
SourceGeneratorsConfigCategory,
|
||||||
|
DiagnosticSeverity.Error,
|
||||||
|
true);
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.0.5" PrivateAssets="all"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Generator 编译期引用 SourceGenerators.Abstractions / Common / Core.Abstractions,但不打包 -->
|
<!-- Generator 编译期引用 SourceGenerators.Abstractions / Common / Core.Abstractions,但不打包 -->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user