using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;
using GFramework.SourceGenerators.Diagnostics;
namespace GFramework.SourceGenerators.Config;
///
/// 根据 AdditionalFiles 中的 JSON schema 生成配置类型和配置表包装。
/// 当前实现聚焦 AI-First 配置系统共享的最小 schema 子集,
/// 支持嵌套对象、对象数组、标量数组,以及可映射的 default / enum / ref-table 元数据。
///
[Generator]
public sealed class SchemaConfigGenerator : IIncrementalGenerator
{
private const string GeneratedNamespace = "GFramework.Game.Config.Generated";
///
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));
});
}
///
/// 解析单个 schema 文件。
///
/// AdditionalFiles 中的 schema 文件。
/// 取消令牌。
/// 解析结果,包含 schema 模型或诊断。
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)));
}
var entityName = ToPascalCase(GetSchemaBaseName(file.Path));
var rootObject = ParseObjectSpec(
file.Path,
root,
"",
$"{entityName}Config",
isRoot: true);
if (rootObject.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic);
}
var schemaObject = rootObject.Object!;
var idProperty = schemaObject.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 (idProperty.TypeSpec.SchemaType != "integer" &&
idProperty.TypeSpec.SchemaType != "string")
{
return SchemaParseResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedKeyType,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
idProperty.TypeSpec.SchemaType));
}
var schema = new SchemaFileSpec(
Path.GetFileName(file.Path),
schemaObject.ClassName,
$"{entityName}Table",
GeneratedNamespace,
idProperty.TypeSpec.ClrType.TrimEnd('?'),
TryGetMetadataString(root, "title"),
TryGetMetadataString(root, "description"),
schemaObject);
return SchemaParseResult.FromSchema(schema);
}
catch (JsonException exception)
{
return SchemaParseResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidSchemaJson,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
exception.Message));
}
}
///
/// 解析对象 schema,并递归构建子属性模型。
///
/// Schema 文件路径。
/// 对象 schema 节点。
/// 当前对象的逻辑字段路径。
/// 要生成的 CLR 类型名。
/// 是否为根对象。
/// 对象模型或诊断。
private static ParsedObjectResult ParseObjectSpec(
string filePath,
JsonElement element,
string displayPath,
string className,
bool isRoot = false)
{
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
return ParsedObjectResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.RootObjectSchemaRequired,
CreateFileLocation(filePath),
Path.GetFileName(filePath)));
}
var requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase);
if (element.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();
foreach (var property in propertiesElement.EnumerateObject())
{
var parsedProperty = ParseProperty(
filePath,
property,
requiredProperties.Contains(property.Name),
CombinePath(displayPath, property.Name));
if (parsedProperty.Diagnostic is not null)
{
return ParsedObjectResult.FromDiagnostic(parsedProperty.Diagnostic);
}
properties.Add(parsedProperty.Property!);
}
return ParsedObjectResult.FromObject(new SchemaObjectSpec(
displayPath,
className,
TryGetMetadataString(element, "title"),
TryGetMetadataString(element, "description"),
properties));
}
///
/// 解析单个 schema 属性定义。
///
/// Schema 文件路径。
/// 属性 JSON 节点。
/// 属性是否必填。
/// 逻辑字段路径。
/// 解析后的属性信息或诊断。
private static ParsedPropertyResult ParseProperty(
string filePath,
JsonProperty property,
bool isRequired,
string displayPath)
{
if (!property.Value.TryGetProperty("type", out var typeElement) ||
typeElement.ValueKind != JsonValueKind.String)
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
""));
}
var schemaType = typeElement.GetString() ?? string.Empty;
var title = TryGetMetadataString(property.Value, "title");
var description = TryGetMetadataString(property.Value, "description");
var refTableName = TryGetMetadataString(property.Value, "x-gframework-ref-table");
var propertyName = ToPascalCase(property.Name);
switch (schemaType)
{
case "integer":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"integer",
isRequired ? "int" : "int?",
TryBuildScalarInitializer(property.Value, "integer"),
TryBuildEnumDocumentation(property.Value, "integer"),
refTableName,
null,
null)));
case "number":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"number",
isRequired ? "double" : "double?",
TryBuildScalarInitializer(property.Value, "number"),
TryBuildEnumDocumentation(property.Value, "number"),
refTableName,
null,
null)));
case "boolean":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"boolean",
isRequired ? "bool" : "bool?",
TryBuildScalarInitializer(property.Value, "boolean"),
TryBuildEnumDocumentation(property.Value, "boolean"),
refTableName,
null,
null)));
case "string":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"string",
isRequired ? "string" : "string?",
TryBuildScalarInitializer(property.Value, "string") ??
(isRequired ? " = string.Empty;" : null),
TryBuildEnumDocumentation(property.Value, "string"),
refTableName,
null,
null)));
case "object":
if (!string.IsNullOrWhiteSpace(refTableName))
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"object-ref"));
}
var objectResult = ParseObjectSpec(
filePath,
property.Value,
displayPath,
$"{propertyName}Config");
if (objectResult.Diagnostic is not null)
{
return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic);
}
var objectSpec = objectResult.Object!;
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Object,
"object",
isRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?",
isRequired ? " = new();" : null,
null,
null,
objectSpec,
null)));
case "array":
return ParseArrayProperty(filePath, property, isRequired, displayPath, propertyName, title,
description, refTableName);
default:
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
schemaType));
}
}
///
/// 解析数组属性,支持标量数组与对象数组。
///
/// Schema 文件路径。
/// 属性 JSON 节点。
/// 属性是否必填。
/// 逻辑字段路径。
/// CLR 属性名。
/// 标题元数据。
/// 说明元数据。
/// 目标引用表名称。
/// 解析后的属性信息或诊断。
private static ParsedPropertyResult ParseArrayProperty(
string filePath,
JsonProperty property,
bool isRequired,
string displayPath,
string propertyName,
string? title,
string? description,
string? refTableName)
{
if (!property.Value.TryGetProperty("items", out var itemsElement) ||
itemsElement.ValueKind != JsonValueKind.Object ||
!itemsElement.TryGetProperty("type", out var itemTypeElement) ||
itemTypeElement.ValueKind != JsonValueKind.String)
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"array"));
}
var itemType = itemTypeElement.GetString() ?? string.Empty;
switch (itemType)
{
case "integer":
case "number":
case "boolean":
case "string":
var itemClrType = itemType switch
{
"integer" => "int",
"number" => "double",
"boolean" => "bool",
_ => "string"
};
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
new SchemaTypeSpec(
SchemaNodeKind.Array,
"array",
$"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>",
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
$" = global::System.Array.Empty<{itemClrType}>();",
TryBuildEnumDocumentation(itemsElement, itemType),
refTableName,
null,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
itemType,
itemClrType,
null,
TryBuildEnumDocumentation(itemsElement, itemType),
refTableName,
null,
null))));
case "object":
if (!string.IsNullOrWhiteSpace(refTableName))
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"array