// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Numerics;
using System.Text.RegularExpressions;
using GFramework.Game.Abstractions.Config;
namespace GFramework.Game.Config;
///
/// 提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。
/// 该校验器与当前配置生成器、VS Code 工具支持的 schema 子集保持一致,
/// 并通过递归遍历方式覆盖嵌套对象、对象数组、标量数组与深层 enum / 引用约束。
/// 当前共享子集额外支持 multipleOf、uniqueItems、
/// contains / minContains / maxContains、
/// minProperties、maxProperties、dependentRequired、
/// dependentSchemas、allOf、object-focused if / then / else
/// 与稳定字符串 format 子集,让数值步进、数组去重、数组匹配计数、
/// 对象属性数量、对象内字段依赖、条件对象子 schema、对象组合约束与条件分支约束
/// 在运行时与生成器 / 工具侧保持一致。
///
internal static partial class YamlConfigSchemaValidator
{
// The runtime intentionally uses the same culture-invariant regex semantics as the
// JS tooling so grouping and backreferences behave consistently across environments.
private const RegexOptions SupportedPatternRegexOptions = RegexOptions.CultureInvariant;
private const string SupportedStringFormatNames = "'date', 'date-time', 'duration', 'email', 'time', 'uri', 'uuid'";
private static readonly TimeSpan SupportedFormatRegexTimeout = TimeSpan.FromSeconds(1);
private static readonly Regex ExactDecimalPattern = new(
@"^(?[+-]?)(?:(?\d+)(?:\.(?\d*))?|\.(?\d+))(?:[eE](?[+-]?\d+))?$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedEmailFormatRegex = new(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedDateFormatRegex = new(
@"^(?\d{4})-(?\d{2})-(?\d{2})$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedDateTimeFormatRegex = new(
@"^(?\d{4})-(?\d{2})-(?\d{2})T(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedDurationFormatRegex = new(
@"^P(?:(?\d+)D)?(?:T(?:(?\d+)H)?(?:(?\d+)M)?(?:(?\d+(?:\.\d+)?)S)?)?$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedTimeFormatRegex = new(
@"^(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
private static readonly Regex SupportedUriSchemeRegex = new(
@"^[A-Za-z][A-Za-z0-9+\.-]*:",
RegexOptions.CultureInvariant | RegexOptions.Compiled,
SupportedFormatRegexTimeout);
///
/// 从磁盘加载并解析一个 JSON Schema 文件。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 取消令牌。
/// 解析后的 schema 模型。
/// 当 为空时抛出。
/// 当 为空时抛出。
/// 当 schema 文件不存在或内容非法时抛出。
internal static async Task LoadAsync(
string tableName,
string schemaPath,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(tableName))
{
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
}
if (string.IsNullOrWhiteSpace(schemaPath))
{
throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
}
if (!File.Exists(schemaPath))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaFileNotFound,
tableName,
$"Schema file '{schemaPath}' was not found.",
schemaPath: schemaPath);
}
string schemaText;
try
{
schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaReadFailed,
tableName,
$"Failed to read schema file '{schemaPath}'.",
schemaPath: schemaPath,
innerException: exception);
}
return ParseLoadedSchema(tableName, schemaPath, schemaText);
}
///
/// 从磁盘同步加载并解析一个 JSON Schema 文件。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 解析后的 schema 模型。
/// 当 为空时抛出。
/// 当 为空时抛出。
/// 当 schema 文件不存在或内容非法时抛出。
internal static YamlConfigSchema Load(
string tableName,
string schemaPath)
{
if (string.IsNullOrWhiteSpace(tableName))
{
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
}
if (string.IsNullOrWhiteSpace(schemaPath))
{
throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
}
if (!File.Exists(schemaPath))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaFileNotFound,
tableName,
$"Schema file '{schemaPath}' was not found.",
schemaPath: schemaPath);
}
string schemaText;
try
{
schemaText = File.ReadAllText(schemaPath);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaReadFailed,
tableName,
$"Failed to read schema file '{schemaPath}'.",
schemaPath: schemaPath,
innerException: exception);
}
return ParseLoadedSchema(tableName, schemaPath, schemaText);
}
///
/// 使用已解析的 schema 校验 YAML 文本。
///
/// 所属配置表名称。
/// 已解析的 schema 模型。
/// YAML 文件路径,仅用于诊断信息。
/// YAML 文本内容。
/// 当参数为空时抛出。
/// 当 YAML 内容与 schema 不匹配时抛出。
internal static void Validate(
string tableName,
YamlConfigSchema schema,
string yamlPath,
string yamlText)
{
ValidateCore(tableName, schema, yamlPath, yamlText, references: null);
}
///
/// 使用已解析的 schema 校验 YAML 文本,并提取声明过的跨表引用。
/// 该方法让结构校验与引用采集共享同一份 YAML 解析结果,避免加载器重复解析同一文件。
///
/// 所属配置表名称。
/// 已解析的 schema 模型。
/// YAML 文件路径,仅用于诊断信息。
/// YAML 文本内容。
/// 当前 YAML 文件中声明的跨表引用集合。
/// 当参数为空时抛出。
/// 当 YAML 内容与 schema 不匹配时抛出。
internal static IReadOnlyList ValidateAndCollectReferences(
string tableName,
YamlConfigSchema schema,
string yamlPath,
string yamlText)
{
var references = new List();
ValidateCore(tableName, schema, yamlPath, yamlText, references);
return references;
}
///
/// 执行共享的 YAML 结构校验流程,并按需收集跨表引用。
/// 这样 可以复用同一条校验链路,同时避免为“不关心引用结果”的调用方分配临时列表。
///
/// 所属配置表名称。
/// 已解析的 schema 模型。
/// YAML 文件路径,仅用于诊断信息。
/// YAML 文本内容。
/// 可选的跨表引用收集器;为 时只做结构校验。
/// 当参数为空时抛出。
/// 当 YAML 内容与 schema 不匹配时抛出。
private static void ValidateCore(
string tableName,
YamlConfigSchema schema,
string yamlPath,
string yamlText,
ICollection? references)
{
if (string.IsNullOrWhiteSpace(tableName))
{
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
}
ArgumentNullException.ThrowIfNull(schema);
ArgumentNullException.ThrowIfNull(yamlPath);
ArgumentNullException.ThrowIfNull(yamlText);
YamlStream yamlStream = new();
try
{
using var reader = new StringReader(yamlText);
yamlStream.Load(reader);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.YamlParseFailed,
tableName,
$"Config file '{yamlPath}' could not be parsed as YAML before schema validation.",
yamlPath: yamlPath,
schemaPath: schema.SchemaPath,
innerException: exception);
}
if (yamlStream.Documents.Count != 1)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.InvalidYamlDocument,
tableName,
$"Config file '{yamlPath}' must contain exactly one YAML document.",
yamlPath: yamlPath,
schemaPath: schema.SchemaPath);
}
ValidateNode(tableName, yamlPath, string.Empty, yamlStream.Documents[0].RootNode, schema.RootNode, references);
}
///
/// 解析已读取到内存中的 schema 文本,并构造运行时最小模型。
///
/// 所属配置表名称。
/// Schema 文件路径,仅用于诊断信息。
/// Schema 文本内容。
/// 解析后的 schema 模型。
private static YamlConfigSchema ParseLoadedSchema(
string tableName,
string schemaPath,
string schemaText)
{
try
{
using var document = JsonDocument.Parse(schemaText);
var root = document.RootElement;
var rootNode = ParseNode(tableName, schemaPath, "", root, isRoot: true);
if (rootNode.NodeType != YamlConfigSchemaPropertyType.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Schema file '{schemaPath}' must declare a root object schema.",
schemaPath: schemaPath);
}
var referencedTableNames = new HashSet(StringComparer.Ordinal);
CollectReferencedTableNames(rootNode, referencedTableNames);
// Preserve a deterministic dependency order so hot-reload bookkeeping and tests
// do not depend on HashSet enumeration details.
var orderedReferencedTableNames = referencedTableNames
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
return new YamlConfigSchema(schemaPath, rootNode, orderedReferencedTableNames);
}
catch (JsonException exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaInvalidJson,
tableName,
$"Schema file '{schemaPath}' contains invalid JSON.",
schemaPath: schemaPath,
innerException: exception);
}
}
///
/// 递归解析 schema 节点,使运行时只保留校验真正需要的最小结构信息。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 当前节点的逻辑属性路径。
/// Schema JSON 节点。
/// 是否为根节点。
/// 可用于运行时校验的节点模型。
private static YamlConfigSchemaNode ParseNode(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
bool isRoot = false)
{
ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element);
var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);
var parsedNode = CreateParsedNodeForType(
tableName,
schemaPath,
propertyPath,
element,
typeName,
referenceTableName,
isRoot);
return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
}
///
/// 显式拒绝当前共享子集中尚未支持、且会改变生成类型形状的组合关键字。
/// 这样 Runtime / Generator / Tooling 会对同一份 schema 给出一致失败,
/// 而不是默默忽略 oneOf / anyOf 造成接受范围漂移。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 当前节点的逻辑属性路径。
/// 当前 schema 节点。
private static void ValidateUnsupportedCombinatorKeywords(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported combinator keyword '{keywordName}'. " +
"The current config schema subset does not support combinators that can change generated type shape.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
///
/// 显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
/// 当前配置系统默认采用闭合对象字段集;这里只接受显式重复该语义的
/// additionalProperties: false,并继续拒绝
/// patternProperties、propertyNames 与
/// unevaluatedProperties 这类会重新打开对象形状的关键字。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 当前节点的逻辑属性路径。
/// 当前 schema 节点。
private static void ValidateUnsupportedOpenObjectKeywords(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
{
return;
}
if (string.Equals(keywordName, "additionalProperties", StringComparison.Ordinal) &&
element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
additionalPropertiesElement.ValueKind == JsonValueKind.False)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
"The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
///
/// 返回当前节点声明的首个未支持组合关键字。
///
/// 当前 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 节点声明的类型名称,并在缺失或类型错误时立刻给出定位清晰的诊断。
///
private static string ResolveNodeTypeName(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("type", out var typeElement) ||
typeElement.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'type'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return typeElement.GetString() ?? string.Empty;
}
///
/// 限制只允许对象 schema 使用对象专属关键字,避免后续分支在运行时才发现语义不兼容。
///
private static void ValidateObjectOnlyKeywords(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
string typeName)
{
if (string.Equals(typeName, "object", StringComparison.Ordinal) ||
TryGetObjectOnlyKeywordName(element) is not { } objectOnlyKeywordName)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
///
/// 根据声明的 schema 类型分派到对应的节点解析器。
///
private static YamlConfigSchemaNode CreateParsedNodeForType(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
string typeName,
string? referenceTableName,
bool isRoot)
{
return typeName switch
{
"object" => ParseObjectSchemaNode(
tableName,
schemaPath,
propertyPath,
element,
referenceTableName,
isRoot),
"array" => ParseArrayNode(tableName, schemaPath, propertyPath, element, referenceTableName),
"integer" => CreateScalarNode(
tableName,
schemaPath,
propertyPath,
YamlConfigSchemaPropertyType.Integer,
element,
referenceTableName),
"number" => CreateScalarNode(
tableName,
schemaPath,
propertyPath,
YamlConfigSchemaPropertyType.Number,
element,
referenceTableName),
"boolean" => CreateScalarNode(
tableName,
schemaPath,
propertyPath,
YamlConfigSchemaPropertyType.Boolean,
element,
referenceTableName),
"string" => CreateScalarNode(
tableName,
schemaPath,
propertyPath,
YamlConfigSchemaPropertyType.String,
element,
referenceTableName),
_ => throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath),
rawValue: typeName)
};
}
///
/// 解析对象类型 schema,并在进入对象节点解析前先校验 ref-table 是否兼容。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 对象属性路径。
/// 对象 schema 节点。
/// 声明在当前节点上的目标引用表。
/// 是否为根节点。
/// 对象节点模型。
private static YamlConfigSchemaNode ParseObjectSchemaNode(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
string? referenceTableName,
bool isRoot)
{
EnsureReferenceKeywordIsSupported(
tableName,
schemaPath,
propertyPath,
YamlConfigSchemaPropertyType.Object,
referenceTableName);
return ParseObjectNode(tableName, schemaPath, propertyPath, element, isRoot);
}
///
/// 解析对象节点,保留属性字典与必填集合,以便后续递归校验时逐层定位错误。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 对象属性路径。
/// 对象 schema 节点。
/// 是否为根节点。
/// 对象节点模型。
private static YamlConfigSchemaNode ParseObjectNode(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
bool isRoot)
{
if (!element.TryGetProperty("properties", out var propertiesElement) ||
propertiesElement.ValueKind != JsonValueKind.Object)
{
var subject = isRoot ? "root schema" : $"object property '{propertyPath}'";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"The {subject} in schema file '{schemaPath}' must declare an object-valued 'properties' section.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
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)
{
continue;
}
var requiredPropertyName = item.GetString();
if (!string.IsNullOrWhiteSpace(requiredPropertyName))
{
requiredProperties.Add(requiredPropertyName);
}
}
}
var properties = new Dictionary(StringComparer.Ordinal);
foreach (var property in propertiesElement.EnumerateObject())
{
properties[property.Name] = ParseNode(
tableName,
schemaPath,
CombineSchemaPath(propertyPath, property.Name),
property.Value);
}
var objectNode = YamlConfigSchemaNode.CreateObject(
properties,
requiredProperties,
ParseObjectConstraints(tableName, schemaPath, propertyPath, element, properties),
schemaPath);
objectNode = objectNode.WithAllowedValues(
ParseEnumValues(tableName, schemaPath, propertyPath, element, objectNode, "enum"));
return objectNode.WithConstantValue(
ParseConstantValue(tableName, schemaPath, propertyPath, element, objectNode));
}
///
/// 解析数组节点。
/// 当前子集支持标量数组和对象数组,不支持数组嵌套数组。
/// 当数组声明跨表引用时,会把引用语义挂到元素节点上,便于后续逐项校验。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 数组属性路径。
/// 数组 schema 节点。
/// 声明在数组节点上的目标引用表。
/// 数组节点模型。
private static YamlConfigSchemaNode ParseArrayNode(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
string? referenceTableName)
{
if (!element.TryGetProperty("items", out var itemsElement) ||
itemsElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Array property '{propertyPath}' in schema file '{schemaPath}' must declare an object-valued 'items' schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var itemNode = ParseNode(tableName, schemaPath, $"{propertyPath}[]", itemsElement);
if (!string.IsNullOrWhiteSpace(referenceTableName))
{
if (itemNode.NodeType != YamlConfigSchemaPropertyType.String &&
itemNode.NodeType != YamlConfigSchemaPropertyType.Integer)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath),
referencedTableName: referenceTableName);
}
itemNode = itemNode.WithReferenceTable(referenceTableName);
}
if (itemNode.NodeType == YamlConfigSchemaPropertyType.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Array property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array items.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var arrayNode = YamlConfigSchemaNode.CreateArray(
itemNode,
allowedValues: null,
ParseArrayConstraints(tableName, schemaPath, propertyPath, element),
schemaPath);
arrayNode = arrayNode.WithAllowedValues(
ParseEnumValues(tableName, schemaPath, propertyPath, element, arrayNode, "enum"));
return arrayNode.WithConstantValue(
ParseConstantValue(tableName, schemaPath, propertyPath, element, arrayNode));
}
///
/// 创建标量节点,并在解析阶段就完成 enum 与引用约束的兼容性检查。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 标量属性路径。
/// 标量类型。
/// 标量 schema 节点。
/// 目标引用表名称。
/// 标量节点模型。
private static YamlConfigSchemaNode CreateScalarNode(
string tableName,
string schemaPath,
string propertyPath,
YamlConfigSchemaPropertyType nodeType,
JsonElement element,
string? referenceTableName)
{
EnsureReferenceKeywordIsSupported(tableName, schemaPath, propertyPath, nodeType, referenceTableName);
var scalarNode = YamlConfigSchemaNode.CreateScalar(
nodeType,
referenceTableName,
allowedValues: null,
ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
schemaPath);
scalarNode = scalarNode.WithAllowedValues(
ParseEnumValues(tableName, schemaPath, propertyPath, element, scalarNode, "enum"));
return scalarNode.WithConstantValue(
ParseConstantValue(tableName, schemaPath, propertyPath, element, scalarNode));
}
///
/// 解析节点上的 not 约束。
/// 该子 schema 继续复用同一套节点解析逻辑,保证 Runtime / Generator / Tooling
/// 对深层结构与格式白名单的解释保持一致。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 当前节点路径。
/// Schema JSON 节点。
/// 解析后的 negated schema;未声明时返回空。
private static YamlConfigSchemaNode? ParseNegatedSchemaNode(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("not", out var notElement))
{
return null;
}
if (notElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'not' as an object-valued schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return ParseNode(
tableName,
schemaPath,
BuildNestedSchemaPath(propertyPath, "not"),
notElement);
}
///
/// 为 contains / not / dependentSchemas 这类内联子 schema 构建稳定的诊断路径。
///
/// 当前节点路径。
/// 内联子 schema 后缀。
/// 带内联后缀的 schema 路径。
private static string BuildNestedSchemaPath(string propertyPath, string suffix)
{
return string.IsNullOrWhiteSpace(propertyPath)
? $"[{suffix}]"
: $"{propertyPath}[{suffix}]";
}
///
/// 递归校验 YAML 节点。
/// 每层都带上逻辑字段路径,这样深层对象与数组元素的错误也能直接定位。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 当前字段路径;根节点时为空。
/// 实际 YAML 节点。
/// 对应的 schema 节点。
/// 已收集的跨表引用。
///
/// 是否允许对象节点出现当前 schema 子树未声明的额外字段。
/// 该开关仅用于 contains 试匹配,让对象子 schema 可以按“声明属性子集匹配”工作;
/// 正常加载主链路仍保持未知字段即失败的严格语义。
///
private static void ValidateNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode,
ICollection? references,
bool allowUnknownObjectProperties = false)
{
switch (schemaNode.NodeType)
{
case YamlConfigSchemaPropertyType.Object:
ValidateObjectNode(
tableName,
yamlPath,
displayPath,
node,
schemaNode,
references,
allowUnknownObjectProperties);
return;
case YamlConfigSchemaPropertyType.Array:
ValidateArrayNode(
tableName,
yamlPath,
displayPath,
node,
schemaNode,
references,
allowUnknownObjectProperties);
return;
case YamlConfigSchemaPropertyType.Integer:
case YamlConfigSchemaPropertyType.Number:
case YamlConfigSchemaPropertyType.Boolean:
case YamlConfigSchemaPropertyType.String:
ValidateScalarNode(tableName, yamlPath, displayPath, node, schemaNode, references);
return;
default:
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.UnexpectedFailure,
tableName,
$"Schema node '{displayPath}' uses unsupported runtime node type '{schemaNode.NodeType}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: schemaNode.NodeType.ToString());
}
}
///
/// 校验对象节点,同时处理重复字段、未知字段和深层必填字段。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 当前对象的逻辑字段路径。
/// 实际 YAML 节点。
/// 对象 schema 节点。
/// 已收集的跨表引用。
///
/// 是否允许当前对象包含 schema 子树未声明的额外字段。
///
private static void ValidateObjectNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode,
ICollection? references,
bool allowUnknownObjectProperties)
{
var mappingNode = GetObjectMappingNode(tableName, yamlPath, displayPath, node, schemaNode);
var seenProperties = ValidateObjectPropertyEntries(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
references,
allowUnknownObjectProperties);
ValidateRequiredObjectProperties(tableName, yamlPath, displayPath, schemaNode, seenProperties);
ValidateObjectConstraints(
tableName,
yamlPath,
displayPath,
mappingNode,
seenProperties,
schemaNode,
references);
ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode);
ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
}
///
/// 确认当前 YAML 节点确实是对象节点,避免后续属性枚举阶段再做重复判空与类型判断。
///
private static YamlMappingNode GetObjectMappingNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode)
{
if (node is YamlMappingNode mappingNode)
{
return mappingNode;
}
var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.PropertyTypeMismatch,
tableName,
$"{subject} in config file '{yamlPath}' must be an object.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
///
/// 遍历对象属性并递归校验每个已声明字段,同时记录用于后续 required 与 dependency 判断的 sibling 集合。
///
private static HashSet ValidateObjectPropertyEntries(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
YamlConfigSchemaNode schemaNode,
ICollection? references,
bool allowUnknownObjectProperties)
{
var seenProperties = new HashSet(StringComparer.Ordinal);
foreach (var entry in mappingNode.Children)
{
if (entry.Key is not YamlScalarNode keyNode ||
string.IsNullOrWhiteSpace(keyNode.Value))
{
var subject = displayPath.Length == 0 ? "root object" : $"object property '{displayPath}'";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.InvalidYamlDocument,
tableName,
$"Config file '{yamlPath}' contains a non-scalar or empty property name inside {subject}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
var propertyName = keyNode.Value;
var propertyPath = CombineDisplayPath(displayPath, propertyName);
if (!seenProperties.Add(propertyName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.DuplicateProperty,
tableName,
$"Config file '{yamlPath}' contains duplicate property '{propertyPath}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: propertyPath);
}
if (schemaNode.Properties is null ||
!schemaNode.Properties.TryGetValue(propertyName, out var propertySchema))
{
if (allowUnknownObjectProperties)
{
continue;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.UnknownProperty,
tableName,
$"Config file '{yamlPath}' contains unknown property '{propertyPath}' that is not declared in schema '{schemaNode.SchemaPathHint}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: propertyPath);
}
ValidateNode(
tableName,
yamlPath,
propertyPath,
entry.Value,
propertySchema,
references,
allowUnknownObjectProperties);
}
return seenProperties;
}
///
/// 在对象主体字段遍历结束后统一检查缺失的 required 字段,保证错误消息使用稳定的完整路径。
///
private static void ValidateRequiredObjectProperties(
string tableName,
string yamlPath,
string displayPath,
YamlConfigSchemaNode schemaNode,
HashSet seenProperties)
{
if (schemaNode.RequiredProperties is null)
{
return;
}
foreach (var requiredProperty in schemaNode.RequiredProperties)
{
if (seenProperties.Contains(requiredProperty))
{
continue;
}
var requiredPath = CombineDisplayPath(displayPath, requiredProperty);
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.MissingRequiredProperty,
tableName,
$"Config file '{yamlPath}' is missing required property '{requiredPath}' defined by schema '{schemaNode.SchemaPathHint}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: requiredPath);
}
}
///
/// 校验对象节点声明的数量约束与条件对象约束。
/// 该阶段除了检查 minProperties / maxProperties,还会复用同一份 sibling 集合处理
/// dependentRequired,并在 dependentSchemas 命中时以 focused constraint block 语义
/// 对整个 做额外试匹配;若声明了 object-focused
/// if / then / else,则先按同样的 focused matcher 判断条件分支,再只对命中的分支追加约束。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 对象字段路径;根对象时为空。
/// 当前 YAML 对象节点;用于让条件子 schema 在完整对象视图上做匹配。
/// 当前对象已出现的属性集合。
/// 对象 schema 节点。
///
/// 可选的跨表引用收集器;当 dependentSchemas、allOf 或条件分支命中且匹配成功时,
/// 只会回写对应内联分支新增的引用。
///
private static void ValidateObjectConstraints(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
HashSet seenProperties,
YamlConfigSchemaNode schemaNode,
ICollection? references)
{
var constraints = schemaNode.ObjectConstraints;
if (constraints is null)
{
return;
}
var propertyCount = seenProperties.Count;
var subject = GetObjectConstraintSubject(displayPath);
ValidateObjectPropertyCountConstraints(
tableName,
yamlPath,
displayPath,
schemaNode,
constraints,
subject,
propertyCount);
ValidateDependentRequiredConstraints(
tableName,
yamlPath,
displayPath,
schemaNode,
constraints,
seenProperties);
ValidateDependentSchemas(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
constraints,
references,
seenProperties,
subject);
ValidateAllOfSchemas(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
constraints,
references,
subject);
ValidateConditionalObjectSchemas(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
constraints,
references,
subject);
}
///
/// 为对象约束构造统一的诊断主语,保证根对象与嵌套对象的错误消息格式一致。
///
private static string GetObjectConstraintSubject(string displayPath)
{
return string.IsNullOrWhiteSpace(displayPath)
? "Root object"
: $"Property '{displayPath}'";
}
///
/// 校验对象属性数量上下限。
///
private static void ValidateObjectPropertyCountConstraints(
string tableName,
string yamlPath,
string displayPath,
YamlConfigSchemaNode schemaNode,
YamlConfigObjectConstraints constraints,
string subject,
int propertyCount)
{
var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture);
if (constraints.MinProperties.HasValue &&
propertyCount < constraints.MinProperties.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must contain at least {constraints.MinProperties.Value.ToString(CultureInfo.InvariantCulture)} properties.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail:
$"Minimum property count: {constraints.MinProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
}
if (constraints.MaxProperties.HasValue &&
propertyCount > constraints.MaxProperties.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must contain at most {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)} properties.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail:
$"Maximum property count: {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
}
}
///
/// 使用已见 sibling 集合校验 dependentRequired,确保对象主路径与试匹配路径共用同一判定语义。
///
private static void ValidateDependentRequiredConstraints(
string tableName,
string yamlPath,
string displayPath,
YamlConfigSchemaNode schemaNode,
YamlConfigObjectConstraints constraints,
HashSet seenProperties)
{
if (constraints.DependentRequired is null ||
constraints.DependentRequired.Count == 0)
{
return;
}
// Reuse the collected sibling-name set so the main validation path and
// the contains/not matcher both interpret object dependencies identically.
foreach (var dependency in constraints.DependentRequired)
{
if (!seenProperties.Contains(dependency.Key))
{
continue;
}
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
foreach (var dependentProperty in dependency.Value)
{
if (seenProperties.Contains(dependentProperty))
{
continue;
}
var requiredPath = CombineDisplayPath(displayPath, dependentProperty);
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.MissingRequiredProperty,
tableName,
$"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: requiredPath,
detail:
$"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared.");
}
}
}
///
/// 在触发字段出现时,以 focused matcher 语义试跑 dependentSchemas。
///
private static void ValidateDependentSchemas(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
YamlConfigSchemaNode schemaNode,
YamlConfigObjectConstraints constraints,
ICollection? references,
HashSet seenProperties,
string subject)
{
if (constraints.DependentSchemas is null ||
constraints.DependentSchemas.Count == 0)
{
return;
}
foreach (var dependency in constraints.DependentSchemas)
{
if (!seenProperties.Contains(dependency.Key))
{
continue;
}
var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
// dependentSchemas acts as an additional conditional constraint block on the
// current object. Keep undeclared sibling fields outside the dependent sub-schema
// from blocking the match so schema authors can express focused follow-up rules.
// The trial matcher merges only new reference usages back into the outer collector,
// so re-checking the same scalar via a conditional sub-schema does not duplicate
// cross-table validation work later in the loader pipeline.
if (TryMatchSchemaNode(
tableName,
yamlPath,
displayPath,
mappingNode,
dependency.Value,
references,
allowUnknownObjectProperties: true))
{
continue;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
detail:
$"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema.");
}
}
///
/// 逐条校验 allOf 约束,保持与 dependentSchemas 相同的 focused object 匹配语义。
///
private static void ValidateAllOfSchemas(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
YamlConfigSchemaNode schemaNode,
YamlConfigObjectConstraints constraints,
ICollection? references,
string subject)
{
if (constraints.AllOfSchemas is null ||
constraints.AllOfSchemas.Count == 0)
{
return;
}
for (var index = 0; index < constraints.AllOfSchemas.Count; index++)
{
var allOfSchema = constraints.AllOfSchemas[index];
// allOf follows the same focused constraint block semantics as dependentSchemas:
// the inline schema may validate a subset of the current object without forcing
// unrelated sibling fields to be restated.
if (TryMatchSchemaNode(
tableName,
yamlPath,
displayPath,
mappingNode,
allOfSchema,
references,
allowUnknownObjectProperties: true))
{
continue;
}
var allOfEntryNumber = index + 1;
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
detail:
$"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object.");
}
}
///
/// 执行对象级 if/then/else 约束,并沿用 focused matcher 允许条件 schema 只声明关注字段。
///
private static void ValidateConditionalObjectSchemas(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
YamlConfigSchemaNode schemaNode,
YamlConfigObjectConstraints constraints,
ICollection? references,
string subject)
{
var conditionalSchemas = constraints.ConditionalSchemas;
if (conditionalSchemas is null)
{
return;
}
// if/then/else follows the same object-focused matcher contract as dependentSchemas/allOf:
// condition evaluation can inspect a subset of the current object without forcing unrelated
// sibling fields to be re-declared inside the branch schema.
var ifMatched = TryMatchSchemaNode(
tableName,
yamlPath,
displayPath,
mappingNode,
conditionalSchemas.IfSchema,
references,
allowUnknownObjectProperties: true);
ValidateConditionalSchemaBranch(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
references,
subject,
ifMatched,
conditionalSchemas.ThenSchema,
branchName: "then",
failureDetail:
"Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema.");
ValidateConditionalSchemaBranch(
tableName,
yamlPath,
displayPath,
mappingNode,
schemaNode,
references,
subject,
!ifMatched,
conditionalSchemas.ElseSchema,
branchName: "else",
failureDetail:
"Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema.");
}
///
/// 校验 if/then/else 的单个分支,并在条件命中但分支未通过时提供统一诊断。
///
private static void ValidateConditionalSchemaBranch(
string tableName,
string yamlPath,
string displayPath,
YamlMappingNode mappingNode,
YamlConfigSchemaNode schemaNode,
ICollection? references,
string subject,
bool shouldValidate,
YamlConfigSchemaNode? branchSchema,
string branchName,
string failureDetail)
{
if (!shouldValidate ||
branchSchema is null ||
TryMatchSchemaNode(
tableName,
yamlPath,
displayPath,
mappingNode,
branchSchema,
references,
allowUnknownObjectProperties: true))
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must satisfy the '{branchName}' schema because the inline 'if' condition {(string.Equals(branchName, "then", StringComparison.Ordinal) ? "matched" : "did not match")}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
detail: failureDetail);
}
///
/// 校验数组节点,并递归验证每个元素。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 数组字段路径。
/// 实际 YAML 节点。
/// 数组 schema 节点。
/// 已收集的跨表引用。
///
/// 是否允许数组元素内的对象节点包含 schema 子树未声明的额外字段。
///
private static void ValidateArrayNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode,
ICollection? references,
bool allowUnknownObjectProperties)
{
if (node is not YamlSequenceNode sequenceNode)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.PropertyTypeMismatch,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be an array.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
if (schemaNode.ItemNode is null)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.UnexpectedFailure,
tableName,
$"Schema node '{displayPath}' is missing array item information.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
if (schemaNode.ArrayConstraints is not null)
{
ValidateArrayConstraints(tableName, yamlPath, displayPath, sequenceNode.Children.Count, schemaNode);
}
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
{
ValidateNode(
tableName,
yamlPath,
$"{displayPath}[{itemIndex}]",
sequenceNode.Children[itemIndex],
schemaNode.ItemNode,
references,
allowUnknownObjectProperties);
}
ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
ValidateAllowedValues(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
}
///
/// 校验标量节点,并在值有效时收集跨表引用。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 标量字段路径。
/// 实际 YAML 节点。
/// 标量 schema 节点。
/// 已收集的跨表引用。
private static void ValidateScalarNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode,
ICollection? references)
{
var scalarNode = GetScalarNodeOrThrow(tableName, yamlPath, displayPath, node, schemaNode);
var value = GetScalarValueOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode);
ValidateScalarTypeOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode, value);
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode);
if (schemaNode.Constraints is not null)
{
ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode);
}
ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode);
ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode);
CollectScalarReferenceUsage(yamlPath, displayPath, schemaNode, references, normalizedValue);
}
///
/// 确认 schema 期望的节点在 YAML 中仍然表现为标量。
///
private static YamlScalarNode GetScalarNodeOrThrow(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode)
{
if (node is YamlScalarNode scalarNode)
{
return scalarNode;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.PropertyTypeMismatch,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
///
/// 读取标量文本值,并把 YAML 显式 null 与普通空字符串区分开。
///
private static string GetScalarValueOrThrow(
string tableName,
string yamlPath,
string displayPath,
YamlScalarNode scalarNode,
YamlConfigSchemaNode schemaNode)
{
if (scalarNode.Value is { } value)
{
return value;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.NullScalarValue,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath));
}
///
/// 按 schema 声明的标量类型验证 YAML 文本值。
///
private static void ValidateScalarTypeOrThrow(
string tableName,
string yamlPath,
string displayPath,
YamlScalarNode scalarNode,
YamlConfigSchemaNode schemaNode,
string value)
{
var tag = scalarNode.Tag.ToString();
var isValid = schemaNode.NodeType switch
{
YamlConfigSchemaPropertyType.String => IsStringScalar(tag),
YamlConfigSchemaPropertyType.Integer => long.TryParse(
value,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out _),
YamlConfigSchemaPropertyType.Number => double.TryParse(
value,
NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture,
out _),
YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _),
_ => false
};
if (isValid)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.PropertyTypeMismatch,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: value);
}
///
/// 在标量值成功通过本地校验后,再把引用信息回写给外层收集器。
///
private static void CollectScalarReferenceUsage(
string yamlPath,
string displayPath,
YamlConfigSchemaNode schemaNode,
ICollection? references,
string normalizedValue)
{
if (schemaNode.ReferenceTableName is null ||
references is null)
{
return;
}
references.Add(
new YamlConfigReferenceUsage(
yamlPath,
schemaNode.SchemaPathHint,
displayPath,
normalizedValue,
schemaNode.ReferenceTableName,
schemaNode.NodeType));
}
///
/// 解析 enum,并在读取阶段将每个候选值预归一化成与运行时 YAML 相同的稳定比较键。
/// 这样对象、数组和标量节点都可以复用同一套匹配逻辑,而不必在每次加载时重新解释 schema 字面量。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 当前节点的已解析 schema 模型。
/// 当前读取的关键字名称。
/// 归一化后的允许值集合;未声明时返回空。
private static IReadOnlyCollection? ParseEnumValues(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaNode schemaNode,
string keywordName)
{
if (!element.TryGetProperty("enum", out var enumElement))
{
return null;
}
if (enumElement.ValueKind != JsonValueKind.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as an array.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var allowedValues = new List();
foreach (var item in enumElement.EnumerateArray())
{
allowedValues.Add(
new YamlConfigAllowedValue(
BuildComparableConstantValue(tableName, schemaPath, propertyPath, keywordName, item, schemaNode),
BuildEnumDisplayValue(item)));
}
return allowedValues;
}
///
/// 读取一个 enum 候选值的展示文本。
/// 这里直接保留原始 JSON 字面量,避免字符串引号、对象字段顺序或数组结构在诊断中被二次格式化后丢失上下文。
///
/// Schema 中的单个枚举值。
/// 适合放入诊断消息的展示文本。
private static string BuildEnumDisplayValue(JsonElement element)
{
return element.GetRawText();
}
///
/// 解析 const,并把 schema 常量预归一化成与运行时 YAML 相同的稳定比较键。
/// 这样运行时只需要复用现有递归比较逻辑,而不必在每次加载时重新解释 JSON 常量。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 已解析的 schema 节点。
/// 常量约束模型;未声明时返回空。
private static YamlConfigConstantValue? ParseConstantValue(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaNode schemaNode)
{
if (!element.TryGetProperty("const", out var constantElement))
{
return null;
}
return new YamlConfigConstantValue(
BuildComparableConstantValue(tableName, schemaPath, propertyPath, "const", constantElement, schemaNode),
constantElement.GetRawText());
}
///
/// 把 schema 中的 const JSON 值转换成与 YAML 运行时一致的比较键。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 关键字名称。
/// 常量 JSON 值。
/// 目标 schema 节点。
/// 可稳定比较的归一化键。
private static string BuildComparableConstantValue(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
JsonElement element,
YamlConfigSchemaNode schemaNode)
{
return schemaNode.NodeType switch
{
YamlConfigSchemaPropertyType.Object => BuildComparableConstantObjectValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
YamlConfigSchemaPropertyType.Array => BuildComparableConstantArrayValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
YamlConfigSchemaPropertyType.Integer => BuildComparableConstantScalarValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
YamlConfigSchemaPropertyType.Number => BuildComparableConstantScalarValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
YamlConfigSchemaPropertyType.Boolean => BuildComparableConstantScalarValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
YamlConfigSchemaPropertyType.String => BuildComparableConstantScalarValue(
tableName,
schemaPath,
propertyPath,
keywordName,
element,
schemaNode),
_ => throw new InvalidOperationException($"Unsupported schema node type '{schemaNode.NodeType}'.")
};
}
///
/// 构建对象常量的稳定比较键。
/// 这里同样忽略 JSON 对象字段顺序,避免 schema 文本格式影响常量比较结果。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 关键字名称。
/// 常量 JSON 值。
/// 对象 schema 节点。
/// 对象常量的可比较键。
private static string BuildComparableConstantObjectValue(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
JsonElement element,
YamlConfigSchemaNode schemaNode)
{
if (element.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only object values are compatible with schema type '{GetTypeName(schemaNode.NodeType)}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var properties = schemaNode.Properties
?? throw new InvalidOperationException("Object schema nodes must expose declared properties.");
var objectEntries = new List>();
foreach (var property in element.EnumerateObject())
{
if (!properties.TryGetValue(property.Name, out var propertySchema))
{
var childPath = CombineSchemaPath(propertyPath, property.Name);
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but nested property '{childPath}' is not declared in the object schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(childPath));
}
objectEntries.Add(
new KeyValuePair(
property.Name,
BuildComparableConstantValue(
tableName,
schemaPath,
CombineSchemaPath(propertyPath, property.Name),
keywordName,
property.Value,
propertySchema)));
}
objectEntries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
return string.Join(
"|",
objectEntries.Select(static entry =>
$"{entry.Key.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Key}={entry.Value.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Value}"));
}
///
/// 构建数组常量的稳定比较键。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 关键字名称。
/// 常量 JSON 值。
/// 数组 schema 节点。
/// 数组常量的可比较键。
private static string BuildComparableConstantArrayValue(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
JsonElement element,
YamlConfigSchemaNode schemaNode)
{
if (element.ValueKind != JsonValueKind.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only array values are compatible with schema type '{GetTypeName(schemaNode.NodeType)}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (schemaNode.ItemNode is null)
{
throw new InvalidOperationException("Array schema nodes must expose their item schema.");
}
return "[" +
string.Join(
",",
element.EnumerateArray().Select((item, index) =>
{
var comparableValue = BuildComparableConstantValue(
tableName,
schemaPath,
$"{propertyPath}[{index}]",
keywordName,
item,
schemaNode.ItemNode);
return
$"{comparableValue.Length.ToString(CultureInfo.InvariantCulture)}:{comparableValue}";
})) +
"]";
}
///
/// 构建标量常量的稳定比较键。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 关键字名称。
/// 常量 JSON 值。
/// 标量 schema 节点。
/// 标量常量的可比较键。
private static string BuildComparableConstantScalarValue(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
JsonElement element,
YamlConfigSchemaNode schemaNode)
{
var normalizedValue = NormalizeKeywordScalarValue(
tableName,
schemaPath,
propertyPath,
keywordName,
schemaNode.NodeType,
element);
return
$"{schemaNode.NodeType}:{normalizedValue.Length.ToString(CultureInfo.InvariantCulture)}:{normalizedValue}";
}
///
/// 解析标量字段支持的范围、长度与模式约束。
/// 当前共享子集支持:
/// `integer/number` 上的 `minimum/maximum/exclusiveMinimum/exclusiveMaximum`,
/// 以及 `string` 上的 `minLength/maxLength/pattern/format`。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 标量类型。
/// 解析后的约束模型;未声明时返回空。
private static YamlConfigScalarConstraints? ParseScalarConstraints(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType)
{
var minimum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minimum");
var maximum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maximum");
var exclusiveMinimum =
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMinimum");
var exclusiveMaximum =
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMaximum");
var multipleOf = TryParseMultipleOfConstraint(tableName, schemaPath, propertyPath, element, nodeType);
var minLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minLength");
var maxLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maxLength");
var pattern = TryParsePatternConstraint(tableName, schemaPath, propertyPath, element, nodeType);
var formatConstraint = TryParseFormatConstraint(tableName, schemaPath, propertyPath, element, nodeType);
if (minimum.HasValue && maximum.HasValue && minimum.Value > maximum.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minimum' greater than 'maximum'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
ValidateNumericConstraintRange(
tableName,
schemaPath,
propertyPath,
minimum,
maximum,
exclusiveMinimum,
exclusiveMaximum);
if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minLength' greater than 'maxLength'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var numericConstraints = CreateNumericScalarConstraints(
minimum,
maximum,
exclusiveMinimum,
exclusiveMaximum,
multipleOf);
var stringConstraints = CreateStringScalarConstraints(
minLength,
maxLength,
pattern,
formatConstraint);
return numericConstraints is null && stringConstraints is null
? null
: new YamlConfigScalarConstraints(numericConstraints, stringConstraints);
}
///
/// 解析数组节点支持的元素数量、去重与 contains 匹配数量约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 数组字段路径。
/// Schema 节点。
/// 数组约束模型;未声明时返回空。
private static YamlConfigArrayConstraints? ParseArrayConstraints(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
var minItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minItems");
var maxItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxItems");
var uniqueItems = TryParseUniqueItemsConstraint(tableName, schemaPath, propertyPath, element);
var containsConstraints = ParseArrayContainsConstraints(tableName, schemaPath, propertyPath, element);
if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minItems' greater than 'maxItems'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return !minItems.HasValue && !maxItems.HasValue && !uniqueItems && containsConstraints is null
? null
: new YamlConfigArrayConstraints(minItems, maxItems, uniqueItems, containsConstraints);
}
///
/// 解析数组节点声明的 contains 约束及其匹配数量边界。
/// 运行时会把 contains 解析成独立的 schema 子树,后续逐项复用同一套递归校验逻辑判断“是否匹配”。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 数组字段路径。
/// Schema 节点。
/// 数组 contains 约束模型;未声明时返回空。
private static YamlConfigArrayContainsConstraints? ParseArrayContainsConstraints(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
var minContains = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minContains");
var maxContains = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxContains");
if (!element.TryGetProperty("contains", out var containsElement))
{
if (minContains.HasValue || maxContains.HasValue)
{
var keywordName = minContains.HasValue ? "minContains" : "maxContains";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares '{keywordName}' without a companion 'contains' schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return null;
}
if (containsElement.ValueKind != JsonValueKind.Object)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'contains' as an object-valued schema.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var containsNode = ParseNode(tableName, schemaPath, $"{propertyPath}[contains]", containsElement);
if (containsNode.NodeType == YamlConfigSchemaPropertyType.Array)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array 'contains' schemas.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var effectiveMinContains = minContains ?? 1;
if (maxContains.HasValue && effectiveMinContains > maxContains.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minContains' greater than 'maxContains'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return new YamlConfigArrayContainsConstraints(containsNode, minContains, maxContains);
}
///
/// 读取数值区间约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 字段类型。
/// 关键字名称。
/// 数值约束;未声明时返回空。
private static double? TryParseNumericConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType,
string keywordName)
{
if (!element.TryGetProperty(keywordName, out var constraintElement))
{
return null;
}
if (nodeType != YamlConfigSchemaPropertyType.Integer &&
nodeType != YamlConfigSchemaPropertyType.Number)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only 'integer' and 'number' scalar types support numeric range constraints.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (constraintElement.ValueKind != JsonValueKind.Number ||
!constraintElement.TryGetDouble(out var constraintValue) ||
double.IsNaN(constraintValue) ||
double.IsInfinity(constraintValue))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a finite number.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return constraintValue;
}
///
/// 读取 multipleOf 约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 字段类型。
/// 步进约束;未声明时返回空。
private static double? TryParseMultipleOfConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType)
{
var multipleOf =
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "multipleOf");
if (!multipleOf.HasValue)
{
return null;
}
if (multipleOf.Value <= 0d)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'multipleOf' as a positive finite number.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return multipleOf;
}
///
/// 读取字符串长度约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 字段类型。
/// 关键字名称。
/// 长度约束;未声明时返回空。
private static int? TryParseLengthConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType,
string keywordName)
{
if (!element.TryGetProperty(keywordName, out var constraintElement))
{
return null;
}
if (nodeType != YamlConfigSchemaPropertyType.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only 'string' scalar types support length constraints.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (constraintElement.ValueKind != JsonValueKind.Number ||
!constraintElement.TryGetInt32(out var constraintValue) ||
constraintValue < 0)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return constraintValue;
}
///
/// 读取字符串正则约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 字段类型。
/// 正则模式;未声明时返回空。
private static string? TryParsePatternConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType)
{
if (!element.TryGetProperty("pattern", out var patternElement))
{
return null;
}
if (nodeType != YamlConfigSchemaPropertyType.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'pattern', but only 'string' scalar types support regular-expression constraints.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (patternElement.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'pattern' as a string.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var pattern = patternElement.GetString() ?? string.Empty;
try
{
_ = new Regex(pattern, SupportedPatternRegexOptions, SupportedFormatRegexTimeout);
}
catch (ArgumentException exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares an invalid 'pattern' regular expression.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath),
rawValue: pattern,
innerException: exception);
}
return pattern;
}
///
/// 读取字符串 format 约束。
/// 运行时只接受当前三端共享并已验证收益的稳定子集,避免把开放格式名误当成“全部支持”。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 字段类型。
/// 归一化后的 format 约束;未声明时返回空。
private static YamlConfigStringFormatConstraint? TryParseFormatConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
YamlConfigSchemaPropertyType nodeType)
{
if (!element.TryGetProperty("format", out var formatElement))
{
return null;
}
if (nodeType != YamlConfigSchemaPropertyType.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'format', but only 'string' scalar types support string formats.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
if (formatElement.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'format' as a string.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var formatName = formatElement.GetString() ?? string.Empty;
if (TryMapSupportedStringFormat(formatName, out var kind))
{
return new YamlConfigStringFormatConstraint(formatName, kind);
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported string format '{formatName}'. Supported formats are {SupportedStringFormatNames}.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath),
rawValue: formatName);
}
///
/// 将 schema 原文中的 format 名称映射为运行时共享枚举。
/// 映射阶段统一使用大小写敏感比较,避免同一 schema 在不同环境下被“宽松容错”解释成不同语义。
///
/// schema 原始 format 名称。
/// 解析成功时输出归一化枚举。
/// 当前格式是否属于共享支持子集。
private static bool TryMapSupportedStringFormat(
string formatName,
out YamlConfigStringFormatKind kind)
{
switch (formatName)
{
case "date":
kind = YamlConfigStringFormatKind.Date;
return true;
case "date-time":
kind = YamlConfigStringFormatKind.DateTime;
return true;
case "duration":
kind = YamlConfigStringFormatKind.Duration;
return true;
case "email":
kind = YamlConfigStringFormatKind.Email;
return true;
case "time":
kind = YamlConfigStringFormatKind.Time;
return true;
case "uri":
kind = YamlConfigStringFormatKind.Uri;
return true;
case "uuid":
kind = YamlConfigStringFormatKind.Uuid;
return true;
default:
kind = default;
return false;
}
}
///
/// 读取数组元素数量约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 关键字名称。
/// 数组元素数量约束;未声明时返回空。
private static int? TryParseArrayLengthConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element,
string keywordName)
{
if (!element.TryGetProperty(keywordName, out var constraintElement))
{
return null;
}
if (constraintElement.ValueKind != JsonValueKind.Number ||
!constraintElement.TryGetInt32(out var constraintValue) ||
constraintValue < 0)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return constraintValue;
}
///
/// 读取数组去重约束。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 是否启用 uniqueItems。
private static bool TryParseUniqueItemsConstraint(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("uniqueItems", out var constraintElement))
{
return false;
}
if (constraintElement.ValueKind != JsonValueKind.True &&
constraintElement.ValueKind != JsonValueKind.False)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'uniqueItems' as a boolean.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return constraintElement.GetBoolean();
}
///
/// 校验数值上下界组合不会形成空区间。
/// 这里把闭区间与开区间统一折算为最强边界,避免 schema 进入“无任何合法值”的状态。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 闭区间最小值。
/// 闭区间最大值。
/// 开区间最小值。
/// 开区间最大值。
private static void ValidateNumericConstraintRange(
string tableName,
string schemaPath,
string propertyPath,
double? minimum,
double? maximum,
double? exclusiveMinimum,
double? exclusiveMaximum)
{
var hasLowerBound = false;
var lowerBound = double.MinValue;
var isLowerBoundExclusive = false;
if (minimum.HasValue)
{
hasLowerBound = true;
lowerBound = minimum.Value;
}
if (exclusiveMinimum.HasValue &&
(!hasLowerBound ||
exclusiveMinimum.Value > lowerBound ||
(exclusiveMinimum.Value.Equals(lowerBound) && !isLowerBoundExclusive)))
{
hasLowerBound = true;
lowerBound = exclusiveMinimum.Value;
isLowerBoundExclusive = true;
}
var hasUpperBound = false;
var upperBound = double.MaxValue;
var isUpperBoundExclusive = false;
if (maximum.HasValue)
{
hasUpperBound = true;
upperBound = maximum.Value;
}
if (exclusiveMaximum.HasValue &&
(!hasUpperBound ||
exclusiveMaximum.Value < upperBound ||
(exclusiveMaximum.Value.Equals(upperBound) && !isUpperBoundExclusive)))
{
hasUpperBound = true;
upperBound = exclusiveMaximum.Value;
isUpperBoundExclusive = true;
}
if (!hasLowerBound || !hasUpperBound)
{
return;
}
if (lowerBound > upperBound ||
(lowerBound.Equals(upperBound) && (isLowerBoundExclusive || isUpperBoundExclusive)))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' declares numeric constraints that do not leave any valid value range.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
}
///
/// 校验标量值是否满足范围与长度约束。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 原始 YAML 标量值。
/// 归一化后的比较值。
/// 标量 schema 节点。
private static void ValidateScalarConstraints(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
string normalizedValue,
YamlConfigSchemaNode schemaNode)
{
var constraints = schemaNode.Constraints;
if (constraints is null)
{
return;
}
switch (schemaNode.NodeType)
{
case YamlConfigSchemaPropertyType.Integer:
case YamlConfigSchemaPropertyType.Number:
ValidateNumericScalarConstraints(
tableName,
yamlPath,
displayPath,
rawValue,
normalizedValue,
schemaNode,
constraints.NumericConstraints);
return;
case YamlConfigSchemaPropertyType.String:
ValidateStringScalarConstraints(
tableName,
yamlPath,
displayPath,
rawValue,
schemaNode,
constraints.StringConstraints);
return;
default:
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.UnexpectedFailure,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' resolved unsupported constraint host type '{schemaNode.NodeType}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: schemaNode.NodeType.ToString());
}
}
///
/// 校验节点值是否命中声明的 enum 集合。
/// 该实现与 const 共享同一套可比较键,保证对象字段顺序、数组顺序和数值归一化语义稳定一致。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径;根节点时为空。
/// 当前 YAML 节点。
/// 对应的 schema 节点。
private static void ValidateAllowedValues(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode)
{
var allowedValues = schemaNode.AllowedValues;
if (allowedValues is null || allowedValues.Count == 0)
{
return;
}
var comparableValue = BuildComparableNodeValue(node, schemaNode);
if (allowedValues.Any(allowedValue =>
string.Equals(allowedValue.ComparableValue, comparableValue, StringComparison.Ordinal)))
{
return;
}
var subject = string.IsNullOrWhiteSpace(displayPath)
? "Root object"
: $"Property '{displayPath}'";
var displayValues = string.Join(", ", allowedValues.Select(static allowedValue => allowedValue.DisplayValue));
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.EnumValueNotAllowed,
tableName,
$"{subject} in config file '{yamlPath}' must be one of [{displayValues}].",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode),
detail: $"Allowed values: {displayValues}.");
}
///
/// 校验节点值是否满足 const 约束。
/// 该检查复用与 uniqueItems 相同的稳定比较键,保证对象字段顺序、数字字面量和布尔大小写不会造成伪差异。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径;根节点时为空。
/// 当前 YAML 节点。
/// 对应的 schema 节点。
private static void ValidateConstantValue(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode)
{
var constantValue = schemaNode.ConstantValue;
if (constantValue is null)
{
return;
}
var comparableValue = BuildComparableNodeValue(node, schemaNode);
if (string.Equals(comparableValue, constantValue.ComparableValue, StringComparison.Ordinal))
{
return;
}
var subject = string.IsNullOrWhiteSpace(displayPath)
? "Root object"
: $"Property '{displayPath}'";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must match constant value {constantValue.DisplayValue}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode),
detail: $"Required constant value: {constantValue.DisplayValue}.");
}
///
/// 根据已读取的数值关键字创建数值约束对象。
/// 该分组让调用方不必再维护一个超过 Sonar 默认阈值的长参数构造函数。
///
/// 最小值约束。
/// 最大值约束。
/// 开区间最小值约束。
/// 开区间最大值约束。
/// 数值步进约束。
/// 数值约束对象;未声明任何数值约束时返回空。
private static YamlConfigNumericConstraints? CreateNumericScalarConstraints(
double? minimum,
double? maximum,
double? exclusiveMinimum,
double? exclusiveMaximum,
double? multipleOf)
{
return !minimum.HasValue &&
!maximum.HasValue &&
!exclusiveMinimum.HasValue &&
!exclusiveMaximum.HasValue &&
!multipleOf.HasValue
? null
: new YamlConfigNumericConstraints(
minimum,
maximum,
exclusiveMinimum,
exclusiveMaximum,
multipleOf);
}
///
/// 根据已读取的字符串关键字创建字符串约束对象。
/// 正则会在 schema 解析阶段预编译,字符串 format 也会先归一化成共享枚举,
/// 避免每次校验都重新解释 schema 原文。
///
/// 最小长度约束。
/// 最大长度约束。
/// 正则模式约束。
/// 字符串 format 约束。
/// 字符串约束对象;未声明任何字符串约束时返回空。
private static YamlConfigStringConstraints? CreateStringScalarConstraints(
int? minLength,
int? maxLength,
string? pattern,
YamlConfigStringFormatConstraint? formatConstraint)
{
return !minLength.HasValue &&
!maxLength.HasValue &&
pattern is null &&
formatConstraint is null
? null
: new YamlConfigStringConstraints(
minLength,
maxLength,
pattern,
pattern is null
? null
: new Regex(
pattern,
SupportedPatternRegexOptions,
SupportedFormatRegexTimeout),
formatConstraint);
}
///
/// 校验数值标量的区间与步进约束。
/// 该方法把解析失败、闭区间、开区间和步进诊断集中到数值路径,避免主调度方法继续增长。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 原始 YAML 标量值。
/// 归一化后的比较值。
/// 标量 schema 节点。
/// 数值约束对象。
private static void ValidateNumericScalarConstraints(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
string normalizedValue,
YamlConfigSchemaNode schemaNode,
YamlConfigNumericConstraints? constraints)
{
if (constraints is null)
{
return;
}
var numericValue = ParseComparableNumericValue(
tableName,
yamlPath,
displayPath,
rawValue,
normalizedValue,
schemaNode);
ValidateNumericLowerBounds(
tableName,
yamlPath,
displayPath,
rawValue,
schemaNode,
constraints,
numericValue);
ValidateNumericUpperBounds(
tableName,
yamlPath,
displayPath,
rawValue,
schemaNode,
constraints,
numericValue);
ValidateNumericMultipleOfConstraint(
tableName,
yamlPath,
displayPath,
rawValue,
normalizedValue,
schemaNode,
constraints,
numericValue);
}
///
/// 校验数值的最小值与开区间下界。
///
private static void ValidateNumericLowerBounds(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
YamlConfigSchemaNode schemaNode,
YamlConfigNumericConstraints constraints,
double numericValue)
{
if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be greater than or equal to {constraints.Minimum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Minimum allowed value: {constraints.Minimum.Value.ToString(CultureInfo.InvariantCulture)}.");
}
if (constraints.ExclusiveMinimum.HasValue && numericValue <= constraints.ExclusiveMinimum.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be greater than {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail:
$"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}.");
}
}
///
/// 校验数值的最大值与开区间上界。
///
private static void ValidateNumericUpperBounds(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
YamlConfigSchemaNode schemaNode,
YamlConfigNumericConstraints constraints,
double numericValue)
{
if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be less than or equal to {constraints.Maximum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Maximum allowed value: {constraints.Maximum.Value.ToString(CultureInfo.InvariantCulture)}.");
}
if (constraints.ExclusiveMaximum.HasValue && numericValue >= constraints.ExclusiveMaximum.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be less than {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail:
$"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
}
}
///
/// 校验数值是否满足 multipleOf 步进约束。
///
private static void ValidateNumericMultipleOfConstraint(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
string normalizedValue,
YamlConfigSchemaNode schemaNode,
YamlConfigNumericConstraints constraints,
double numericValue)
{
if (!constraints.MultipleOf.HasValue ||
IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value))
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail:
$"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}.");
}
///
/// 将归一化后的数值文本还原为双精度值,用于统一后续区间比较。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 原始 YAML 标量值。
/// 归一化后的比较值。
/// 标量 schema 节点。
/// 可比较的双精度值。
private static double ParseComparableNumericValue(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
string normalizedValue,
YamlConfigSchemaNode schemaNode)
{
if (double.TryParse(
normalizedValue,
NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture,
out var numericValue))
{
return numericValue;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.UnexpectedFailure,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' could not be normalized into a comparable numeric value.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue);
}
///
/// 校验字符串标量的长度、模式与 format 约束。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 原始 YAML 标量值。
/// 标量 schema 节点。
/// 字符串约束对象。
private static void ValidateStringScalarConstraints(
string tableName,
string yamlPath,
string displayPath,
string rawValue,
YamlConfigSchemaNode schemaNode,
YamlConfigStringConstraints? constraints)
{
if (constraints is null)
{
return;
}
var stringLength = rawValue.Length;
if (constraints.MinLength.HasValue && stringLength < constraints.MinLength.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be at least {constraints.MinLength.Value} characters long, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Minimum length: {constraints.MinLength.Value}.");
}
if (constraints.MaxLength.HasValue && stringLength > constraints.MaxLength.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must be at most {constraints.MaxLength.Value} characters long, but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Maximum length: {constraints.MaxLength.Value}.");
}
if (constraints.PatternRegex is not null &&
!constraints.PatternRegex.IsMatch(rawValue))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must match regular expression '{constraints.Pattern}', but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Expected pattern: {constraints.Pattern}.");
}
if (constraints.FormatConstraint is not null &&
!MatchesSupportedStringFormat(rawValue, constraints.FormatConstraint.Kind))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must satisfy string format '{constraints.FormatConstraint.SchemaName}', but the current YAML scalar value is '{rawValue}'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Expected string format: {constraints.FormatConstraint.SchemaName}.");
}
}
///
/// 判断一个字符串是否满足当前共享支持的 format 子集。
/// 这里只接受 Runtime / Generator / Tooling 三端都能稳定解释的格式,
/// 避免把 JSON Schema 的开放格式名直接当成“全部支持”。
///
/// 待校验的字符串值。
/// 共享 format 枚举。
/// 当前字符串是否满足指定 format。
private static bool MatchesSupportedStringFormat(
string value,
YamlConfigStringFormatKind formatKind)
{
ArgumentNullException.ThrowIfNull(value);
return formatKind switch
{
YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value),
YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value),
YamlConfigStringFormatKind.Duration => MatchesSupportedDurationFormat(value),
YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value),
YamlConfigStringFormatKind.Time => MatchesSupportedTimeFormat(value),
YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value),
YamlConfigStringFormatKind.Uuid => Guid.TryParseExact(value, "D", out _),
_ => false
};
}
///
/// 判断字符串是否满足共享支持的 date 格式。
/// 这里固定采用 yyyy-MM-dd,避免文化区域或宽松解析导致工具与运行时漂移。
///
/// 待校验的字符串值。
/// 当前值是否是合法日期。
private static bool MatchesSupportedDateFormat(string value)
{
return SupportedDateFormatRegex.IsMatch(value) &&
DateOnly.TryParseExact(
value,
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out _);
}
///
/// 判断字符串是否满足共享支持的 date-time 格式。
/// 该格式固定要求显式时区偏移,避免消费者误把本地时区文本当成跨进程稳定配置。
///
/// 待校验的字符串值。
/// 当前值是否是合法时间戳。
private static bool MatchesSupportedDateTimeFormat(string value)
{
if (!SupportedDateTimeFormatRegex.IsMatch(value))
{
return false;
}
return DateTimeOffset.TryParseExact(
value,
["yyyy'-'MM'-'dd'T'HH':'mm':'ssK", "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"],
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out _);
}
///
/// 判断字符串是否满足共享支持的 duration 格式。
/// 当前共享子集只接受 day-time duration:可声明 D/H/M/S,秒允许小数,
/// 但拒绝 Y / M(月) / W 等依赖日历语义的部分,
/// 避免不同宿主对“一个月/一年到底多长”出现解释漂移。
///
/// 待校验的字符串值。
/// 当前值是否是合法持续时间文本。
private static bool MatchesSupportedDurationFormat(string value)
{
var match = SupportedDurationFormatRegex.Match(value);
if (!match.Success)
{
return false;
}
var hasDayComponent = match.Groups["days"].Success;
var hasHourComponent = match.Groups["hours"].Success;
var hasMinuteComponent = match.Groups["minutes"].Success;
var hasSecondComponent = match.Groups["seconds"].Success;
var hasAnyComponent = hasDayComponent || hasHourComponent || hasMinuteComponent || hasSecondComponent;
if (!hasAnyComponent)
{
return false;
}
var hasTimeSection = value.Contains('T', StringComparison.Ordinal);
if (hasTimeSection &&
!hasHourComponent &&
!hasMinuteComponent &&
!hasSecondComponent)
{
return false;
}
return true;
}
///
/// 判断字符串是否满足共享支持的 time 格式。
/// 该格式固定要求显式时区偏移,并只接受 24 小时制的合法时分秒文本,
/// 避免不同宿主对“time-only + offset”解析默认日期或时区时产生漂移。
///
/// 待校验的字符串值。
/// 当前值是否是合法时间文本。
private static bool MatchesSupportedTimeFormat(string value)
{
var match = SupportedTimeFormatRegex.Match(value);
if (!match.Success)
{
return false;
}
var hour = int.Parse(match.Groups["hour"].Value, CultureInfo.InvariantCulture);
var minute = int.Parse(match.Groups["minute"].Value, CultureInfo.InvariantCulture);
var second = int.Parse(match.Groups["second"].Value, CultureInfo.InvariantCulture);
if (hour > 23 || minute > 59 || second > 59)
{
return false;
}
var offset = match.Groups["offset"].Value;
if (string.Equals(offset, "Z", StringComparison.Ordinal))
{
return true;
}
var offsetHour = int.Parse(offset.AsSpan(1, 2), CultureInfo.InvariantCulture);
var offsetMinute = int.Parse(offset.AsSpan(4, 2), CultureInfo.InvariantCulture);
return offsetHour <= 23 && offsetMinute <= 59;
}
///
/// 判断字符串是否满足共享支持的 uri 格式。
/// 这里要求输入显式包含 scheme,避免把普通路径意外解释成平台相关的绝对 URI。
///
/// 待校验的字符串值。
/// 当前值是否是合法绝对 URI。
private static bool MatchesSupportedUriFormat(string value)
{
return SupportedUriSchemeRegex.IsMatch(value) &&
Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
uri.IsAbsoluteUri;
}
///
/// 校验数组值是否满足元素数量约束。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 当前数组元素数量。
/// 数组 schema 节点。
private static void ValidateArrayConstraints(
string tableName,
string yamlPath,
string displayPath,
int itemCount,
YamlConfigSchemaNode schemaNode)
{
var constraints = schemaNode.ArrayConstraints;
if (constraints is null)
{
return;
}
if (constraints.MinItems.HasValue && itemCount < constraints.MinItems.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must contain at least {constraints.MinItems.Value} items, but the current YAML sequence contains {itemCount}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
detail: $"Minimum item count: {constraints.MinItems.Value}.");
}
if (constraints.MaxItems.HasValue && itemCount > constraints.MaxItems.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must contain at most {constraints.MaxItems.Value} items, but the current YAML sequence contains {itemCount}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
detail: $"Maximum item count: {constraints.MaxItems.Value}.");
}
}
///
/// 校验数组是否满足去重约束。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 实际数组节点。
/// 数组 schema 节点。
private static void ValidateArrayUniqueItemsConstraint(
string tableName,
string yamlPath,
string displayPath,
YamlSequenceNode sequenceNode,
YamlConfigSchemaNode schemaNode)
{
var constraints = schemaNode.ArrayConstraints;
if (constraints is null ||
!constraints.UniqueItems ||
schemaNode.ItemNode is null)
{
return;
}
// The canonical item key uses schema-aware normalization so object key order,
// scalar quoting, and numeric formatting do not accidentally bypass uniqueItems.
Dictionary seenItems = new(StringComparer.Ordinal);
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
{
var itemNode = sequenceNode.Children[itemIndex];
var comparableValue = BuildComparableNodeValue(itemNode, schemaNode.ItemNode);
if (seenItems.TryGetValue(comparableValue, out var existingIndex))
{
var itemPath = $"{displayPath}[{itemIndex}]";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' requires unique array items, but item '{itemPath}' duplicates '{displayPath}[{existingIndex}]'.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: itemPath,
rawValue: DescribeYamlNodeForDiagnostics(itemNode, schemaNode.ItemNode),
detail: "The schema declares uniqueItems = true.");
}
seenItems.Add(comparableValue, itemIndex);
}
}
///
/// 校验数组是否满足 contains 声明的匹配数量边界。
/// 该实现会对每个数组项复用同一套递归校验逻辑做“非抛出式匹配”,避免 contains 与主校验链各自维护不同的 schema 解释规则。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 字段路径。
/// 实际数组节点。
/// 数组 schema 节点。
/// 匹配成功的 contains 子树所声明的跨表引用收集器。
private static void ValidateArrayContainsConstraints(
string tableName,
string yamlPath,
string displayPath,
YamlSequenceNode sequenceNode,
YamlConfigSchemaNode schemaNode,
ICollection? references)
{
var containsConstraints = schemaNode.ArrayConstraints?.ContainsConstraints;
if (containsConstraints is null)
{
return;
}
var matchingCount = CountMatchingContainsItems(
tableName,
yamlPath,
displayPath,
sequenceNode,
containsConstraints.ContainsNode,
references);
var rawValue = matchingCount.ToString(CultureInfo.InvariantCulture);
var requiredMinContains = containsConstraints.MinContains ?? 1;
if (matchingCount < requiredMinContains)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must contain at least {requiredMinContains} items matching the 'contains' schema, but the current YAML sequence contains {matchingCount}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Minimum matching contains count: {requiredMinContains}.");
}
if (containsConstraints.MaxContains.HasValue &&
matchingCount > containsConstraints.MaxContains.Value)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"Property '{displayPath}' in config file '{yamlPath}' must contain at most {containsConstraints.MaxContains.Value} items matching the 'contains' schema, but the current YAML sequence contains {matchingCount}.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: rawValue,
detail: $"Maximum matching contains count: {containsConstraints.MaxContains.Value}.");
}
}
///
/// 统计当前数组中有多少元素满足 contains 子 schema。
/// 非预期内部错误会继续抛出,只有正常的 schema 不匹配才会被当成“当前元素不计数”。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 数组字段路径。
/// 实际数组节点。
/// contains 子 schema。
/// 匹配成功元素的可选跨表引用收集器。
/// 匹配 contains 子 schema 的元素数量。
private static int CountMatchingContainsItems(
string tableName,
string yamlPath,
string displayPath,
YamlSequenceNode sequenceNode,
YamlConfigSchemaNode containsNode,
ICollection? references)
{
var matchingCount = 0;
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
{
if (TryMatchSchemaNode(
tableName,
yamlPath,
$"{displayPath}[{itemIndex}]",
sequenceNode.Children[itemIndex],
containsNode,
references,
allowUnknownObjectProperties: true))
{
matchingCount++;
}
}
return matchingCount;
}
///
/// 判断当前 YAML 节点是否满足给定 schema 子树。
/// contains / not 都通过该路径复用主校验逻辑,因此普通约束失败会返回 ,
/// 但内部意外状态仍会继续抛出。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 当前节点路径。
/// 实际 YAML 节点。
/// 要试匹配的 schema 子树。
/// 当前节点匹配成功后要写回的可选跨表引用收集器。
/// 对象试匹配时是否允许额外字段。
/// 当前节点是否匹配指定 schema 子树。
private static bool TryMatchSchemaNode(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode,
ICollection? references,
bool allowUnknownObjectProperties)
{
// 约束试匹配不能把失败路径的引用泄漏给外层,但匹配成功的分支仍需要把引用写回,
// 这样 contains / not 等内联 schema 才能与主校验链复用同一套递归解释规则。
List? matchedReferences = references is null ? null : new();
try
{
ValidateNode(
tableName,
yamlPath,
displayPath,
node,
schemaNode,
matchedReferences,
allowUnknownObjectProperties);
if (references is not null &&
matchedReferences is not null)
{
AddUniqueReferenceUsages(references, matchedReferences);
}
return true;
}
catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind !=
ConfigLoadFailureKind.UnexpectedFailure)
{
return false;
}
}
///
/// 将试匹配分支采集到的引用回写到外层集合,并按结构化标识去重。
///
/// 外层引用集合。
/// 当前成功匹配分支采集到的引用。
private static void AddUniqueReferenceUsages(
ICollection references,
IEnumerable matchedReferences)
{
foreach (var referenceUsage in matchedReferences)
{
if (!ContainsReferenceUsage(references, referenceUsage))
{
references.Add(referenceUsage);
}
}
}
///
/// 判断外层引用集合中是否已经存在同一条引用使用记录。
///
/// 要检查的引用集合。
/// 当前待合并的引用记录。
/// 当集合中已存在语义相同的记录时返回 。
private static bool ContainsReferenceUsage(
IEnumerable references,
YamlConfigReferenceUsage candidate)
{
foreach (var referenceUsage in references)
{
if (string.Equals(referenceUsage.YamlPath, candidate.YamlPath, StringComparison.Ordinal) &&
string.Equals(referenceUsage.SchemaPath, candidate.SchemaPath, StringComparison.Ordinal) &&
string.Equals(referenceUsage.PropertyPath, candidate.PropertyPath, StringComparison.Ordinal) &&
string.Equals(referenceUsage.RawValue, candidate.RawValue, StringComparison.Ordinal) &&
string.Equals(referenceUsage.ReferencedTableName, candidate.ReferencedTableName, StringComparison.Ordinal) &&
referenceUsage.ValueType == candidate.ValueType)
{
return true;
}
}
return false;
}
///
/// 校验节点是否命中了 not 声明的禁用 schema。
/// 与 contains 不同,not 会沿用主校验链的严格对象语义,避免把“声明属性子集”误当成完整命中。
///
/// 所属配置表名称。
/// YAML 文件路径。
/// 当前字段路径。
/// 当前 YAML 节点。
/// 当前 schema 节点。
private static void ValidateNegatedSchemaConstraint(
string tableName,
string yamlPath,
string displayPath,
YamlNode node,
YamlConfigSchemaNode schemaNode)
{
if (schemaNode.NegatedSchemaNode is null ||
!TryMatchSchemaNode(
tableName,
yamlPath,
displayPath,
node,
schemaNode.NegatedSchemaNode,
references: null,
allowUnknownObjectProperties: false))
{
return;
}
var subject = string.IsNullOrWhiteSpace(displayPath)
? "Root object"
: $"Property '{displayPath}'";
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConstraintViolation,
tableName,
$"{subject} in config file '{yamlPath}' must not match the 'not' schema.",
yamlPath: yamlPath,
schemaPath: schemaNode.SchemaPathHint,
displayPath: GetDiagnosticPath(displayPath),
rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode.NegatedSchemaNode),
detail: "The current YAML value matches the forbidden 'not' schema.");
}
///
/// 将一个已通过结构校验的 YAML 节点归一化为可比较字符串。
/// 该键同时服务于 uniqueItems 与 const,
/// 因此要忽略对象字段顺序和字符串引号形式。
///
/// YAML 节点。
/// 对应 schema 节点。
/// 可稳定比较的归一化键。
private static string BuildComparableNodeValue(YamlNode node, YamlConfigSchemaNode schemaNode)
{
return schemaNode.NodeType switch
{
YamlConfigSchemaPropertyType.Object => BuildComparableObjectValue(node, schemaNode),
YamlConfigSchemaPropertyType.Array => BuildComparableArrayValue(node, schemaNode),
YamlConfigSchemaPropertyType.Integer => BuildComparableScalarValue(node, schemaNode),
YamlConfigSchemaPropertyType.Number => BuildComparableScalarValue(node, schemaNode),
YamlConfigSchemaPropertyType.Boolean => BuildComparableScalarValue(node, schemaNode),
YamlConfigSchemaPropertyType.String => BuildComparableScalarValue(node, schemaNode),
_ => throw new InvalidOperationException($"Unsupported schema node type '{schemaNode.NodeType}'.")
};
}
///
/// 构建对象节点的可比较键。
/// 对象字段会先按属性名排序,避免 YAML 原始字段顺序影响 uniqueItems 的等价关系。
///
/// YAML 节点。
/// 对象 schema 节点。
/// 对象节点的稳定比较键。
private static string BuildComparableObjectValue(YamlNode node, YamlConfigSchemaNode schemaNode)
{
if (node is not YamlMappingNode mappingNode)
{
throw new InvalidOperationException("Validated object nodes must be YAML mappings.");
}
var properties = schemaNode.Properties
?? throw new InvalidOperationException(
"Validated object nodes must expose declared properties.");
var objectEntries = new List>(mappingNode.Children.Count);
foreach (var entry in mappingNode.Children)
{
if (entry.Key is not YamlScalarNode keyNode ||
keyNode.Value is null ||
!properties.TryGetValue(keyNode.Value, out var propertySchema))
{
throw new InvalidOperationException("Validated object nodes must use declared scalar property names.");
}
objectEntries.Add(
new KeyValuePair(
keyNode.Value,
BuildComparableNodeValue(entry.Value, propertySchema)));
}
objectEntries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
return string.Join(
"|",
objectEntries.Select(static entry =>
$"{entry.Key.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Key}={entry.Value.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Value}"));
}
///
/// 构建数组节点的可比较键。
/// 数组仍保留元素顺序,因为 uniqueItems 只忽略对象字段顺序,不忽略数组顺序。
///
/// YAML 节点。
/// 数组 schema 节点。
/// 数组节点的稳定比较键。
private static string BuildComparableArrayValue(YamlNode node, YamlConfigSchemaNode schemaNode)
{
if (node is not YamlSequenceNode sequenceNode ||
schemaNode.ItemNode is null)
{
throw new InvalidOperationException("Validated array nodes must be YAML sequences with item schema.");
}
return "[" +
string.Join(
",",
sequenceNode.Children.Select(item =>
{
var comparableValue = BuildComparableNodeValue(item, schemaNode.ItemNode);
return $"{comparableValue.Length.ToString(CultureInfo.InvariantCulture)}:{comparableValue}";
})) +
"]";
}
///
/// 构建标量节点的可比较键。
/// 标量会沿用与 enum / 引用校验一致的归一化规则,避免数字格式和引号形式导致伪差异。
///
/// YAML 节点。
/// 标量 schema 节点。
/// 标量节点的稳定比较键。
private static string BuildComparableScalarValue(YamlNode node, YamlConfigSchemaNode schemaNode)
{
if (node is not YamlScalarNode scalarNode ||
scalarNode.Value is null)
{
throw new InvalidOperationException("Validated scalar nodes must be YAML scalars.");
}
var normalizedScalar = NormalizeScalarValue(schemaNode.NodeType, scalarNode.Value);
return
$"{schemaNode.NodeType}:{normalizedScalar.Length.ToString(CultureInfo.InvariantCulture)}:{normalizedScalar}";
}
///
/// 为唯一性诊断提取一个可读的节点摘要。
///
/// YAML 节点。
/// 对应 schema 节点。
/// 诊断摘要。
private static string DescribeYamlNodeForDiagnostics(YamlNode node, YamlConfigSchemaNode schemaNode)
{
return schemaNode.NodeType switch
{
YamlConfigSchemaPropertyType.Object => "{...}",
YamlConfigSchemaPropertyType.Array => "[...]",
_ when node is YamlScalarNode scalarNode => scalarNode.Value ?? string.Empty,
_ => node.GetType().Name
};
}
///
/// 判断数值是否满足 multipleOf。
/// 优先按十进制字面量做精确整倍数判断,
/// 以同时避免 0.1 / 0.01 这类十进制步进的伪失败和大数量级非整倍数的伪通过;
/// 只有当值超出精确十进制路径时才退回双精度容差比较。
///
/// 用于数值比较的规范化 YAML 标量文本。
/// 当前值。
/// 步进约束。
/// 是否满足整倍数关系。
private static bool IsMultipleOf(string normalizedValue, double value, double divisor)
{
if (TryIsExactDecimalMultiple(normalizedValue, divisor, out var exactResult))
{
return exactResult;
}
var quotient = value / divisor;
var nearestInteger = Math.Round(quotient);
var tolerance = 1e-9 * Math.Max(1d, Math.Abs(quotient));
return Math.Abs(quotient - nearestInteger) <= tolerance;
}
///
/// 尝试按十进制字面量精确判断 multipleOf。
/// 该路径直接对齐 YAML / JSON 中常见的有限十进制写法,
/// 避免双精度舍入把明显的非整倍数误判为合法。
///
/// 规范化后的 YAML 数值文本。
/// Schema 声明的步进约束。
/// 精确路径下的判断结果。
/// 是否成功进入精确十进制判断路径。
private static bool TryIsExactDecimalMultiple(string valueText, double divisor, out bool isMultiple)
{
var divisorText = divisor.ToString("R", CultureInfo.InvariantCulture);
if (!TryParseExactDecimal(valueText, out var valueSignificand, out var valueScale) ||
!TryParseExactDecimal(divisorText, out var divisorSignificand, out var divisorScale) ||
divisorSignificand.IsZero)
{
isMultiple = false;
return false;
}
var commonScale = Math.Max(valueScale, divisorScale);
var scaledValue = ScaleDecimalSignificand(valueSignificand, valueScale, commonScale);
var scaledDivisor = ScaleDecimalSignificand(divisorSignificand, divisorScale, commonScale);
isMultiple = scaledValue % scaledDivisor == BigInteger.Zero;
return true;
}
///
/// 将有限十进制或科学计数法文本拆成“整数有效数字 + 十进制位数”形式。
/// 这样可以把整倍数判断转成同一尺度下的整数取模,避免浮点误差参与计算。
///
/// 待解析的数值文本。
/// 去掉小数点后的有效数字。
/// 十进制缩放位数;原值等于 / 10^。
/// 是否成功解析为有限十进制数。
private static bool TryParseExactDecimal(string text, out BigInteger significand, out int scale)
{
var match = ExactDecimalPattern.Match(text);
if (!match.Success)
{
significand = BigInteger.Zero;
scale = 0;
return false;
}
var exponentGroup = match.Groups["exponent"].Value;
var exponent = 0;
if (!string.IsNullOrEmpty(exponentGroup) &&
!int.TryParse(exponentGroup, NumberStyles.Integer, CultureInfo.InvariantCulture, out exponent))
{
significand = BigInteger.Zero;
scale = 0;
return false;
}
var integerDigits = match.Groups["integer"].Value;
var fractionDigits = match.Groups["fraction"].Success
? match.Groups["fraction"].Value
: match.Groups["fractionOnly"].Value;
var digits = string.Concat(integerDigits, fractionDigits);
if (digits.Length == 0)
{
digits = "0";
}
digits = digits.TrimStart('0');
if (digits.Length == 0)
{
significand = BigInteger.Zero;
scale = 0;
return true;
}
scale = checked(fractionDigits.Length - exponent);
if (scale < 0)
{
digits = string.Concat(digits, new string('0', -scale));
scale = 0;
}
while (scale > 0 && digits[^1] == '0')
{
digits = digits[..^1];
scale--;
}
significand = BigInteger.Parse(digits, CultureInfo.InvariantCulture);
if (string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal))
{
significand = BigInteger.Negate(significand);
}
return true;
}
///
/// 将十进制有效数字放大到目标尺度,便于在同一量纲下执行整数取模。
///
/// 原始有效数字。
/// 当前十进制位数。
/// 目标十进制位数。
/// 放大到目标尺度后的有效数字。
private static BigInteger ScaleDecimalSignificand(BigInteger significand, int currentScale, int targetScale)
{
if (currentScale == targetScale)
{
return significand;
}
return significand * BigInteger.Pow(10, targetScale - currentScale);
}
///
/// 解析跨表引用目标表名称。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// Schema 节点。
/// 目标表名称;未声明时返回空。
private static string? TryGetReferenceTableName(
string tableName,
string schemaPath,
string propertyPath,
JsonElement element)
{
if (!element.TryGetProperty("x-gframework-ref-table", out var referenceTableElement))
{
return null;
}
if (referenceTableElement.ValueKind != JsonValueKind.String)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'x-gframework-ref-table' value.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
var referenceTableName = referenceTableElement.GetString();
if (string.IsNullOrWhiteSpace(referenceTableName))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a non-empty 'x-gframework-ref-table' value.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
return referenceTableName;
}
///
/// 验证哪些 schema 类型允许声明跨表引用。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 字段类型。
/// 目标表名称。
private static void EnsureReferenceKeywordIsSupported(
string tableName,
string schemaPath,
string propertyPath,
YamlConfigSchemaPropertyType propertyType,
string? referenceTableName)
{
if (referenceTableName == null)
{
return;
}
if (propertyType == YamlConfigSchemaPropertyType.String ||
propertyType == YamlConfigSchemaPropertyType.Integer)
{
return;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath),
referencedTableName: referenceTableName);
}
///
/// 递归收集 schema 中声明的目标表名称。
///
/// 当前 schema 节点。
/// 输出集合。
private static void CollectReferencedTableNames(
YamlConfigSchemaNode node,
ISet referencedTableNames)
{
if (node.ReferenceTableName != null)
{
referencedTableNames.Add(node.ReferenceTableName);
}
if (node.Properties is not null)
{
foreach (var property in node.Properties.Values)
{
CollectReferencedTableNames(property, referencedTableNames);
}
}
if (node.ItemNode is not null)
{
CollectReferencedTableNames(node.ItemNode, referencedTableNames);
}
var containsNode = node.ArrayConstraints?.ContainsConstraints?.ContainsNode;
if (containsNode is not null)
{
CollectReferencedTableNames(containsNode, referencedTableNames);
}
var dependentSchemas = node.ObjectConstraints?.DependentSchemas;
if (dependentSchemas is not null)
{
foreach (var dependentSchemaNode in dependentSchemas.Values)
{
CollectReferencedTableNames(dependentSchemaNode, referencedTableNames);
}
}
var allOfSchemas = node.ObjectConstraints?.AllOfSchemas;
if (allOfSchemas is not null)
{
foreach (var allOfSchemaNode in allOfSchemas)
{
CollectReferencedTableNames(allOfSchemaNode, referencedTableNames);
}
}
var conditionalSchemas = node.ObjectConstraints?.ConditionalSchemas;
if (conditionalSchemas is not null)
{
CollectReferencedTableNames(conditionalSchemas.IfSchema, referencedTableNames);
if (conditionalSchemas.ThenSchema is not null)
{
CollectReferencedTableNames(conditionalSchemas.ThenSchema, referencedTableNames);
}
if (conditionalSchemas.ElseSchema is not null)
{
CollectReferencedTableNames(conditionalSchemas.ElseSchema, referencedTableNames);
}
}
}
///
/// 将 schema 关键字中的标量值归一化到运行时比较字符串。
///
/// 所属配置表名称。
/// Schema 文件路径。
/// 字段路径。
/// 关键字名称。
/// 期望的标量类型。
/// 当前关键字值节点。
/// 归一化后的字符串值。
private static string NormalizeKeywordScalarValue(
string tableName,
string schemaPath,
string propertyPath,
string keywordName,
YamlConfigSchemaPropertyType expectedType,
JsonElement item)
{
try
{
return expectedType switch
{
YamlConfigSchemaPropertyType.String when item.ValueKind == JsonValueKind.String =>
item.GetString() ?? string.Empty,
YamlConfigSchemaPropertyType.Integer when item.ValueKind == JsonValueKind.Number =>
item.GetInt64().ToString(CultureInfo.InvariantCulture),
YamlConfigSchemaPropertyType.Number when item.ValueKind == JsonValueKind.Number =>
item.GetDouble().ToString(CultureInfo.InvariantCulture),
YamlConfigSchemaPropertyType.Boolean when item.ValueKind == JsonValueKind.True =>
bool.TrueString.ToLowerInvariant(),
YamlConfigSchemaPropertyType.Boolean when item.ValueKind == JsonValueKind.False =>
bool.FalseString.ToLowerInvariant(),
_ => throw new InvalidOperationException()
};
}
catch
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.SchemaUnsupported,
tableName,
$"Property '{propertyPath}' in schema file '{schemaPath}' contains a '{keywordName}' value that is incompatible with schema type '{GetTypeName(expectedType)}'.",
schemaPath: schemaPath,
displayPath: GetDiagnosticPath(propertyPath));
}
}
///
/// 将内部路径转换为适合放入诊断对象的可选字段路径。
///
/// 内部使用的属性路径。
/// 可用于诊断的路径;根节点时返回空。
private static string? GetDiagnosticPath(string path)
{
return string.IsNullOrWhiteSpace(path) || string.Equals(path, "", StringComparison.Ordinal)
? null
: path;
}
///
/// 将 YAML 标量值规范化成运行时比较格式。
///
/// 期望的标量类型。
/// 原始字符串值。
/// 归一化后的字符串。
private static string NormalizeScalarValue(YamlConfigSchemaPropertyType expectedType, string value)
{
return expectedType switch
{
YamlConfigSchemaPropertyType.String => value,
YamlConfigSchemaPropertyType.Integer when long.TryParse(
value,
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out var integerValue) =>
integerValue.ToString(CultureInfo.InvariantCulture),
YamlConfigSchemaPropertyType.Number when double.TryParse(
value,
NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture,
out var numberValue) =>
numberValue.ToString(CultureInfo.InvariantCulture),
YamlConfigSchemaPropertyType.Boolean when bool.TryParse(value, out var booleanValue) =>
booleanValue.ToString().ToLowerInvariant(),
YamlConfigSchemaPropertyType.Integer =>
throw new InvalidOperationException($"Value '{value}' cannot be normalized as integer."),
YamlConfigSchemaPropertyType.Number =>
throw new InvalidOperationException($"Value '{value}' cannot be normalized as number."),
YamlConfigSchemaPropertyType.Boolean =>
throw new InvalidOperationException($"Value '{value}' cannot be normalized as boolean."),
_ =>
throw new InvalidOperationException(
$"Schema node type '{expectedType}' cannot be normalized as a scalar value.")
};
}
///
/// 获取 schema 类型的可读名称,用于错误信息。
///
/// Schema 节点类型。
/// 可读类型名。
private static string GetTypeName(YamlConfigSchemaPropertyType type)
{
return type switch
{
YamlConfigSchemaPropertyType.Integer => "integer",
YamlConfigSchemaPropertyType.Number => "number",
YamlConfigSchemaPropertyType.Boolean => "boolean",
YamlConfigSchemaPropertyType.String => "string",
YamlConfigSchemaPropertyType.Array => "array",
YamlConfigSchemaPropertyType.Object => "object",
_ => type.ToString()
};
}
///
/// 组合 schema 中的逻辑路径,便于诊断时指出深层字段。
///
/// 父级路径。
/// 当前属性名。
/// 组合后的路径。
private static string CombineSchemaPath(string parentPath, string propertyName)
{
return string.Equals(parentPath, "", StringComparison.Ordinal)
? propertyName
: $"{parentPath}.{propertyName}";
}
///
/// 组合 YAML 诊断展示路径。
///
/// 父级路径。
/// 当前属性名。
/// 组合后的路径。
private static string CombineDisplayPath(string parentPath, string propertyName)
{
return string.IsNullOrWhiteSpace(parentPath) ? propertyName : $"{parentPath}.{propertyName}";
}
///
/// 返回当前节点上声明的“仅对象可用”关键字名称。
///
/// Schema 节点。
/// 命中的关键字名称;未命中时返回空。
private static string? TryGetObjectOnlyKeywordName(JsonElement element)
{
if (element.TryGetProperty("allOf", out _))
{
return "allOf";
}
if (element.TryGetProperty("if", out _))
{
return "if";
}
if (element.TryGetProperty("then", out _))
{
return "then";
}
return element.TryGetProperty("else", out _)
? "else"
: null;
}
///
/// 判断当前标量是否应按字符串处理。
/// 这里显式排除 YAML 的数字、布尔和 null 标签,避免未加引号的值被当成字符串混入运行时。
///
/// YAML 标量标签。
/// 是否为字符串标量。
private static bool IsStringScalar(string tag)
{
if (string.IsNullOrWhiteSpace(tag))
{
return true;
}
return !string.Equals(tag, "tag:yaml.org,2002:int", StringComparison.Ordinal) &&
!string.Equals(tag, "tag:yaml.org,2002:float", StringComparison.Ordinal) &&
!string.Equals(tag, "tag:yaml.org,2002:bool", StringComparison.Ordinal) &&
!string.Equals(tag, "tag:yaml.org,2002:null", StringComparison.Ordinal);
}
}