using System.Numerics; using System.Text.RegularExpressions; using GFramework.Game.Abstractions.Config; namespace GFramework.Game.Config; /// /// 提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。 /// 该校验器与当前配置生成器、VS Code 工具支持的 schema 子集保持一致, /// 并通过递归遍历方式覆盖嵌套对象、对象数组、标量数组与深层 enum / 引用约束。 /// 当前共享子集额外支持 multipleOfuniqueItems、 /// contains / minContains / maxContains、 /// minPropertiesmaxProperties 与稳定字符串 format 子集, /// 让数值步进、数组去重、数组匹配计数和对象属性数量规则在运行时与生成器 / 工具侧保持一致。 /// internal static 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 Regex ExactDecimalPattern = new( @"^(?[+-]?)(?:(?\d+)(?:\.(?\d*))?|\.(?\d+))(?:[eE](?[+-]?\d+))?$", RegexOptions.CultureInvariant | RegexOptions.Compiled); private static readonly Regex SupportedEmailFormatRegex = new( @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.CultureInvariant | RegexOptions.Compiled); private static readonly Regex SupportedDateFormatRegex = new( @"^(?\d{4})-(?\d{2})-(?\d{2})$", RegexOptions.CultureInvariant | RegexOptions.Compiled); 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); private static readonly Regex SupportedDurationFormatRegex = new( @"^P(?:(?\d+)D)?(?:T(?:(?\d+)H)?(?:(?\d+)M)?(?:(?\d+(?:\.\d+)?)S)?)?$", RegexOptions.CultureInvariant | RegexOptions.Compiled); private static readonly Regex SupportedTimeFormatRegex = new( @"^(?\d{2}):(?\d{2}):(?\d{2})(?\.\d+)?(?Z|[+-]\d{2}:\d{2})$", RegexOptions.CultureInvariant | RegexOptions.Compiled); private static readonly Regex SupportedUriSchemeRegex = new( @"^[A-Za-z][A-Za-z0-9+\.-]*:", RegexOptions.CultureInvariant | RegexOptions.Compiled); /// /// 从磁盘加载并解析一个 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) { 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)); } var typeName = typeElement.GetString() ?? string.Empty; var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element); var parsedNode = 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) }; return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element)); } /// /// 解析对象类型 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), 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 这类内联子 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) { if (node is not YamlMappingNode 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)); } 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); } 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); } if (schemaNode.ObjectConstraints is not null) { ValidateObjectConstraints(tableName, yamlPath, displayPath, seenProperties.Count, schemaNode); } ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode); ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode); ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode); } /// /// 校验对象节点声明的属性数量约束。 /// /// 所属配置表名称。 /// YAML 文件路径。 /// 对象字段路径;根对象时为空。 /// 当前对象实际属性数量。 /// 对象 schema 节点。 private static void ValidateObjectConstraints( string tableName, string yamlPath, string displayPath, int propertyCount, YamlConfigSchemaNode schemaNode) { var constraints = schemaNode.ObjectConstraints; if (constraints is null) { return; } var subject = string.IsNullOrWhiteSpace(displayPath) ? "Root object" : $"Property '{displayPath}'"; 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)}."); } } /// /// 校验数组节点,并递归验证每个元素。 /// /// 所属配置表名称。 /// 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) { if (node is not YamlScalarNode 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)); } var value = scalarNode.Value; if (value is null) { 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)); } 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) { 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); } 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); if (schemaNode.ReferenceTableName != null && references is not null) { 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 YamlConfigObjectConstraints? ParseObjectConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element) { var minProperties = TryParseObjectPropertyCountConstraint( tableName, schemaPath, propertyPath, element, "minProperties"); var maxProperties = TryParseObjectPropertyCountConstraint( tableName, schemaPath, propertyPath, element, "maxProperties"); if (minProperties.HasValue && maxProperties.HasValue && minProperties.Value > maxProperties.Value) { var targetDescription = DescribeObjectSchemaTarget(propertyPath); throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{targetDescription} in schema file '{schemaPath}' declares 'minProperties' greater than 'maxProperties'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } return !minProperties.HasValue && !maxProperties.HasValue ? null : new YamlConfigObjectConstraints(minProperties, maxProperties); } /// /// 读取数值区间约束。 /// /// 所属配置表名称。 /// 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); } 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 节点。 /// 关键字名称。 /// 属性数量约束;未声明时返回空。 private static int? TryParseObjectPropertyCountConstraint( 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) { var targetDescription = DescribeObjectSchemaTarget(propertyPath); throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{targetDescription} in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } return constraintValue; } /// /// 为对象级 schema 关键字构造稳定的诊断主体。 /// 根对象不会再显示为空字符串属性名,避免坏 schema 诊断出现 Property '' 之类的文本。 /// /// 对象字段路径。 /// 用于错误消息的对象主体描述。 private static string DescribeObjectSchemaTarget(string propertyPath) { return string.IsNullOrWhiteSpace(propertyPath) ? "Root object" : $"Property '{propertyPath}'"; } /// /// 读取数组去重约束。 /// /// 所属配置表名称。 /// 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), 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); 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)}."); } 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)}."); } if (constraints.MultipleOf.HasValue && !IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value)) { 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 (offset == "Z") { 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) { foreach (var referenceUsage in matchedReferences) { references.Add(referenceUsage); } } return true; } catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind != ConfigLoadFailureKind.UnexpectedFailure) { 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 节点归一化为可比较字符串。 /// 该键同时服务于 uniqueItemsconst, /// 因此要忽略对象字段顺序和字符串引号形式。 /// /// 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 (match.Groups["sign"].Value == "-") { 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); } } /// /// 将 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 parentPath == "" ? propertyName : $"{parentPath}.{propertyName}"; } /// /// 组合 YAML 诊断展示路径。 /// /// 父级路径。 /// 当前属性名。 /// 组合后的路径。 private static string CombineDisplayPath(string parentPath, string propertyName) { return string.IsNullOrWhiteSpace(parentPath) ? propertyName : $"{parentPath}.{propertyName}"; } /// /// 判断当前标量是否应按字符串处理。 /// 这里显式排除 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); } } /// /// 表示已解析并可用于运行时校验的 JSON Schema。 /// 该模型保留根节点与引用依赖集合,避免运行时引入完整 schema 引擎。 /// internal sealed class YamlConfigSchema { /// /// 初始化一个可用于运行时校验的 schema 模型。 /// /// Schema 文件路径。 /// 根节点模型。 /// Schema 声明的目标引用表名称集合。 public YamlConfigSchema( string schemaPath, YamlConfigSchemaNode rootNode, IReadOnlyCollection referencedTableNames) { ArgumentNullException.ThrowIfNull(schemaPath); ArgumentNullException.ThrowIfNull(rootNode); ArgumentNullException.ThrowIfNull(referencedTableNames); SchemaPath = schemaPath; RootNode = rootNode; ReferencedTableNames = referencedTableNames; } /// /// 获取 schema 文件路径。 /// public string SchemaPath { get; } /// /// 获取根节点模型。 /// public YamlConfigSchemaNode RootNode { get; } /// /// 获取 schema 声明的目标引用表名称集合。 /// 该信息用于热重载时推导受影响的依赖表闭包。 /// public IReadOnlyCollection ReferencedTableNames { get; } } /// /// 表示单个 schema 节点的最小运行时描述。 /// 同一个模型同时覆盖对象、数组和标量,便于递归校验逻辑只依赖一种树结构。 /// internal sealed class YamlConfigSchemaNode { private readonly NodeChildren _children; private readonly NodeValidation _validation; private YamlConfigSchemaNode( YamlConfigSchemaPropertyType nodeType, NodeChildren children, NodeValidation validation, string schemaPathHint) { ArgumentNullException.ThrowIfNull(children); ArgumentNullException.ThrowIfNull(validation); ArgumentNullException.ThrowIfNull(schemaPathHint); _children = children; _validation = validation; NodeType = nodeType; Properties = children.Properties; RequiredProperties = children.RequiredProperties; ItemNode = children.ItemNode; ReferenceTableName = validation.ReferenceTableName; AllowedValues = validation.AllowedValues; Constraints = validation.Constraints; ArrayConstraints = validation.ArrayConstraints; ObjectConstraints = validation.ObjectConstraints; ConstantValue = validation.ConstantValue; NegatedSchemaNode = validation.NegatedSchemaNode; SchemaPathHint = schemaPathHint; } /// /// 获取节点类型。 /// public YamlConfigSchemaPropertyType NodeType { get; } /// /// 获取对象属性集合;非对象节点时返回空。 /// public IReadOnlyDictionary? Properties { get; } /// /// 获取对象必填属性集合;非对象节点时返回空。 /// public IReadOnlyCollection? RequiredProperties { get; } /// /// 获取数组元素节点;非数组节点时返回空。 /// public YamlConfigSchemaNode? ItemNode { get; } /// /// 获取目标引用表名称;未声明跨表引用时返回空。 /// public string? ReferenceTableName { get; } /// /// 获取节点允许值集合;未声明 enum 时返回空。 /// public IReadOnlyCollection? AllowedValues { get; } /// /// 获取标量范围与长度约束;未声明时返回空。 /// public YamlConfigScalarConstraints? Constraints { get; } /// /// 获取对象属性数量约束;未声明时返回空。 /// public YamlConfigObjectConstraints? ObjectConstraints { get; } /// /// 获取数组元素数量约束;未声明时返回空。 /// public YamlConfigArrayConstraints? ArrayConstraints { get; } /// /// 获取节点常量约束;未声明 const 时返回空。 /// public YamlConfigConstantValue? ConstantValue { get; } /// /// 获取节点声明的 not 子 schema;未声明时返回空。 /// public YamlConfigSchemaNode? NegatedSchemaNode { get; } /// /// 获取用于诊断显示的 schema 路径提示。 /// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。 /// public string SchemaPathHint { get; } /// /// 创建对象节点描述。 /// /// 对象属性集合。 /// 对象必填属性集合。 /// 对象属性数量约束。 /// 用于错误信息的 schema 文件路径提示。 /// 对象节点模型。 public static YamlConfigSchemaNode CreateObject( IReadOnlyDictionary? properties, IReadOnlyCollection? requiredProperties, YamlConfigObjectConstraints? objectConstraints, string schemaPathHint) { return new YamlConfigSchemaNode( YamlConfigSchemaPropertyType.Object, new NodeChildren(properties, requiredProperties, itemNode: null), new NodeValidation( referenceTableName: null, allowedValues: null, constraints: null, arrayConstraints: null, objectConstraints, constantValue: null, negatedSchemaNode: null), schemaPathHint); } /// /// 创建数组节点描述。 /// /// 数组元素节点。 /// 数组节点允许值集合。 /// 数组元素数量约束。 /// 用于错误信息的 schema 文件路径提示。 /// 数组节点模型。 public static YamlConfigSchemaNode CreateArray( YamlConfigSchemaNode itemNode, IReadOnlyCollection? allowedValues, YamlConfigArrayConstraints? arrayConstraints, string schemaPathHint) { return new YamlConfigSchemaNode( YamlConfigSchemaPropertyType.Array, new NodeChildren(properties: null, requiredProperties: null, itemNode), new NodeValidation( referenceTableName: null, allowedValues, constraints: null, arrayConstraints, objectConstraints: null, constantValue: null, negatedSchemaNode: null), schemaPathHint); } /// /// 创建标量节点描述。 /// /// 标量节点类型。 /// 目标引用表名称。 /// 标量允许值集合。 /// 标量范围与长度约束。 /// 用于错误信息的 schema 文件路径提示。 /// 标量节点模型。 public static YamlConfigSchemaNode CreateScalar( YamlConfigSchemaPropertyType nodeType, string? referenceTableName, IReadOnlyCollection? allowedValues, YamlConfigScalarConstraints? constraints, string schemaPathHint) { return new YamlConfigSchemaNode( nodeType, NodeChildren.None, new NodeValidation( referenceTableName, allowedValues, constraints, arrayConstraints: null, objectConstraints: null, constantValue: null, negatedSchemaNode: null), schemaPathHint); } /// /// 基于当前节点复制一个只替换引用表名称的新节点。 /// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。 /// /// 新的目标引用表名称。 /// 复制后的节点。 public YamlConfigSchemaNode WithReferenceTable(string referenceTableName) { return new YamlConfigSchemaNode( NodeType, _children, _validation.WithReferenceTable(referenceTableName), SchemaPathHint); } /// /// 基于当前节点复制一个只替换 enum 允许值集合的新节点。 /// /// 新的允许值集合。 /// 复制后的节点。 public YamlConfigSchemaNode WithAllowedValues(IReadOnlyCollection? allowedValues) { return new YamlConfigSchemaNode( NodeType, _children, _validation.WithAllowedValues(allowedValues), SchemaPathHint); } /// /// 基于当前节点复制一个只替换常量约束的新节点。 /// /// 新的常量约束。 /// 复制后的节点。 public YamlConfigSchemaNode WithConstantValue(YamlConfigConstantValue? constantValue) { return new YamlConfigSchemaNode( NodeType, _children, _validation.WithConstantValue(constantValue), SchemaPathHint); } /// /// 基于当前节点复制一个只替换 not 子 schema 的新节点。 /// /// 新的 negated schema。 /// 复制后的节点。 public YamlConfigSchemaNode WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) { return new YamlConfigSchemaNode( NodeType, _children, _validation.WithNegatedSchemaNode(negatedSchemaNode), SchemaPathHint); } private sealed class NodeChildren { public NodeChildren( IReadOnlyDictionary? properties, IReadOnlyCollection? requiredProperties, YamlConfigSchemaNode? itemNode) { Properties = properties; RequiredProperties = requiredProperties; ItemNode = itemNode; } public static NodeChildren None { get; } = new(properties: null, requiredProperties: null, itemNode: null); public IReadOnlyDictionary? Properties { get; } public IReadOnlyCollection? RequiredProperties { get; } public YamlConfigSchemaNode? ItemNode { get; } } private sealed class NodeValidation { public NodeValidation( string? referenceTableName, IReadOnlyCollection? allowedValues, YamlConfigScalarConstraints? constraints, YamlConfigArrayConstraints? arrayConstraints, YamlConfigObjectConstraints? objectConstraints, YamlConfigConstantValue? constantValue, YamlConfigSchemaNode? negatedSchemaNode) { ReferenceTableName = referenceTableName; AllowedValues = allowedValues; Constraints = constraints; ArrayConstraints = arrayConstraints; ObjectConstraints = objectConstraints; ConstantValue = constantValue; NegatedSchemaNode = negatedSchemaNode; } public static NodeValidation None { get; } = new( referenceTableName: null, allowedValues: null, constraints: null, arrayConstraints: null, objectConstraints: null, constantValue: null, negatedSchemaNode: null); public string? ReferenceTableName { get; } public IReadOnlyCollection? AllowedValues { get; } public YamlConfigScalarConstraints? Constraints { get; } public YamlConfigArrayConstraints? ArrayConstraints { get; } public YamlConfigObjectConstraints? ObjectConstraints { get; } public YamlConfigConstantValue? ConstantValue { get; } public YamlConfigSchemaNode? NegatedSchemaNode { get; } public NodeValidation WithReferenceTable(string referenceTableName) { return new NodeValidation(referenceTableName, AllowedValues, Constraints, ArrayConstraints, ObjectConstraints, ConstantValue, NegatedSchemaNode); } public NodeValidation WithAllowedValues(IReadOnlyCollection? allowedValues) { return new NodeValidation(ReferenceTableName, allowedValues, Constraints, ArrayConstraints, ObjectConstraints, ConstantValue, NegatedSchemaNode); } public NodeValidation WithConstantValue(YamlConfigConstantValue? constantValue) { return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, ObjectConstraints, constantValue, NegatedSchemaNode); } public NodeValidation WithNegatedSchemaNode(YamlConfigSchemaNode? negatedSchemaNode) { return new NodeValidation(ReferenceTableName, AllowedValues, Constraints, ArrayConstraints, ObjectConstraints, ConstantValue, negatedSchemaNode); } } } /// /// 表示一个节点上声明的 const 约束。 /// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 /// internal sealed class YamlConfigConstantValue { /// /// 初始化常量约束模型。 /// /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始常量文本。 public YamlConfigConstantValue(string comparableValue, string displayValue) { ArgumentNullException.ThrowIfNull(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); ComparableValue = comparableValue; DisplayValue = displayValue; } /// /// 获取用于运行时比较的稳定键。 /// public string ComparableValue { get; } /// /// 获取用于诊断输出的原始 JSON 常量文本。 /// public string DisplayValue { get; } } /// /// 表示一个节点上声明的单个 enum 候选值。 /// 该模型同时保留稳定比较键与原始 JSON 文本,分别供运行时匹配和诊断输出复用。 /// internal sealed class YamlConfigAllowedValue { /// /// 初始化一个枚举候选值模型。 /// /// 用于与 YAML 节点比较的稳定键。 /// 用于诊断输出的原始 JSON 文本。 public YamlConfigAllowedValue(string comparableValue, string displayValue) { ArgumentNullException.ThrowIfNull(comparableValue); ArgumentException.ThrowIfNullOrWhiteSpace(displayValue); ComparableValue = comparableValue; DisplayValue = displayValue; } /// /// 获取用于运行时比较的稳定键。 /// public string ComparableValue { get; } /// /// 获取用于诊断输出的原始 JSON 文本。 /// public string DisplayValue { get; } } /// /// 表示一个对象节点上声明的属性数量约束。 /// 该模型将对象级约束与数组 / 标量约束拆开保存,避免运行时节点继续暴露无关成员。 /// internal sealed class YamlConfigObjectConstraints { /// /// 初始化对象约束模型。 /// /// 最小属性数量约束。 /// 最大属性数量约束。 public YamlConfigObjectConstraints(int? minProperties, int? maxProperties) { MinProperties = minProperties; MaxProperties = maxProperties; } /// /// 获取最小属性数量约束。 /// public int? MinProperties { get; } /// /// 获取最大属性数量约束。 /// public int? MaxProperties { get; } } /// /// 聚合一个标量节点上声明的数值约束与字符串约束。 /// 该包装层保留“标量字段有约束”的统一入口,同时把不同语义的约束分成更小的专用模型。 /// internal sealed class YamlConfigScalarConstraints { /// /// 初始化标量约束模型。 /// /// 数值约束分组。 /// 字符串约束分组。 public YamlConfigScalarConstraints( YamlConfigNumericConstraints? numericConstraints, YamlConfigStringConstraints? stringConstraints) { NumericConstraints = numericConstraints; StringConstraints = stringConstraints; } /// /// 获取数值约束分组。 /// public YamlConfigNumericConstraints? NumericConstraints { get; } /// /// 获取字符串约束分组。 /// public YamlConfigStringConstraints? StringConstraints { get; } } /// /// 表示标量节点上声明的数值范围与步进约束。 /// 该类型只覆盖整数 / 浮点共享的关键字,避免字符串字段继续暴露不相关的成员。 /// internal sealed class YamlConfigNumericConstraints { /// /// 初始化数值约束模型。 /// /// 最小值约束。 /// 最大值约束。 /// 开区间最小值约束。 /// 开区间最大值约束。 /// 数值步进约束。 public YamlConfigNumericConstraints( double? minimum, double? maximum, double? exclusiveMinimum, double? exclusiveMaximum, double? multipleOf) { Minimum = minimum; Maximum = maximum; ExclusiveMinimum = exclusiveMinimum; ExclusiveMaximum = exclusiveMaximum; MultipleOf = multipleOf; } /// /// 获取最小值约束。 /// public double? Minimum { get; } /// /// 获取最大值约束。 /// public double? Maximum { get; } /// /// 获取开区间最小值约束。 /// public double? ExclusiveMinimum { get; } /// /// 获取开区间最大值约束。 /// public double? ExclusiveMaximum { get; } /// /// 获取数值步进约束。 /// public double? MultipleOf { get; } } /// /// 表示标量节点上声明的字符串长度、模式与 format 约束。 /// 该模型将正则原文、预编译正则和共享 format 枚举绑定保存, /// 保证诊断内容与运行时匹配逻辑保持一致。 /// internal sealed class YamlConfigStringConstraints { /// /// 初始化字符串约束模型。 /// /// 最小长度约束。 /// 最大长度约束。 /// 正则模式约束原文。 /// 已编译的正则表达式。 /// 字符串 format 约束。 public YamlConfigStringConstraints( int? minLength, int? maxLength, string? pattern, Regex? patternRegex, YamlConfigStringFormatConstraint? formatConstraint) { MinLength = minLength; MaxLength = maxLength; Pattern = pattern; PatternRegex = patternRegex; FormatConstraint = formatConstraint; } /// /// 获取最小长度约束。 /// public int? MinLength { get; } /// /// 获取最大长度约束。 /// public int? MaxLength { get; } /// /// 获取正则模式约束原文。 /// public string? Pattern { get; } /// /// 获取已编译的正则表达式。 /// public Regex? PatternRegex { get; } /// /// 获取字符串 format 约束。 /// public YamlConfigStringFormatConstraint? FormatConstraint { get; } } /// /// 表示一个已归一化的字符串 format 约束。 /// 该模型同时保留 schema 原文与共享枚举,方便诊断信息稳定展示,又避免运行时校验反复解析字符串。 /// internal sealed class YamlConfigStringFormatConstraint { /// /// 初始化字符串 format 约束模型。 /// /// schema 中声明的 format 名称。 /// 归一化后的共享 format 枚举。 public YamlConfigStringFormatConstraint( string schemaName, YamlConfigStringFormatKind kind) { ArgumentException.ThrowIfNullOrWhiteSpace(schemaName); SchemaName = schemaName; Kind = kind; } /// /// 获取 schema 中声明的 format 名称。 /// public string SchemaName { get; } /// /// 获取归一化后的共享 format 枚举。 /// public YamlConfigStringFormatKind Kind { get; } } /// /// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。 /// internal enum YamlConfigStringFormatKind { /// /// 表示 yyyy-MM-dd 形式的日期。 /// Date, /// /// 表示带显式时区偏移的 RFC 3339 日期时间。 /// DateTime, /// /// 表示 day-time duration 形式的持续时间。 /// Duration, /// /// 表示基础电子邮件地址格式。 /// Email, /// /// 表示带显式时区偏移的 RFC 3339 时间。 /// Time, /// /// 表示绝对 URI。 /// Uri, /// /// 表示连字符分隔的 UUID 文本。 /// Uuid } /// /// 表示一个数组节点上声明的元素数量、去重与 contains 匹配计数约束。 /// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。 /// internal sealed class YamlConfigArrayConstraints { /// /// 初始化数组约束模型。 /// /// 最小元素数量约束。 /// 最大元素数量约束。 /// 是否要求数组元素唯一。 /// 数组 contains 约束;未声明时为空。 public YamlConfigArrayConstraints( int? minItems, int? maxItems, bool uniqueItems, YamlConfigArrayContainsConstraints? containsConstraints) { MinItems = minItems; MaxItems = maxItems; UniqueItems = uniqueItems; ContainsConstraints = containsConstraints; } /// /// 获取最小元素数量约束。 /// public int? MinItems { get; } /// /// 获取最大元素数量约束。 /// public int? MaxItems { get; } /// /// 获取是否要求数组元素唯一。 /// public bool UniqueItems { get; } /// /// 获取数组 contains 约束;未声明时返回空。 /// public YamlConfigArrayContainsConstraints? ContainsConstraints { get; } } /// /// 表示数组节点声明的 contains 匹配约束。 /// 该模型把 contains 子 schema 与匹配数量边界聚合在一起,避免数组节点再额外散落多组相关成员。 /// internal sealed class YamlConfigArrayContainsConstraints { /// /// 初始化数组 contains 约束模型。 /// /// contains 子 schema。 /// 最小匹配数量;为 时按 JSON Schema 语义默认 1。 /// 最大匹配数量。 public YamlConfigArrayContainsConstraints( YamlConfigSchemaNode containsNode, int? minContains, int? maxContains) { ArgumentNullException.ThrowIfNull(containsNode); ContainsNode = containsNode; MinContains = minContains; MaxContains = maxContains; } /// /// 获取 contains 子 schema。 /// public YamlConfigSchemaNode ContainsNode { get; } /// /// 获取最小匹配数量;未显式声明时返回空,由调用方按默认值 1 解释。 /// public int? MinContains { get; } /// /// 获取最大匹配数量。 /// public int? MaxContains { get; } } /// /// 表示单个 YAML 文件中提取出的跨表引用。 /// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。 /// internal sealed class YamlConfigReferenceUsage { /// /// 初始化一个跨表引用使用记录。 /// /// 源 YAML 文件路径。 /// 定义该引用的 schema 文件路径。 /// 声明引用的字段路径。 /// YAML 中的原始标量值。 /// 目标配置表名称。 /// 引用值的 schema 标量类型。 public YamlConfigReferenceUsage( string yamlPath, string schemaPath, string propertyPath, string rawValue, string referencedTableName, YamlConfigSchemaPropertyType valueType) { ArgumentNullException.ThrowIfNull(yamlPath); ArgumentNullException.ThrowIfNull(schemaPath); ArgumentNullException.ThrowIfNull(propertyPath); ArgumentNullException.ThrowIfNull(rawValue); ArgumentNullException.ThrowIfNull(referencedTableName); YamlPath = yamlPath; SchemaPath = schemaPath; PropertyPath = propertyPath; RawValue = rawValue; ReferencedTableName = referencedTableName; ValueType = valueType; } /// /// 获取源 YAML 文件路径。 /// public string YamlPath { get; } /// /// 获取定义该引用的 schema 文件路径。 /// public string SchemaPath { get; } /// /// 获取声明引用的字段路径。 /// public string PropertyPath { get; } /// /// 获取 YAML 中的原始标量值。 /// public string RawValue { get; } /// /// 获取目标配置表名称。 /// public string ReferencedTableName { get; } /// /// 获取引用值的 schema 标量类型。 /// public YamlConfigSchemaPropertyType ValueType { get; } /// /// 获取便于诊断显示的字段路径。 /// public string DisplayPath => PropertyPath; } /// /// 表示当前运行时 schema 校验器支持的属性类型。 /// internal enum YamlConfigSchemaPropertyType { /// /// 对象类型。 /// Object, /// /// 整数类型。 /// Integer, /// /// 数值类型。 /// Number, /// /// 布尔类型。 /// Boolean, /// /// 字符串类型。 /// String, /// /// 数组类型。 /// Array }