// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
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)
{
if (!TryReadSchemaText(file, cancellationToken, out var text, out var diagnostic))
{
return SchemaParseResult.FromDiagnostic(diagnostic!);
}
try
{
using var document = JsonDocument.Parse(text!.ToString());
return ParseSchemaRoot(file.Path, document.RootElement);
}
catch (JsonException exception)
{
return SchemaParseResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidSchemaJson,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
exception.Message));
}
}
///
/// Reads an AdditionalFiles schema text while converting all IO failures into generator diagnostics.
///
/// AdditionalFiles entry supplied by Roslyn.
/// Cancellation token forwarded by the incremental generator pipeline.
/// Read source text when the file can be loaded.
/// Diagnostic describing the read failure.
/// when schema text was read successfully; otherwise .
private static bool TryReadSchemaText(
AdditionalText file,
CancellationToken cancellationToken,
out SourceText? text,
out Diagnostic? diagnostic)
{
try
{
text = file.GetText(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception exception)
{
text = null;
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidSchemaJson,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
exception.Message);
return false;
}
if (text is null)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidSchemaJson,
CreateFileLocation(file.Path),
Path.GetFileName(file.Path),
"File content could not be read.");
return false;
}
diagnostic = null;
return true;
}
///
/// Parses a JSON schema root after JSON syntax has already been validated.
///
/// Schema file path used for diagnostics and generated metadata.
/// Parsed root JSON element.
/// Parsed schema model or the first schema diagnostic encountered.
private static SchemaParseResult ParseSchemaRoot(string filePath, JsonElement root)
{
if (!TryValidateSchemaRoot(filePath, root, out var diagnostic))
{
return SchemaParseResult.FromDiagnostic(diagnostic!);
}
var schemaBaseName = GetSchemaBaseName(filePath);
if (!TryBuildRootTypeIdentifiers(filePath, schemaBaseName, out var entityName, out var rootClassName, out diagnostic))
{
return SchemaParseResult.FromDiagnostic(diagnostic!);
}
var rootObject = ParseObjectSpec(filePath, root, "", rootClassName, isRoot: true);
if (rootObject.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(rootObject.Diagnostic);
}
var schemaObject = rootObject.Object!;
var idProperty = FindValidIdProperty(filePath, schemaObject, out diagnostic);
if (diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(diagnostic);
}
var configRelativePath = ResolveConfigRelativePath(filePath, root, schemaBaseName);
if (configRelativePath.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(configRelativePath.Diagnostic);
}
return SchemaParseResult.FromSchema(CreateSchemaFileSpec(
filePath,
root,
entityName,
schemaObject,
idProperty!,
schemaBaseName,
configRelativePath.Path!));
}
///
/// Validates schema-level contracts that must hold before object and property models are built.
///
/// Schema file path used for diagnostics.
/// Root JSON schema element.
/// First validation diagnostic, if any.
/// when the root can be parsed as a config schema.
private static bool TryValidateSchemaRoot(string filePath, JsonElement root, out Diagnostic? diagnostic)
{
if (!root.TryGetProperty("type", out var rootTypeElement) ||
rootTypeElement.ValueKind != JsonValueKind.String ||
!IsSchemaType(rootTypeElement.GetString() ?? string.Empty, "object"))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.RootObjectSchemaRequired,
CreateFileLocation(filePath),
Path.GetFileName(filePath));
return false;
}
return TryValidateStringFormatMetadataRecursively(filePath, "", root, out diagnostic) &&
TryValidateUnsupportedCombinatorKeywordsRecursively(filePath, "", root, out diagnostic) &&
TryValidateUnsupportedOpenObjectKeywordsRecursively(filePath, "", root, out diagnostic) &&
TryValidateDependentRequiredMetadataRecursively(filePath, "", root, out diagnostic) &&
TryValidateDependentSchemasMetadataRecursively(filePath, "", root, out diagnostic) &&
TryValidateAllOfMetadataRecursively(filePath, "", root, out diagnostic) &&
TryValidateConditionalSchemasMetadataRecursively(filePath, "", root, out diagnostic);
}
///
/// Finds and validates the required root id property that becomes the generated table key.
///
/// Schema file path used for diagnostics.
/// Parsed root object model.
/// Diagnostic explaining why the key is invalid.
/// The key property when it satisfies the generator contract; otherwise null.
private static SchemaPropertySpec? FindValidIdProperty(
string filePath,
SchemaObjectSpec schemaObject,
out Diagnostic? diagnostic)
{
var idProperty = schemaObject.Properties.FirstOrDefault(static property =>
string.Equals(property.SchemaName, "id", StringComparison.OrdinalIgnoreCase));
if (idProperty is null || !idProperty.IsRequired)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.IdPropertyRequired,
CreateFileLocation(filePath),
Path.GetFileName(filePath));
return null;
}
if (!IsSchemaType(idProperty.TypeSpec.SchemaType, "integer") &&
!IsSchemaType(idProperty.TypeSpec.SchemaType, "string"))
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedKeyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
idProperty.TypeSpec.SchemaType);
return null;
}
diagnostic = null;
return idProperty;
}
///
/// Creates the generator-level schema model from validated root metadata and the parsed object tree.
///
/// Schema file path.
/// Root JSON element used for optional title and description metadata.
/// Generated entity name derived from the schema file name.
/// Parsed root object model.
/// Validated required key property.
/// Normalized schema base name.
/// Resolved config-relative directory path.
/// Completed schema file model used by source emission.
private static SchemaFileSpec CreateSchemaFileSpec(
string filePath,
JsonElement root,
string entityName,
SchemaObjectSpec schemaObject,
SchemaPropertySpec idProperty,
string schemaBaseName,
string configRelativePath)
{
return new SchemaFileSpec(
Path.GetFileName(filePath),
entityName,
schemaObject.ClassName,
$"{entityName}Table",
GeneratedNamespace,
idProperty.TypeSpec.ClrType.TrimEnd('?'),
idProperty.PropertyName,
schemaBaseName,
configRelativePath,
GetSchemaRelativePath(filePath),
TryGetMetadataString(root, "title"),
TryGetMetadataString(root, "description"),
schemaObject);
}
///
/// 解析对象 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();
if (!TryParseObjectProperties(
filePath,
displayPath,
isRoot,
propertiesElement,
requiredProperties,
properties,
out var propertyDiagnostic))
{
return ParsedObjectResult.FromDiagnostic(propertyDiagnostic!);
}
return ParsedObjectResult.FromObject(new SchemaObjectSpec(
displayPath,
className,
TryGetMetadataString(element, "title"),
TryGetMetadataString(element, "description"),
TryBuildConstraintDocumentation(element, "object"),
properties));
}
///
/// 解析对象 schema 的直接子属性,并在进入代码发射前阻止归一化后的属性名冲突落入生成输出。
///
/// Schema 文件路径。
/// 当前对象的逻辑路径。
/// 当前对象是否为根对象。
/// 对象的 properties JSON 节点。
/// 当前对象声明的必填字段集合。
/// 成功时返回的已解析属性列表。
/// 解析失败时返回的首个诊断。
/// 当所有属性都可安全生成时返回 。
private static bool TryParseObjectProperties(
string filePath,
string displayPath,
bool isRoot,
JsonElement propertiesElement,
ISet requiredProperties,
ICollection properties,
out Diagnostic? diagnostic)
{
var schemaKeyByGeneratedPropertyName = new Dictionary(StringComparer.Ordinal);
foreach (var property in propertiesElement.EnumerateObject())
{
var propertyDisplayPath = CombinePath(displayPath, property.Name);
var parsedProperty = ParseProperty(
filePath,
property,
requiredProperties.Contains(property.Name),
propertyDisplayPath,
isDirectChildOfRoot: isRoot);
if (parsedProperty.Diagnostic is not null)
{
diagnostic = parsedProperty.Diagnostic;
return false;
}
if (!TryRegisterGeneratedPropertyName(
filePath,
propertyDisplayPath,
property.Name,
parsedProperty.Property!.PropertyName,
schemaKeyByGeneratedPropertyName,
out diagnostic))
{
return false;
}
properties.Add(parsedProperty.Property!);
}
diagnostic = null;
return true;
}
///
/// 解析单个 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;
if (!TryCreatePropertyParseContext(
filePath,
property,
isRequired,
displayPath,
isDirectChildOfRoot,
schemaType,
out var context,
out var diagnostic))
{
return ParsedPropertyResult.FromDiagnostic(diagnostic!);
}
return CreatePropertyBySchemaType(filePath, property, schemaType, context!);
}
///
/// Collects shared property metadata and validates cross-cutting metadata before type-specific parsing begins.
///
/// Schema file path used for diagnostics.
/// Schema property JSON node.
/// Whether the property is required.
/// Logical schema path.
/// Whether the property is a direct child of the schema root.
/// Validated schema type keyword.
/// Shared parsing metadata when validation succeeds.
/// First metadata validation diagnostic.
/// when the property can continue to type-specific parsing.
private static bool TryCreatePropertyParseContext(
string filePath,
JsonProperty property,
bool isRequired,
string displayPath,
bool isDirectChildOfRoot,
string schemaType,
out PropertyParseContext? context,
out Diagnostic? diagnostic)
{
context = null;
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))
{
diagnostic = formatDiagnostic;
return false;
}
var indexedLookupMetadata = TryGetMetadataBoolean(property.Value, LookupIndexMetadataKey);
if (indexedLookupMetadata.Diagnostic is not null)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidLookupIndexMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
LookupIndexMetadataKey,
indexedLookupMetadata.Diagnostic!);
return false;
}
var isIndexedLookup = indexedLookupMetadata.Value ?? false;
if (!TryBuildPropertyIdentifier(filePath, displayPath, property.Name, out var propertyName, out diagnostic))
{
return false;
}
if (isIndexedLookup &&
!TryValidateIndexedLookupEligibility(
filePath,
property.Name,
displayPath,
isDirectChildOfRoot,
isRequired,
refTableName,
out diagnostic))
{
return false;
}
context = new PropertyParseContext(
displayPath,
propertyName,
isRequired,
title,
description,
refTableName,
isIndexedLookup);
diagnostic = null;
return true;
}
///
/// Dispatches schema property parsing by JSON schema type after shared validation and metadata extraction.
///
/// Schema file path used for diagnostics.
/// Schema property JSON node.
/// Validated schema type keyword.
/// Shared parsing metadata collected from the property node.
/// Parsed property model or an unsupported-type diagnostic.
private static ParsedPropertyResult CreatePropertyBySchemaType(
string filePath,
JsonProperty property,
string schemaType,
PropertyParseContext context)
{
return schemaType switch
{
"integer" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "int" : "int?"),
"number" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "double" : "double?"),
"boolean" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "bool" : "bool?"),
"string" => CreateScalarPropertyResult(property, context, schemaType, context.IsRequired ? "string" : "string?"),
"object" => CreateObjectPropertyResult(filePath, property, context),
"array" => ParseArrayProperty(filePath, property, context),
_ => ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
context.DisplayPath,
schemaType)),
};
}
///
/// Builds a parsed scalar property model while preserving schema metadata used by generated XML docs.
///
/// Schema property JSON node.
/// Shared parsing metadata collected from the property node.
/// Scalar schema type.
/// Generated CLR property type.
/// Parsed property result for a scalar schema node.
private static ParsedPropertyResult CreateScalarPropertyResult(
JsonProperty property,
PropertyParseContext context,
string schemaType,
string clrType)
{
var initializer = TryBuildScalarInitializer(property.Value, schemaType);
if (IsSchemaType(schemaType, "string") && initializer is null && context.IsRequired)
{
initializer = " = string.Empty;";
}
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
context.DisplayPath,
context.PropertyName,
context.IsRequired,
context.Title,
context.Description,
context.IsIndexedLookup,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
schemaType,
clrType,
initializer,
TryBuildEnumDocumentation(property.Value, schemaType),
TryBuildConstraintDocumentation(property.Value, schemaType),
context.RefTableName,
null,
null)));
}
///
/// Builds a parsed object property model and reports unsupported object reference/index combinations.
///
/// Schema file path used for diagnostics.
/// Schema property JSON node.
/// Shared parsing metadata collected from the property node.
/// Parsed property result for an object schema node.
private static ParsedPropertyResult CreateObjectPropertyResult(
string filePath,
JsonProperty property,
PropertyParseContext context)
{
if (context.IsIndexedLookup)
{
return ParsedPropertyResult.FromDiagnostic(
CreateInvalidLookupIndexDiagnostic(filePath, context.DisplayPath, LookupIndexTopLevelScalarOnlyMessage));
}
if (!string.IsNullOrWhiteSpace(context.RefTableName))
{
return ParsedPropertyResult.FromDiagnostic(
Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedPropertyType,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
context.DisplayPath,
"object-ref"));
}
var objectResult = ParseObjectSpec(filePath, property.Value, context.DisplayPath, $"{context.PropertyName}Config");
if (objectResult.Diagnostic is not null)
{
return ParsedPropertyResult.FromDiagnostic(objectResult.Diagnostic);
}
var objectSpec = objectResult.Object!;
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
context.DisplayPath,
context.PropertyName,
context.IsRequired,
context.Title,
context.Description,
false,
new SchemaTypeSpec(
SchemaNodeKind.Object,
"object",
context.IsRequired ? objectSpec.ClassName : $"{objectSpec.ClassName}?",
context.IsRequired ? " = new();" : null,
TryBuildEnumDocumentation(property.Value, "object"),
null,
null,
objectSpec,
null)));
}
///
/// 验证字段是否满足生成只读精确匹配索引的前提。
///
/// 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);
}
///
/// 递归拒绝当前共享子集尚未支持的组合关键字。
/// 这里显式拦截 oneOf / anyOf,避免生成器静默接受会改变生成类型形状的 schema。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树是否未声明不支持的组合关键字。
private static bool TryValidateUnsupportedCombinatorKeywordsRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, _) =>
{
return TryValidateUnsupportedCombinatorKeywords(
currentFilePath,
currentDisplayPath,
currentElement,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 递归拒绝当前共享子集尚未支持的开放对象关键字形状。
/// 当前对象字段集默认是闭合的,因此这里只接受显式重复该语义的
/// additionalProperties: false,并继续拒绝
/// patternProperties、propertyNames 与
/// unevaluatedProperties 这类会重新打开对象形状的关键字。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 当前节点树是否未声明不支持的开放对象关键字形状。
private static bool TryValidateUnsupportedOpenObjectKeywordsRecursively(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
return TryTraverseSchemaRecursively(
filePath,
displayPath,
element,
static (currentFilePath, currentDisplayPath, currentElement, _) =>
{
return TryValidateUnsupportedOpenObjectKeywords(
currentFilePath,
currentDisplayPath,
currentElement,
out var currentDiagnostic)
? (true, (Diagnostic?)null)
: (false, currentDiagnostic);
},
out diagnostic);
}
///
/// 验证当前节点是否声明了会改变生成类型形状的未支持组合关键字。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 未声明不支持关键字时返回 。
private static bool TryValidateUnsupportedCombinatorKeywords(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedCombinatorKeyword,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
keywordName,
"The current config schema subset does not support combinators that can change generated type shape.");
return false;
}
///
/// 验证当前节点是否声明了当前共享子集尚未支持的开放对象关键字形状。
///
/// Schema 文件路径。
/// 逻辑字段路径。
/// 当前 schema 节点。
/// 失败时返回的诊断。
/// 未声明不支持关键字时返回 。
private static bool TryValidateUnsupportedOpenObjectKeywords(
string filePath,
string displayPath,
JsonElement element,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.UnsupportedOpenObjectKeyword,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
keywordName,
"The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.");
return false;
}
///
/// 返回当前节点声明的首个未支持组合关键字。
///
/// 当前 schema 节点。
/// 命中的关键字名称;未声明时返回空。
private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
{
return element.TryGetProperty("oneOf", out _) ? "oneOf" :
element.TryGetProperty("anyOf", out _) ? "anyOf" :
null;
}
///
/// 返回当前节点声明的首个未支持开放对象关键字。
///
/// 当前 schema 节点。
/// 命中的关键字名称;未声明时返回空。
private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
{
if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind != JsonValueKind.False)
{
return "additionalProperties";
}
return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
null;
}
///
/// 以统一顺序递归遍历 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 (IsSchemaType(schemaType, "object") &&
!TryTraverseObjectChildren(filePath, displayPath, element, nodeValidator, out diagnostic))
{
return false;
}
if (!TryTraverseSingleSchemaProperty(filePath, $"{displayPath}[not]", element, "not", nodeValidator, out diagnostic))
{
return false;
}
return !IsSchemaType(schemaType, "array") ||
TryTraverseArrayChildren(filePath, displayPath, element, nodeValidator, out diagnostic);
}
///
/// Traverses every object-only child schema keyword in the same order as the parser observes properties.
///
/// Schema file path.
/// Logical object path.
/// Object schema node.
/// Current validation callback.
/// First child validation diagnostic.
/// when all object child schemas pass validation.
private static bool TryTraverseObjectChildren(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
if (!TryTraverseObjectProperties(filePath, displayPath, element, nodeValidator, out diagnostic))
{
return false;
}
if (!TryTraverseDependentSchemas(filePath, displayPath, element, nodeValidator, out diagnostic))
{
return false;
}
return TryTraverseAllOfEntries(filePath, displayPath, element, nodeValidator, out diagnostic) &&
TryTraverseObjectSchemaBranches(filePath, displayPath, element, nodeValidator, out diagnostic);
}
///
/// Traverses nested object properties in declaration order.
///
/// Schema file path.
/// Logical object path.
/// Object schema node.
/// Current validation callback.
/// First property validation diagnostic.
/// when every property subtree passes validation.
private static bool TryTraverseObjectProperties(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
return true;
}
foreach (var property in propertiesElement.EnumerateObject())
{
if (!TryTraverseSchemaRecursively(
filePath,
CombinePath(displayPath, property.Name),
property.Value,
nodeValidator,
out diagnostic))
{
return false;
}
}
return true;
}
///
/// Traverses object dependentSchemas entries that are themselves schema objects.
///
/// Schema file path.
/// Logical object path.
/// Object schema node.
/// Current validation callback.
/// First dependent schema validation diagnostic.
/// when every dependent schema subtree passes validation.
private static bool TryTraverseDependentSchemas(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("dependentSchemas", out var dependentSchemasElement) ||
dependentSchemasElement.ValueKind != JsonValueKind.Object)
{
return true;
}
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;
}
}
return true;
}
///
/// Traverses object allOf entries while preserving their numeric path segments.
///
/// Schema file path.
/// Logical object path.
/// Object schema node.
/// Current validation callback.
/// First allOf validation diagnostic.
/// when every object allOf entry passes validation.
private static bool TryTraverseAllOfEntries(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!element.TryGetProperty("allOf", out var allOfElement) ||
allOfElement.ValueKind != JsonValueKind.Array)
{
return true;
}
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++;
}
return true;
}
///
/// Traverses the object-focused conditional schema branches.
///
/// Schema file path.
/// Logical object path.
/// Object schema node.
/// Current validation callback.
/// First branch validation diagnostic.
/// when all present conditional branches pass validation.
private static bool TryTraverseObjectSchemaBranches(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
return TryTraverseSingleSchemaProperty(
filePath,
BuildConditionalSchemaPath(displayPath, "if"),
element,
"if",
nodeValidator,
out diagnostic) &&
TryTraverseSingleSchemaProperty(
filePath,
BuildConditionalSchemaPath(displayPath, "then"),
element,
"then",
nodeValidator,
out diagnostic) &&
TryTraverseSingleSchemaProperty(
filePath,
BuildConditionalSchemaPath(displayPath, "else"),
element,
"else",
nodeValidator,
out diagnostic);
}
///
/// Traverses a single child schema property when it is present and object-shaped.
///
/// Schema file path.
/// Logical path assigned to the child schema.
/// Parent schema node.
/// Child schema keyword.
/// Current validation callback.
/// Child validation diagnostic.
/// when the child is absent or passes validation.
private static bool TryTraverseSingleSchemaProperty(
string filePath,
string childDisplayPath,
JsonElement element,
string propertyName,
Func nodeValidator,
out Diagnostic? diagnostic)
{
diagnostic = null;
return !element.TryGetProperty(propertyName, out var childElement) ||
childElement.ValueKind != JsonValueKind.Object ||
TryTraverseSchemaRecursively(filePath, childDisplayPath, childElement, nodeValidator, out diagnostic);
}
///
/// Traverses array items and contains child schemas.
///
/// Schema file path.
/// Logical array path.
/// Array schema node.
/// Current validation callback.
/// First array child validation diagnostic.
/// when all array child schemas pass validation.
private static bool TryTraverseArrayChildren(
string filePath,
string displayPath,
JsonElement element,
Func nodeValidator,
out Diagnostic? diagnostic)
{
return TryTraverseSingleSchemaProperty(filePath, $"{displayPath}[]", element, "items", nodeValidator, out diagnostic) &&
TryTraverseSingleSchemaProperty(
filePath,
$"{displayPath}[contains]",
element,
"contains",
nodeValidator,
out diagnostic);
}
///
/// 为对象级 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 (!TryGetDeclaredProperties(
filePath,
displayPath,
element,
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
"dependentRequired",
out var declaredProperties,
out diagnostic))
{
return false;
}
foreach (var dependency in dependentRequiredElement.EnumerateObject())
{
if (!TryValidateDependentRequiredEntry(
filePath,
displayPath,
dependency,
declaredProperties,
out diagnostic))
{
return false;
}
}
return true;
}
///
/// 验证单个 dependentRequired 触发项的声明形状。
/// 该 helper 先锁定 trigger 字段本身是否属于当前对象,再把每个 target 交给更细粒度的 sibling 校验,
/// 让诊断能够明确区分“触发字段不存在”和“依赖目标非法”两类失败语义。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 当前 dependentRequired 触发项。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 dependentRequired 触发项是否有效。
private static bool TryValidateDependentRequiredEntry(
string filePath,
string displayPath,
JsonProperty dependency,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
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 (!TryValidateDependentRequiredTarget(
filePath,
displayPath,
dependency.Name,
dependencyTarget,
declaredProperties,
out diagnostic))
{
return false;
}
}
return true;
}
///
/// 验证单个 dependentRequired target 是否为已声明的 sibling 字段名。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 触发依赖的字段名。
/// 当前 target 元素。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 dependentRequired target 是否有效。
private static bool TryValidateDependentRequiredTarget(
string filePath,
string displayPath,
string dependencyName,
JsonElement dependencyTarget,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (dependencyTarget.ValueKind != JsonValueKind.String)
{
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Property '{dependencyName}' 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 '{dependencyName}' cannot declare blank 'dependentRequired' entries.");
return false;
}
var normalizedDependencyTargetName = dependencyTargetName!;
if (declaredProperties.Contains(normalizedDependencyTargetName))
{
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidDependentRequiredMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Dependent target '{normalizedDependencyTargetName}' is not declared in the same object schema.");
return false;
}
///
/// 验证当前 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 (!TryGetDeclaredProperties(
filePath,
displayPath,
element,
ConfigSchemaDiagnostics.InvalidDependentSchemasMetadata,
"dependentSchemas",
out var declaredProperties,
out diagnostic))
{
return false;
}
foreach (var dependency in dependentSchemasElement.EnumerateObject())
{
if (!TryValidateDependentSchemaEntry(
filePath,
displayPath,
dependency,
declaredProperties,
out diagnostic))
{
return false;
}
}
return true;
}
///
/// 验证单个 dependentSchemas 触发项是否保持为当前运行时支持的 object 子 schema 形状。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 当前 dependentSchemas 触发项。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 dependentSchemas 触发项是否有效。
private static bool TryValidateDependentSchemaEntry(
string filePath,
string displayPath,
JsonProperty dependency,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
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 (!TryGetDeclaredProperties(
filePath,
displayPath,
element,
ConfigSchemaDiagnostics.InvalidAllOfMetadata,
"allOf",
out var declaredProperties,
out diagnostic))
{
return false;
}
var allOfIndex = 0;
foreach (var allOfSchema in allOfElement.EnumerateArray())
{
if (!TryValidateAllOfEntryShape(
filePath,
displayPath,
allOfSchema,
allOfIndex,
out diagnostic))
{
return false;
}
if (!TryValidateAllOfEntryTargets(
filePath,
displayPath,
allOfSchema,
allOfIndex,
declaredProperties,
out diagnostic))
{
return false;
}
allOfIndex++;
}
return true;
}
///
/// 验证单个 allOf 条目是否维持 object-valued、object-typed 的 focused constraint 形状。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 当前 allOf 条目。
/// 从 0 开始的条目索引。
/// 失败时返回的诊断。
/// 当前 allOf 条目形状是否有效。
private static bool TryValidateAllOfEntryShape(
string filePath,
string displayPath,
JsonElement allOfSchema,
int allOfIndex,
out Diagnostic? diagnostic)
{
diagnostic = null;
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;
}
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 (!TryValidateConditionalSchemaPresence(
filePath,
displayPath,
hasIf,
hasThen,
hasElse,
out diagnostic))
{
return false;
}
if (!hasIf)
{
return true;
}
if (!TryGetDeclaredProperties(
filePath,
displayPath,
element,
ConfigSchemaDiagnostics.InvalidConditionalSchemaMetadata,
"if/then/else",
out var declaredProperties,
out diagnostic))
{
return false;
}
return TryValidateConditionalSchemaBranches(
filePath,
displayPath,
ifElement,
hasThen,
thenElement,
hasElse,
elseElement,
declaredProperties,
out diagnostic);
}
///
/// 验证 object-focused 条件分支集合。
/// if 分支始终必检,then / else 仅在声明时校验,
/// 以保持生成器对分支缺失与分支内容错误的诊断顺序稳定。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// if 分支 schema。
/// 是否声明 then。
/// then 分支 schema。
/// 是否声明 else。
/// else 分支 schema。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前条件分支集合是否有效。
private static bool TryValidateConditionalSchemaBranches(
string filePath,
string displayPath,
JsonElement ifElement,
bool hasThen,
JsonElement thenElement,
bool hasElse,
JsonElement elseElement,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
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 if / then / else 的存在性组合是否合法。
///
/// Schema 文件路径。
/// 父对象逻辑路径。
/// 是否声明 if。
/// 是否声明 then。
/// 是否声明 else。
/// 失败时返回的诊断。
/// 当前条件关键字组合是否有效。
private static bool TryValidateConditionalSchemaPresence(
string filePath,
string displayPath,
bool hasIf,
bool hasThen,
bool hasElse,
out Diagnostic? diagnostic)
{
diagnostic = null;
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)
{
return true;
}
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;
}
private static bool TryGetDeclaredProperties(
string filePath,
string displayPath,
JsonElement element,
DiagnosticDescriptor descriptor,
string keywordName,
out HashSet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
declaredProperties = new HashSet(StringComparer.Ordinal);
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
diagnostic = Diagnostic.Create(
descriptor,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
$"Object schemas using '{keywordName}' must also declare an object-valued 'properties' map.");
return false;
}
declaredProperties = new HashSet(
propertiesElement
.EnumerateObject()
.Select(static property => property.Name),
StringComparer.Ordinal);
return true;
}
///
/// 验证单个 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 (!TryValidateObjectFocusedSchemaProperties(
filePath,
displayPath,
entryLabel,
schemaElement,
declaredProperties,
out diagnostic))
{
return false;
}
return TryValidateObjectFocusedSchemaRequiredProperties(
filePath,
displayPath,
entryLabel,
schemaElement,
declaredProperties,
out diagnostic);
}
///
/// 验证 object-focused 条件 schema 的 properties 只引用父对象已声明字段。
///
/// Schema 文件路径。
/// 当前分支逻辑路径。
/// 分支标签。
/// 当前分支 schema。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前分支 properties 是否有效。
private static bool TryValidateObjectFocusedSchemaProperties(
string filePath,
string displayPath,
string entryLabel,
JsonElement schemaElement,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!schemaElement.TryGetProperty("properties", out var propertiesElement))
{
return true;
}
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;
}
return true;
}
///
/// 验证 object-focused 条件 schema 的 required 约束只引用父对象已声明字段。
///
/// Schema 文件路径。
/// 当前分支逻辑路径。
/// 分支标签。
/// 当前分支 schema。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前分支 required 是否有效。
private static bool TryValidateObjectFocusedSchemaRequiredProperties(
string filePath,
string displayPath,
string entryLabel,
JsonElement schemaElement,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
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 (!TryValidateAllOfEntryProperties(
filePath,
allOfEntryPath,
allOfSchema,
allOfIndex,
declaredProperties,
out diagnostic))
{
return false;
}
return TryValidateAllOfEntryRequiredProperties(
filePath,
allOfEntryPath,
allOfSchema,
allOfIndex,
declaredProperties,
out diagnostic);
}
///
/// 验证单个 allOf 条目的 properties 映射不会引入父对象未声明字段。
///
/// Schema 文件路径。
/// 当前 allOf 条目逻辑路径。
/// 当前 allOf 条目。
/// 从 0 开始的条目索引。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 allOf 条目的 properties 映射是否有效。
private static bool TryValidateAllOfEntryProperties(
string filePath,
string allOfEntryPath,
JsonElement allOfSchema,
int allOfIndex,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
if (!allOfSchema.TryGetProperty("properties", out var allOfPropertiesElement))
{
return true;
}
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;
}
return true;
}
///
/// 验证单个 allOf 条目的 required 约束不会引用父对象未声明字段。
///
/// Schema 文件路径。
/// 当前 allOf 条目逻辑路径。
/// 当前 allOf 条目。
/// 从 0 开始的条目索引。
/// 父对象已声明属性集合。
/// 失败时返回的诊断。
/// 当前 allOf 条目的 required 约束是否有效。
private static bool TryValidateAllOfEntryRequiredProperties(
string filePath,
string allOfEntryPath,
JsonElement allOfSchema,
int allOfIndex,
ISet declaredProperties,
out Diagnostic? diagnostic)
{
diagnostic = null;
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 节点。
/// 属性解析共享上下文。
/// 解析后的属性信息或诊断。
private static ParsedPropertyResult ParseArrayProperty(
string filePath,
JsonProperty property,
PropertyParseContext context)
{
if (context.IsIndexedLookup)
{
return ParsedPropertyResult.FromDiagnostic(
CreateInvalidLookupIndexDiagnostic(filePath, context.DisplayPath, LookupIndexTopLevelScalarOnlyMessage));
}
if (!TryGetArrayItemSchema(filePath, property, context.DisplayPath, out var itemsElement, out var itemType, out var diagnostic))
{
return ParsedPropertyResult.FromDiagnostic(diagnostic!);
}
if (!TryValidateStringFormatMetadata(filePath, $"{context.DisplayPath}[]", itemsElement, itemType,
out var formatDiagnostic))
{
return ParsedPropertyResult.FromDiagnostic(formatDiagnostic!);
}
return itemType switch
{
"integer" or "number" or "boolean" or "string" =>
CreateScalarArrayPropertyResult(property, context, itemsElement, itemType),
"object" => CreateObjectArrayPropertyResult(filePath, property, context, itemsElement),
_ => ParsedPropertyResult.FromDiagnostic(CreateUnsupportedArrayItemDiagnostic(
filePath,
context.DisplayPath,
$"array<{itemType}>")),
};
}
///
/// Reads the items.type declaration required by the supported array schema subset.
///
/// Schema file path used for diagnostics.
/// Array property JSON node.
/// Logical schema path.
/// Array item schema node when present.
/// Array item schema type when present.
/// Diagnostic explaining an invalid declaration.
/// when the array item schema is supported by the parser.
private static bool TryGetArrayItemSchema(
string filePath,
JsonProperty property,
string displayPath,
out JsonElement itemsElement,
out string itemType,
out Diagnostic? diagnostic)
{
if (property.Value.TryGetProperty("items", out itemsElement) &&
itemsElement.ValueKind == JsonValueKind.Object &&
itemsElement.TryGetProperty("type", out var itemTypeElement) &&
itemTypeElement.ValueKind == JsonValueKind.String)
{
itemType = itemTypeElement.GetString() ?? string.Empty;
diagnostic = null;
return true;
}
itemType = string.Empty;
var itemDescription = "array";
if (itemsElement.ValueKind == JsonValueKind.Object &&
itemsElement.TryGetProperty("type", out var unsupportedTypeElement) &&
unsupportedTypeElement.ValueKind == JsonValueKind.String)
{
itemDescription = $"array<{unsupportedTypeElement.GetString() ?? string.Empty}>";
}
diagnostic = CreateUnsupportedArrayItemDiagnostic(filePath, displayPath, itemDescription);
return false;
}
///
/// Builds an array property whose items are scalar values.
///
/// Array property JSON node.
/// Property parsing context.
/// Array item schema node.
/// Array item scalar schema type.
/// Parsed array property model.
private static ParsedPropertyResult CreateScalarArrayPropertyResult(
JsonProperty property,
PropertyParseContext context,
JsonElement itemsElement,
string itemType)
{
var itemClrType = itemType switch
{
"integer" => "int",
"number" => "double",
"boolean" => "bool",
_ => "string"
};
return ParsedPropertyResult.FromProperty(new SchemaPropertySpec(
property.Name,
context.DisplayPath,
context.PropertyName,
context.IsRequired,
context.Title,
context.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"),
context.RefTableName,
null,
new SchemaTypeSpec(
SchemaNodeKind.Scalar,
itemType,
itemClrType,
null,
TryBuildEnumDocumentation(itemsElement, itemType),
TryBuildConstraintDocumentation(itemsElement, itemType),
context.RefTableName,
null,
null))));
}
///
/// Builds an array property whose items are nested object values.
///
/// Schema file path used for diagnostics.
/// Array property JSON node.
/// Property parsing context.
/// Array item schema node.
/// Parsed array property model or an object-array diagnostic.
private static ParsedPropertyResult CreateObjectArrayPropertyResult(
string filePath,
JsonProperty property,
PropertyParseContext context,
JsonElement itemsElement)
{
if (!string.IsNullOrWhiteSpace(context.RefTableName))
{
return ParsedPropertyResult.FromDiagnostic(
CreateUnsupportedArrayItemDiagnostic(filePath, context.DisplayPath, "array