using GFramework.Game.SourceGenerators.Diagnostics;
namespace GFramework.Game.SourceGenerators.Config;
///
/// 根据 AdditionalFiles 中的 JSON schema 生成配置类型和配置表包装。
/// 当前实现聚焦 AI-First 配置系统共享的最小 schema 子集,
/// 支持嵌套对象、对象数组、标量数组,以及可映射的 default / enum / const / ref-table 元数据。
/// 当前共享子集也会把 multipleOf、uniqueItems、
/// contains / minContains / maxContains、
/// minProperties、maxProperties、dependentRequired、
/// dependentSchemas、allOf、object-focused if / then / else
/// 与稳定字符串 format 子集写入生成代码文档,
/// 让消费者能直接在强类型 API 上看到运行时生效且不改变生成类型形状的约束。
///
[Generator]
public sealed class SchemaConfigGenerator : IIncrementalGenerator
{
private const string ConfigPathMetadataKey = "x-gframework-config-path";
private const string LookupIndexMetadataKey = "x-gframework-index";
private const string GeneratedNamespace = "GFramework.Game.Config.Generated";
private const string LookupIndexTopLevelScalarOnlyMessage =
"Only top-level required non-key scalar properties can declare a generated lookup index.";
private const string LookupIndexRequiresRequiredScalarMessage =
"Generated lookup indexes currently require a required scalar property so dictionary keys remain non-null.";
private const string LookupIndexPrimaryKeyMessage =
"The primary key already has Get/TryGet lookup semantics and should not declare a generated lookup index.";
private const string LookupIndexReferencePropertyMessage =
"Reference properties are excluded from generated lookup indexes because they already carry cross-table semantics.";
private const string SupportedStringFormatNames =
"'date', 'date-time', 'duration', 'email', 'time', 'uri', and 'uuid'";
///
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));
productionContext.AddSource(
$"{result.Schema.EntityName}ConfigBindings.g.cs",
SourceText.From(GenerateBindingsClass(result.Schema), Encoding.UTF8));
});
var collectedSchemas = schemaFiles.Collect();
context.RegisterSourceOutput(collectedSchemas, static (productionContext, results) =>
{
var schemas = results
.Where(static result => result.Schema is not null)
.Select(static result => result.Schema!)
.OrderBy(static schema => schema.TableRegistrationName, StringComparer.Ordinal)
.ToArray();
if (schemas.Length == 0)
{
return;
}
productionContext.AddSource(
"GeneratedConfigCatalog.g.cs",
SourceText.From(GenerateCatalogClass(schemas), 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)));
}
if (!TryValidateStringFormatMetadataRecursively(
file.Path,
"",
root,
out var rootFormatDiagnostic))
{
return SchemaParseResult.FromDiagnostic(rootFormatDiagnostic!);
}
if (!TryValidateDependentRequiredMetadataRecursively(
file.Path,
"",
root,
out var dependentRequiredDiagnostic))
{
return SchemaParseResult.FromDiagnostic(dependentRequiredDiagnostic!);
}
if (!TryValidateDependentSchemasMetadataRecursively(
file.Path,
"",
root,
out var dependentSchemasDiagnostic))
{
return SchemaParseResult.FromDiagnostic(dependentSchemasDiagnostic!);
}
if (!TryValidateAllOfMetadataRecursively(
file.Path,
"",
root,
out var allOfDiagnostic))
{
return SchemaParseResult.FromDiagnostic(allOfDiagnostic!);
}
if (!TryValidateConditionalSchemasMetadataRecursively(
file.Path,
"",
root,
out var conditionalDiagnostic))
{
return SchemaParseResult.FromDiagnostic(conditionalDiagnostic!);
}
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 (!IsSchemaType(idProperty.TypeSpec.SchemaType, "integer") &&
!IsSchemaType(idProperty.TypeSpec.SchemaType, "string"))
{
return SchemaParseResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedKeyType,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
idProperty.TypeSpec.SchemaType));
}
var schemaBaseName = GetSchemaBaseName(file.Path);
var configRelativePath = ResolveConfigRelativePath(file.Path, root, schemaBaseName);
if (configRelativePath.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(configRelativePath.Diagnostic);
}
var schema = new SchemaFileSpec(
Path.GetFileName(file.Path),
entityName,
schemaObject.ClassName,
$"{entityName}Table",
GeneratedNamespace,
idProperty.TypeSpec.ClrType.TrimEnd('?'),
idProperty.PropertyName,
schemaBaseName,
configRelativePath.Path!,
GetSchemaRelativePath(file.Path),
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.Ordinal);
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),
isDirectChildOfRoot: isRoot);
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"),
TryBuildConstraintDocumentation(element, "object"),
properties));
}
///
/// 解析单个 schema 属性定义。
///
/// Schema 文件路径。
/// 属性 JSON 节点。
/// 属性是否必填。
/// 逻辑字段路径。
/// 属性是否为根对象下的直接子属性。
/// 解析后的属性信息或诊断。
private static ParsedPropertyResult ParseProperty(
string filePath,
JsonProperty property,
bool isRequired,
string displayPath,
bool isDirectChildOfRoot)
{
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");
if (!TryValidateStringFormatMetadata(
filePath,
displayPath,
property.Value,
schemaType,
out var formatDiagnostic))
{
return ParsedPropertyResult.FromDiagnostic(formatDiagnostic!);
}
var indexedLookupMetadata = TryGetMetadataBoolean(property.Value, LookupIndexMetadataKey);
if (indexedLookupMetadata.Diagnostic is not null)
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidLookupIndexMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
LookupIndexMetadataKey,
indexedLookupMetadata.Diagnostic!));
}
var isIndexedLookup = indexedLookupMetadata.Value ?? false;
if (!TryBuildPropertyIdentifier(filePath, displayPath, property.Name, out var propertyName, out var diagnostic))
{
return ParsedPropertyResult.FromDiagnostic(diagnostic!);
}
if (isIndexedLookup &&
!TryValidateIndexedLookupEligibility(
filePath,
property.Name,
displayPath,
isDirectChildOfRoot,
isRequired,
refTableName,
out diagnostic))
{
return ParsedPropertyResult.FromDiagnostic(diagnostic!);
}
switch (schemaType)
{
case "integer":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
isIndexedLookup,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"integer",
isRequired ? "int" : "int?",
TryBuildScalarInitializer(property.Value, "integer"),
TryBuildEnumDocumentation(property.Value, "integer"),
TryBuildConstraintDocumentation(property.Value, "integer"),
refTableName,
null,
null)));
case "number":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
isIndexedLookup,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"number",
isRequired ? "double" : "double?",
TryBuildScalarInitializer(property.Value, "number"),
TryBuildEnumDocumentation(property.Value, "number"),
TryBuildConstraintDocumentation(property.Value, "number"),
refTableName,
null,
null)));
case "boolean":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
isIndexedLookup,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"boolean",
isRequired ? "bool" : "bool?",
TryBuildScalarInitializer(property.Value, "boolean"),
TryBuildEnumDocumentation(property.Value, "boolean"),
TryBuildConstraintDocumentation(property.Value, "boolean"),
refTableName,
null,
null)));
case "string":
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
displayPath,
propertyName,
isRequired,
title,
description,
isIndexedLookup,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
"string",
isRequired ? "string" : "string?",
TryBuildScalarInitializer(property.Value, "string") ??
(isRequired ? " = string.Empty;" : null),
TryBuildEnumDocumentation(property.Value, "string"),
TryBuildConstraintDocumentation(property.Value, "string"),
refTableName,
null,
null)));
case "object":
if (isIndexedLookup)
{
return ParsedPropertyResult.FromDiagnostic(
CreateInvalidLookupIndexDiagnostic(filePath, displayPath,
LookupIndexTopLevelScalarOnlyMessage));
}
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,
false,
new SchemaTypeSpec(
SchemaNodeKind.Object,
"object",
isRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?",
isRequired ? " = new();" : null,
TryBuildEnumDocumentation(property.Value, "object"),
null,
null,
objectSpec,
null)));
case "array":
return ParseArrayProperty(filePath, property, isRequired, displayPath, propertyName, title,
description, refTableName, isIndexedLookup);
default:
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
schemaType));
}
}
///
/// 验证字段是否满足生成只读精确匹配索引的前提。
///
/// Schema 文件路径。
/// Schema 原始字段名。
/// 逻辑字段路径。
/// 字段是否直接从属于 schema 根对象。
/// 字段是否必填。
/// 可选的引用表名。
/// 不满足条件时输出的诊断。
/// 当前字段是否允许声明只读索引。
private static bool TryValidateIndexedLookupEligibility(
string filePath,
string schemaName,
string displayPath,
bool isDirectChildOfRoot,
bool isRequired,
string? refTableName,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!isDirectChildOfRoot)
{
diagnostic = CreateInvalidLookupIndexDiagnostic(
filePath,
displayPath,
LookupIndexTopLevelScalarOnlyMessage);
return false;
}
if (!isRequired)
{
diagnostic = CreateInvalidLookupIndexDiagnostic(
filePath,
displayPath,
LookupIndexRequiresRequiredScalarMessage);
return false;
}
if (string.Equals(schemaName, "id", StringComparison.OrdinalIgnoreCase))
{
diagnostic = CreateInvalidLookupIndexDiagnostic(
filePath,
displayPath,
LookupIndexPrimaryKeyMessage);
return false;
}
if (!string.IsNullOrWhiteSpace(refTableName))
{
diagnostic = CreateInvalidLookupIndexDiagnostic(
filePath,
displayPath,
LookupIndexReferencePropertyMessage);
return false;
}
return true;
}
///
/// 验证字符串 format 元数据是否属于当前共享支持子集。
/// 生成器不尝试解释开放格式名,而是直接在编译阶段拒绝三端无法稳定对齐的 schema。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 属性 schema 节点。
/// 当前 schema type。
/// 失败时返回的诊断。
/// 当前节点的 format 元数据是否有效。
private static bool TryValidateStringFormatMetadata(
string filePath,
string displayPath,
JsonElement element,
string schemaType,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("format", out var formatElement))
{
return true;
}
if (!string.Equals(schemaType, "string", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidStringFormatMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Only 'string' properties can declare 'format'.");
return false;
}
if (formatElement.ValueKind != JsonValueKind.String)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidStringFormatMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"The 'format' value must be a string.");
return false;
}
var formatName = formatElement.GetString() ?? string.Empty;
if (IsSupportedStringFormat(formatName))
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidStringFormatMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Unsupported string format '{formatName}'. Supported formats are {SupportedStringFormatNames}.");
return false;
}
///
/// 递归验证 schema 树中的字符串 format 元数据。
/// 该遍历专门补足根节点、contains 子 schema 等不会完全进入常规属性解析路径的片段,
/// 避免生成器对同一份 schema 比运行时和工具链更宽松。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树的 format 元数据是否有效。
private static bool TryValidateStringFormatMetadataRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
{
if (string.IsNullOrWhiteSpace(schemaType))
{
return (true, (Diagnostic?)null);
}
return TryValidateStringFormatMetadata(
currentFilePath,
currentDisplayPath,
currentElement,
schemaType,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 以统一顺序递归遍历 schema 树,并把每个节点交给调用方提供的校验逻辑。
/// 该遍历覆盖对象属性、dependentSchemas / allOf /
/// if / then / else / not 子 schema、
/// 数组 items 与 contains,
/// 避免不同关键字验证器在同一棵 schema 树上各自维护一份容易漂移的递归流程。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 当前节点的关键字校验回调。
/// 失败时返回的诊断。
/// 当前节点树是否通过指定关键字的校验。
private static bool TryTraverseSchemaRecursively(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (element.ValueKind != JsonValueKind.Object)
{
return true;
}
var schemaType = string.Empty;
if (element.TryGetProperty("type", out var typeElement) &&
typeElement.ValueKind == JsonValueKind.String)
{
schemaType = typeElement.GetString() ?? string.Empty;
}
var nodeValidationResult = nodeValidator(filePath, displayPath, element, schemaType);
if (!nodeValidationResult.IsValid)
{
diagnostic = nodeValidationResult.Diagnostic;
return false;
}
if (string.Equals(schemaType, "object", StringComparison.Ordinal) &&
element.TryGetProperty("properties", out var propertiesElement) &&
propertiesElement.ValueKind == JsonValueKind.Object)
{
foreach (var property in propertiesElement.EnumerateObject())
{
if (!TryTraverseSchemaRecursively(
filePath,
CombinePath(displayPath, property.Name),
property.Value,
nodeValidator,
out diagnostic))
{
return false;
}
}
}
if (string.Equals(schemaType, "object", StringComparison.Ordinal) &&
element.TryGetProperty("dependentSchemas", out var dependentSchemasElement) &&
dependentSchemasElement.ValueKind == JsonValueKind.Object)
{
foreach (var dependentSchema in dependentSchemasElement.EnumerateObject())
{
if (dependentSchema.Value.ValueKind != JsonValueKind.Object)
{
continue;
}
if (!TryTraverseSchemaRecursively(
filePath,
$"{displayPath}[dependentSchemas:{dependentSchema.Name}]",
dependentSchema.Value,
nodeValidator,
out diagnostic))
{
return false;
}
}
}
if (string.Equals(schemaType, "object", StringComparison.Ordinal) &&
element.TryGetProperty("allOf", out var allOfElement) &&
allOfElement.ValueKind == JsonValueKind.Array)
{
var allOfIndex = 0;
foreach (var allOfSchema in allOfElement.EnumerateArray())
{
if (allOfSchema.ValueKind != JsonValueKind.Object)
{
allOfIndex++;
continue;
}
if (!TryTraverseSchemaRecursively(
filePath,
BuildAllOfEntryPath(displayPath, allOfIndex),
allOfSchema,
nodeValidator,
out diagnostic))
{
return false;
}
allOfIndex++;
}
}
if (string.Equals(schemaType, "object", StringComparison.Ordinal))
{
if (element.TryGetProperty("if", out var ifElement) &&
ifElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
BuildConditionalSchemaPath(displayPath, "if"),
ifElement,
nodeValidator,
out diagnostic))
{
return false;
}
if (element.TryGetProperty("then", out var thenElement) &&
thenElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
BuildConditionalSchemaPath(displayPath, "then"),
thenElement,
nodeValidator,
out diagnostic))
{
return false;
}
if (element.TryGetProperty("else", out var elseElement) &&
elseElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
BuildConditionalSchemaPath(displayPath, "else"),
elseElement,
nodeValidator,
out diagnostic))
{
return false;
}
}
if (element.TryGetProperty("not", out var notElement) &&
notElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
$"{displayPath}[not]",
notElement,
nodeValidator,
out diagnostic))
{
return false;
}
if (!string.Equals(schemaType, "array", StringComparison.Ordinal))
{
return true;
}
if (element.TryGetProperty("items", out var itemsElement) &&
itemsElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
$"{displayPath}[]",
itemsElement,
nodeValidator,
out diagnostic))
{
return false;
}
if (element.TryGetProperty("contains", out var containsElement) &&
containsElement.ValueKind == JsonValueKind.Object &&
!TryTraverseSchemaRecursively(
filePath,
$"{displayPath}[contains]",
containsElement,
nodeValidator,
out diagnostic))
{
return false;
}
return true;
}
///
/// 为对象级 allOf 条目生成与运行时一致的逻辑路径。
///
/// 父对象路径。
/// 从 0 开始的条目索引。
/// 格式化后的 allOf 条目路径。
private static string BuildAllOfEntryPath(string displayPath, int allOfIndex)
{
return $"{displayPath}[allOf[{allOfIndex}]]";
}
///
/// 为 object-focused 条件分支生成与运行时一致的逻辑路径。
///
/// 父对象路径。
/// 条件关键字名称。
/// 格式化后的条件分支路径。
private static string BuildConditionalSchemaPath(string displayPath, string keywordName)
{
return $"{displayPath}[{keywordName}]";
}
///
/// 递归验证 schema 树中的对象级 dependentRequired 元数据。
/// 该遍历会覆盖根节点、dependentSchemas / not 子 schema、
/// 数组元素与 contains 子 schema,
/// 避免生成器在对象字段依赖规则上比运行时和工具侧更宽松。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树的 dependentRequired 元数据是否有效。
private static bool TryValidateDependentRequiredMetadataRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
{
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
{
return (true, (Diagnostic?)null);
}
return TryValidateDependentRequiredMetadata(
currentFilePath,
currentDisplayPath,
currentElement,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 验证单个对象 schema 节点上的 dependentRequired 元数据。
/// 生成器只接受“当前对象已声明字段之间”的依赖关系,避免强类型文档描述出运行时无法解析的无效键名。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前对象 schema 节点。
/// 失败时返回的诊断。
/// 当前对象上的 dependentRequired 元数据是否有效。
private static bool TryValidateDependentRequiredMetadata(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("dependentRequired", out var dependentRequiredElement))
{
return true;
}
if (dependentRequiredElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"The 'dependentRequired' value must be an object.");
return false;
}
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'dependentRequired' must also declare an object-valued 'properties' map.");
return false;
}
var declaredProperties = new HashSet(
propertiesElement
.EnumerateObject()
.Select(static property => property.Name),
StringComparer.Ordinal);
foreach (var dependency in dependentRequiredElement.EnumerateObject())
{
if (!declaredProperties.Contains(dependency.Name))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Trigger property '{dependency.Name}' is not declared in the same object schema.");
return false;
}
if (dependency.Value.ValueKind != JsonValueKind.Array)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependency.Name}' must declare 'dependentRequired' as an array of sibling property names.");
return false;
}
foreach (var dependencyTarget in dependency.Value.EnumerateArray())
{
if (dependencyTarget.ValueKind != JsonValueKind.String)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependency.Name}' must declare 'dependentRequired' entries as strings.");
return false;
}
var dependencyTargetName = dependencyTarget.GetString();
if (string.IsNullOrWhiteSpace(dependencyTargetName))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependency.Name}' cannot declare blank 'dependentRequired' entries.");
return false;
}
var normalizedDependencyTargetName = dependencyTargetName!;
if (!declaredProperties.Contains(normalizedDependencyTargetName))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Dependent target '{normalizedDependencyTargetName}' is not declared in the same object schema.");
return false;
}
}
}
return true;
}
///
/// 验证当前 schema 节点是否以运行时支持的方式声明了 dependentSchemas。
/// 只有 object 节点允许挂载该关键字;一旦关键字出现,就继续复用对象节点的形状校验,
/// 保证发布到 XML 文档和运行时的约束解释范围保持一致。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 当前节点声明的 schema 类型。
/// 失败时返回的诊断。
/// 当前节点上的 dependentSchemas 声明是否有效。
private static bool TryValidateDependentSchemasDeclaration(
string filePath,
string displayPath,
JsonElement element,
string? schemaType,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("dependentSchemas", out _))
{
return true;
}
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Only object schemas can declare 'dependentSchemas'.");
return false;
}
return TryValidateDependentSchemasMetadata(filePath, displayPath, element, out diagnostic);
}
///
/// 递归验证 schema 树中的对象级 dependentSchemas 元数据。
/// 该遍历会覆盖根节点、not、数组元素、contains 与嵌套 dependentSchemas,
/// 确保生成器对条件对象子 schema 的接受范围不会比运行时更宽松。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树的 dependentSchemas 元数据是否有效。
private static bool TryValidateDependentSchemasMetadataRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
{
return TryValidateDependentSchemasDeclaration(
currentFilePath,
currentDisplayPath,
currentElement,
schemaType,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 验证单个对象 schema 节点上的 dependentSchemas 元数据。
/// 生成器当前只接受“已声明 sibling 字段触发 object 子 schema”的形状,
/// 避免 XML 文档描述出运行时无法识别的条件 schema。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前对象 schema 节点。
/// 失败时返回的诊断。
/// 当前对象上的 dependentSchemas 元数据是否有效。
private static bool TryValidateDependentSchemasMetadata(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("dependentSchemas", out var dependentSchemasElement))
{
return true;
}
if (dependentSchemasElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"The 'dependentSchemas' value must be an object.");
return false;
}
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'dependentSchemas' must also declare an object-valued 'properties' map.");
return false;
}
var declaredProperties = new HashSet(
propertiesElement
.EnumerateObject()
.Select(static property => property.Name),
StringComparer.Ordinal);
foreach (var dependency in dependentSchemasElement.EnumerateObject())
{
if (!declaredProperties.Contains(dependency.Name))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Trigger property '{dependency.Name}' is not declared in the same object schema.");
return false;
}
if (dependency.Value.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependency.Name}' must declare 'dependentSchemas' as an object-valued schema.");
return false;
}
if (!dependency.Value.TryGetProperty("type", out var dependentSchemaTypeElement) ||
dependentSchemaTypeElement.ValueKind != JsonValueKind.String ||
!string.Equals(dependentSchemaTypeElement.GetString(), "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependency.Name}' must declare an object-typed 'dependentSchemas' schema.");
return false;
}
}
return true;
}
///
/// 验证当前 schema 节点是否以运行时支持的方式声明了 allOf。
/// 当前共享子集只接受 object 节点上的 object-typed inline schema 数组,
/// 以便把它们解释成 focused constraint block,而不引入额外的类型合并语义。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 当前节点声明的 schema 类型。
/// 失败时返回的诊断。
/// 当前节点上的 allOf 声明是否有效。
private static bool TryValidateAllOfDeclaration(
string filePath,
string displayPath,
JsonElement element,
string? schemaType,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("allOf", out _))
{
return true;
}
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Only object schemas can declare 'allOf'.");
return false;
}
return TryValidateAllOfMetadata(filePath, displayPath, element, out diagnostic);
}
///
/// 递归验证 schema 树中的对象级 allOf 元数据。
/// 该遍历会覆盖根节点、not、数组元素、contains、dependentSchemas
/// 与嵌套 allOf,确保生成器对组合约束的接受范围与运行时保持一致。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树的 allOf 元数据是否有效。
private static bool TryValidateAllOfMetadataRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
{
return TryValidateAllOfDeclaration(
currentFilePath,
currentDisplayPath,
currentElement,
schemaType,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 验证单个对象 schema 节点上的 allOf 元数据。
/// 生成器当前只接受 object-typed inline schema 数组,
/// 避免 XML 文档描述出运行时不会按 focused constraint block 解释的组合形状。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前对象 schema 节点。
/// 失败时返回的诊断。
/// 当前对象上的 allOf 元数据是否有效。
private static bool TryValidateAllOfMetadata(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("allOf", out var allOfElement))
{
return true;
}
if (allOfElement.ValueKind != JsonValueKind.Array)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"The 'allOf' value must be an array.");
return false;
}
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'allOf' must also declare an object-valued 'properties' map.");
return false;
}
var declaredProperties = new HashSet(
propertiesElement
.EnumerateObject()
.Select(static property => property.Name),
StringComparer.Ordinal);
var allOfIndex = 0;
foreach (var allOfSchema in allOfElement.EnumerateArray())
{
if (allOfSchema.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Entry #{allOfIndex + 1} in 'allOf' must be an object-valued schema.");
return false;
}
if (!allOfSchema.TryGetProperty("type", out var allOfTypeElement) ||
allOfTypeElement.ValueKind != JsonValueKind.String ||
!string.Equals(allOfTypeElement.GetString(), "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Entry #{allOfIndex + 1} in 'allOf' must declare an object-typed schema.");
return false;
}
if (!TryValidateAllOfEntryTargets(
filePath,
displayPath,
allOfSchema,
allOfIndex,
declaredProperties,
out diagnostic))
{
return false;
}
allOfIndex++;
}
return true;
}
///
/// 验证当前 schema 节点是否以运行时支持的方式声明了 object-focused if / then / else。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 当前节点声明的 schema 类型。
/// 失败时返回的诊断。
/// 当前节点上的条件元数据是否有效。
private static bool TryValidateConditionalSchemasDeclaration(
string filePath,
string displayPath,
JsonElement element,
string? schemaType,
out Diagnostic? diagnostic)
{
diagnostic = null;
var hasIf = element.TryGetProperty("if", out _);
var hasThen = element.TryGetProperty("then", out _);
var hasElse = element.TryGetProperty("else", out _);
if (!hasIf && !hasThen && !hasElse)
{
return true;
}
if (!string.Equals(schemaType, "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Only object schemas can declare 'if', 'then', or 'else'.");
return false;
}
return TryValidateConditionalSchemasMetadata(filePath, displayPath, element, out diagnostic);
}
///
/// 递归验证 schema 树中的 object-focused if / then / else 元数据。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树的条件元数据是否有效。
private static bool TryValidateConditionalSchemasMetadataRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, schemaType) =>
{
return TryValidateConditionalSchemasDeclaration(
currentFilePath,
currentDisplayPath,
currentElement,
schemaType,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 验证单个对象 schema 节点上的 object-focused 条件元数据。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前对象 schema 节点。
/// 失败时返回的诊断。
/// 当前对象上的条件元数据是否有效。
private static bool TryValidateConditionalSchemasMetadata(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
var hasIf = element.TryGetProperty("if", out var ifElement);
var hasThen = element.TryGetProperty("then", out var thenElement);
var hasElse = element.TryGetProperty("else", out var elseElement);
if (!hasIf && !hasThen && !hasElse)
{
return true;
}
if (!hasIf)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'then' or 'else' must also declare 'if'.");
return false;
}
if (!hasThen && !hasElse)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'if' must also declare at least one of 'then' or 'else'.");
return false;
}
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
"Object schemas using 'if/then/else' must also declare an object-valued 'properties' map.");
return false;
}
var declaredProperties = new HashSet(
propertiesElement
.EnumerateObject()
.Select(static property => property.Name),
StringComparer.Ordinal);
if (!TryValidateConditionalSchemaBranch(
filePath,
displayPath,
ifElement,
"if",
declaredProperties,
out diagnostic))
{
return false;
}
if (hasThen &&
!TryValidateConditionalSchemaBranch(
filePath,
displayPath,
thenElement,
"then",
declaredProperties,
out diagnostic))
{
return false;
}
return !hasElse ||
TryValidateConditionalSchemaBranch(
filePath,
displayPath,
elseElement,
"else",
declaredProperties,
out diagnostic);
}
///
/// 验证单个 object-focused 条件分支的类型与父对象字段引用范围。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 当前条件分支 schema。
/// 条件关键字名称。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前条件分支是否有效。
private static bool TryValidateConditionalSchemaBranch(
string filePath,
string displayPath,
JsonElement schemaElement,
string keywordName,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
var branchPath = BuildConditionalSchemaPath(displayPath, keywordName);
if (schemaElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
branchPath,
$"The '{keywordName}' value must be an object-valued schema.");
return false;
}
if (!schemaElement.TryGetProperty("type", out var typeElement) ||
typeElement.ValueKind != JsonValueKind.String ||
!string.Equals(typeElement.GetString(), "object", StringComparison.Ordinal))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
branchPath,
$"The '{keywordName}' schema must declare an object-typed schema.");
return false;
}
return TryValidateObjectFocusedSchemaTargets(
filePath,
branchPath,
keywordName,
schemaElement,
declaredProperties,
out diagnostic);
}
///
/// 验证 object-focused 内联 schema 只引用父对象已声明的同级字段。
///
/// Schema 文件路径。
/// 当前内联 schema 路径。
/// 用于诊断文本的条目标签。
/// 当前内联 schema。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前内联 schema 是否有效。
private static bool TryValidateObjectFocusedSchemaTargets(
string filePath,
string displayPath,
string entryLabel,
JsonElement schemaElement,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (schemaElement.TryGetProperty("properties", out var propertiesElement))
{
if (propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema must declare 'properties' as an object-valued map.");
return false;
}
foreach (var property in propertiesElement.EnumerateObject())
{
if (declaredProperties.Contains(property.Name))
{
continue;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema declares property '{property.Name}', but that property is not declared in the parent object schema.");
return false;
}
}
if (!schemaElement.TryGetProperty("required", out var requiredElement))
{
return true;
}
if (requiredElement.ValueKind != JsonValueKind.Array)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema must declare 'required' as an array of parent property names.");
return false;
}
foreach (var requiredProperty in requiredElement.EnumerateArray())
{
if (requiredProperty.ValueKind != JsonValueKind.String)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema must declare 'required' entries as parent property-name strings.");
return false;
}
var requiredPropertyName = requiredProperty.GetString();
if (string.IsNullOrWhiteSpace(requiredPropertyName))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema cannot declare blank property names in 'required'.");
return false;
}
if (declaredProperties.Contains(requiredPropertyName!))
{
continue;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"The '{entryLabel}' schema requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.");
return false;
}
return true;
}
///
/// 验证单个 allOf 条目只约束父对象已声明的同级字段。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 当前 allOf 条目。
/// 从 0 开始的条目索引。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 allOf 条目是否只引用父对象已声明字段。
private static bool TryValidateAllOfEntryTargets(
string filePath,
string displayPath,
JsonElement allOfSchema,
int allOfIndex,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
var allOfEntryPath = BuildAllOfEntryPath(displayPath, allOfIndex);
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))
{
continue;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
allOfEntryPath,
$"Entry #{allOfIndex + 1} in 'allOf' declares property '{property.Name}', but that property is not declared in the parent object schema.");
return false;
}
}
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)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
allOfEntryPath,
$"Entry #{allOfIndex + 1} in 'allOf' must declare 'required' entries as parent property-name strings.");
return false;
}
var requiredPropertyName = requiredProperty.GetString();
if (string.IsNullOrWhiteSpace(requiredPropertyName))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
allOfEntryPath,
$"Entry #{allOfIndex + 1} in 'allOf' cannot declare blank property names in 'required'.");
return false;
}
var normalizedRequiredPropertyName = requiredPropertyName!;
if (declaredProperties.Contains(normalizedRequiredPropertyName))
{
continue;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
allOfEntryPath,
$"Entry #{allOfIndex + 1} in 'allOf' requires property '{normalizedRequiredPropertyName}', but that property is not declared in the parent object schema.");
return false;
}
return true;
}
///
/// 判断给定 format 名称是否属于当前共享支持子集。
///
/// schema 中声明的 format 名称。
/// 是否支持该格式。
private static bool IsSupportedStringFormat(string formatName)
{
return formatName switch
{
"date" => true,
"date-time" => true,
"duration" => true,
"email" => true,
"time" => true,
"uri" => true,
"uuid" => true,
_ => false
};
}
///
/// 使用 schema 关键字的大小写敏感语义比较类型名称。
///
/// 从 JSON schema 读取或推导出的类型名称。
/// 期望匹配的 schema 类型关键字。
/// 当两个类型名称按 schema 关键字语义完全一致时返回 。
private static bool IsSchemaType(string schemaType, string expectedType)
{
return string.Equals(schemaType, expectedType, StringComparison.Ordinal);
}
///
/// 判断类型是否支持 JSON schema 数值范围和倍数约束。
///
/// 从 JSON schema 读取或推导出的类型名称。
/// 当类型为 integer 或 number 时返回 。
private static bool IsNumericSchemaType(string schemaType)
{
return IsSchemaType(schemaType, "integer") || IsSchemaType(schemaType, "number");
}
///
/// 解析数组属性,支持标量数组与对象数组。
///
/// Schema 文件路径。
/// 属性 JSON 节点。
/// 属性是否必填。
/// 逻辑字段路径。
/// CLR 属性名。
/// 标题元数据。
/// 说明元数据。
/// 目标引用表名称。
/// 是否为索引查找。
/// 解析后的属性信息或诊断。
private static ParsedPropertyResult ParseArrayProperty(
string filePath,
JsonProperty property,
bool isRequired,
string displayPath,
string propertyName,
string? title,
string? description,
string? refTableName,
bool isIndexedLookup)
{
if (isIndexedLookup)
{
return ParsedPropertyResult.FromDiagnostic(
CreateInvalidLookupIndexDiagnostic(filePath, displayPath, LookupIndexTopLevelScalarOnlyMessage));
}
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;
if (!TryValidateStringFormatMetadata(filePath, $"{displayPath}[]", itemsElement, itemType,
out var formatDiagnostic))
{
return ParsedPropertyResult.FromDiagnostic(formatDiagnostic!);
}
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,
false,
new SchemaTypeSpec(
SchemaNodeKind.Array,
"array",
$"global::System.Collections.Generic.IReadOnlyList<{itemClrType}>",
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
$" = global::System.Array.Empty<{itemClrType}>();",
TryBuildEnumDocumentation(property.Value, "array") ??
TryBuildEnumDocumentation(itemsElement, itemType),
TryBuildConstraintDocumentation(property.Value, "array"),
refTableName,
null,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
itemType,
itemClrType,
null,
TryBuildEnumDocumentation(itemsElement, itemType),
TryBuildConstraintDocumentation(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