using System.Text.RegularExpressions; using GFramework.Game.Abstractions.Config; namespace GFramework.Game.Config; /// /// 提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。 /// 该校验器与当前配置生成器、VS Code 工具支持的 schema 子集保持一致, /// 并通过递归遍历方式覆盖嵌套对象、对象数组、标量数组与深层 enum / 引用约束。 /// internal static class YamlConfigSchemaValidator { /// /// 从磁盘加载并解析一个 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); } catch (Exception exception) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaReadFailed, tableName, $"Failed to read schema file '{schemaPath}'.", schemaPath: schemaPath, innerException: exception); } 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); return new YamlConfigSchema(schemaPath, rootNode, referencedTableNames.ToArray()); } catch (JsonException exception) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaInvalidJson, tableName, $"Schema file '{schemaPath}' contains invalid JSON.", schemaPath: schemaPath, innerException: exception); } } /// /// 使用已解析的 schema 校验 YAML 文本。 /// /// 所属配置表名称。 /// 已解析的 schema 模型。 /// YAML 文件路径,仅用于诊断信息。 /// YAML 文本内容。 /// 当参数为空时抛出。 /// 当 YAML 内容与 schema 不匹配时抛出。 internal static void Validate( string tableName, YamlConfigSchema schema, string yamlPath, string yamlText) { ValidateAndCollectReferences(tableName, schema, yamlPath, yamlText); } /// /// 使用已解析的 schema 校验 YAML 文本,并提取声明过的跨表引用。 /// 该方法让结构校验与引用采集共享同一份 YAML 解析结果,避免加载器重复解析同一文件。 /// /// 所属配置表名称。 /// 已解析的 schema 模型。 /// YAML 文件路径,仅用于诊断信息。 /// YAML 文本内容。 /// 当前 YAML 文件中声明的跨表引用集合。 /// 当参数为空时抛出。 /// 当 YAML 内容与 schema 不匹配时抛出。 internal static IReadOnlyList ValidateAndCollectReferences( string tableName, YamlConfigSchema schema, string yamlPath, string yamlText) { 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); } var references = new List(); ValidateNode(tableName, yamlPath, string.Empty, yamlStream.Documents[0].RootNode, schema.RootNode, references); return references; } /// /// 递归解析 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); switch (typeName) { case "object": EnsureReferenceKeywordIsSupported(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Object, referenceTableName); return ParseObjectNode(tableName, schemaPath, propertyPath, element, isRoot); case "array": return ParseArrayNode(tableName, schemaPath, propertyPath, element, referenceTableName); case "integer": return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Integer, element, referenceTableName); case "number": return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Number, element, referenceTableName); case "boolean": return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Boolean, element, referenceTableName); case "string": return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.String, element, referenceTableName); default: throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath), rawValue: typeName); } } /// /// 解析对象节点,保留属性字典与必填集合,以便后续递归校验时逐层定位错误。 /// /// 所属配置表名称。 /// 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); } return new YamlConfigSchemaNode( YamlConfigSchemaPropertyType.Object, properties, requiredProperties, itemNode: null, referenceTableName: null, allowedValues: null, constraints: null, arrayConstraints: null, schemaPath); } /// /// 解析数组节点。 /// 当前子集支持标量数组和对象数组,不支持数组嵌套数组。 /// 当数组声明跨表引用时,会把引用语义挂到元素节点上,便于后续逐项校验。 /// /// 所属配置表名称。 /// 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)); } return new YamlConfigSchemaNode( YamlConfigSchemaPropertyType.Array, properties: null, requiredProperties: null, itemNode, referenceTableName: null, allowedValues: null, constraints: null, arrayConstraints: ParseArrayConstraints(tableName, schemaPath, propertyPath, element), schemaPath); } /// /// 创建标量节点,并在解析阶段就完成 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); return new YamlConfigSchemaNode( nodeType, properties: null, requiredProperties: null, itemNode: null, referenceTableName, ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"), ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType), arrayConstraints: null, schemaPath); } /// /// 递归校验 YAML 节点。 /// 每层都带上逻辑字段路径,这样深层对象与数组元素的错误也能直接定位。 /// /// 所属配置表名称。 /// YAML 文件路径。 /// 当前字段路径;根节点时为空。 /// 实际 YAML 节点。 /// 对应的 schema 节点。 /// 已收集的跨表引用。 private static void ValidateNode( string tableName, string yamlPath, string displayPath, YamlNode node, YamlConfigSchemaNode schemaNode, ICollection references) { switch (schemaNode.NodeType) { case YamlConfigSchemaPropertyType.Object: ValidateObjectNode(tableName, yamlPath, displayPath, node, schemaNode, references); return; case YamlConfigSchemaPropertyType.Array: ValidateArrayNode(tableName, yamlPath, displayPath, node, schemaNode, references); 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 节点。 /// 已收集的跨表引用。 private static void ValidateObjectNode( string tableName, string yamlPath, string displayPath, YamlNode node, YamlConfigSchemaNode schemaNode, ICollection references) { 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)) { 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); } 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); } } /// /// 校验数组节点,并递归验证每个元素。 /// /// 所属配置表名称。 /// YAML 文件路径。 /// 数组字段路径。 /// 实际 YAML 节点。 /// 数组 schema 节点。 /// 已收集的跨表引用。 private static void ValidateArrayNode( string tableName, string yamlPath, string displayPath, YamlNode node, YamlConfigSchemaNode schemaNode, ICollection references) { 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); } } /// /// 校验标量节点,并在值有效时收集跨表引用。 /// /// 所属配置表名称。 /// 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); if (schemaNode.AllowedValues is { Count: > 0 } && !schemaNode.AllowedValues.Contains(normalizedValue, StringComparer.Ordinal)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.EnumValueNotAllowed, tableName, $"Property '{displayPath}' in config file '{yamlPath}' must be one of [{string.Join(", ", schemaNode.AllowedValues)}], but the current YAML scalar value is '{value}'.", yamlPath: yamlPath, schemaPath: schemaNode.SchemaPathHint, displayPath: GetDiagnosticPath(displayPath), rawValue: value, detail: $"Allowed values: {string.Join(", ", schemaNode.AllowedValues)}."); } if (schemaNode.Constraints is not null) { ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode); } if (schemaNode.ReferenceTableName != null) { references.Add( new YamlConfigReferenceUsage( yamlPath, schemaNode.SchemaPathHint, displayPath, normalizedValue, schemaNode.ReferenceTableName, schemaNode.NodeType)); } } /// /// 解析 enum,并在读取阶段验证枚举值与字段类型的兼容性。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 字段路径。 /// Schema 节点。 /// 期望的标量类型。 /// 当前读取的关键字名称。 /// 归一化后的枚举值集合;未声明时返回空。 private static IReadOnlyCollection? ParseEnumValues( string tableName, string schemaPath, string propertyPath, JsonElement element, YamlConfigSchemaPropertyType expectedType, 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( NormalizeEnumValue(tableName, schemaPath, propertyPath, keywordName, expectedType, item)); } return allowedValues; } /// /// 解析标量字段支持的范围、长度与模式约束。 /// 当前共享子集支持: /// `integer/number` 上的 `minimum/maximum/exclusiveMinimum/exclusiveMaximum`, /// 以及 `string` 上的 `minLength/maxLength/pattern`。 /// /// 所属配置表名称。 /// 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 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); 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)); } if (!minimum.HasValue && !maximum.HasValue && !exclusiveMinimum.HasValue && !exclusiveMaximum.HasValue && !minLength.HasValue && !maxLength.HasValue && pattern is null) { return null; } return new YamlConfigScalarConstraints( minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, pattern, pattern is null ? null : new Regex( pattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture)); } /// /// 解析数组节点支持的元素数量约束。 /// /// 所属配置表名称。 /// 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"); 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 ? null : new YamlConfigArrayConstraints(minItems, maxItems); } /// /// 读取数值区间约束。 /// /// 所属配置表名称。 /// 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; } /// /// 读取字符串长度约束。 /// /// 所属配置表名称。 /// 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, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture); } 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; } /// /// 读取数组元素数量约束。 /// /// 所属配置表名称。 /// 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 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: if (!double.TryParse( normalizedValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var 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); } 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)}."); } return; case YamlConfigSchemaPropertyType.String: 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}."); } 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()); } } /// /// 校验数组值是否满足元素数量约束。 /// /// 所属配置表名称。 /// 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}."); } } /// /// 解析跨表引用目标表名称。 /// /// 所属配置表名称。 /// 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); } } /// /// 将 schema 中的 enum 单值归一化到运行时比较字符串。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 字段路径。 /// 关键字名称。 /// 期望的标量类型。 /// 当前枚举值节点。 /// 归一化后的字符串值。 private static string NormalizeEnumValue( 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 { /// /// 初始化一个 schema 节点描述。 /// /// 节点类型。 /// 对象属性集合。 /// 对象必填属性集合。 /// 数组元素节点。 /// 目标引用表名称。 /// 标量允许值集合。 /// 标量范围与长度约束。 /// 数组元素数量约束。 /// 用于错误信息的 schema 文件路径提示。 public YamlConfigSchemaNode( YamlConfigSchemaPropertyType nodeType, IReadOnlyDictionary? properties, IReadOnlyCollection? requiredProperties, YamlConfigSchemaNode? itemNode, string? referenceTableName, IReadOnlyCollection? allowedValues, YamlConfigScalarConstraints? constraints, YamlConfigArrayConstraints? arrayConstraints, string schemaPathHint) { NodeType = nodeType; Properties = properties; RequiredProperties = requiredProperties; ItemNode = itemNode; ReferenceTableName = referenceTableName; AllowedValues = allowedValues; Constraints = constraints; ArrayConstraints = arrayConstraints; 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 YamlConfigArrayConstraints? ArrayConstraints { get; } /// /// 获取用于诊断显示的 schema 路径提示。 /// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。 /// public string SchemaPathHint { get; } /// /// 基于当前节点复制一个只替换引用表名称的新节点。 /// 该方法用于把数组级别的 ref-table 语义挂接到元素节点上。 /// /// 新的目标引用表名称。 /// 复制后的节点。 public YamlConfigSchemaNode WithReferenceTable(string referenceTableName) { return new YamlConfigSchemaNode( NodeType, Properties, RequiredProperties, ItemNode, referenceTableName, AllowedValues, Constraints, ArrayConstraints, SchemaPathHint); } } /// /// 表示一个标量节点上声明的数值范围或字符串长度约束。 /// 该模型让运行时、热重载和跨文件诊断都能复用同一份最小约束信息。 /// internal sealed class YamlConfigScalarConstraints { /// /// 初始化标量约束模型。 /// /// 最小值约束。 /// 最大值约束。 /// 开区间最小值约束。 /// 开区间最大值约束。 /// 最小长度约束。 /// 最大长度约束。 /// 正则模式约束。 /// 已编译的正则表达式。 public YamlConfigScalarConstraints( double? minimum, double? maximum, double? exclusiveMinimum, double? exclusiveMaximum, int? minLength, int? maxLength, string? pattern, Regex? patternRegex) { Minimum = minimum; Maximum = maximum; ExclusiveMinimum = exclusiveMinimum; ExclusiveMaximum = exclusiveMaximum; MinLength = minLength; MaxLength = maxLength; Pattern = pattern; PatternRegex = patternRegex; } /// /// 获取最小值约束。 /// public double? Minimum { get; } /// /// 获取最大值约束。 /// public double? Maximum { get; } /// /// 获取开区间最小值约束。 /// public double? ExclusiveMinimum { get; } /// /// 获取开区间最大值约束。 /// public double? ExclusiveMaximum { get; } /// /// 获取最小长度约束。 /// public int? MinLength { get; } /// /// 获取最大长度约束。 /// public int? MaxLength { get; } /// /// 获取正则模式约束原文。 /// public string? Pattern { get; } /// /// 获取已编译的正则表达式。 /// public Regex? PatternRegex { get; } } /// /// 表示一个数组节点上声明的元素数量约束。 /// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。 /// internal sealed class YamlConfigArrayConstraints { /// /// 初始化数组约束模型。 /// /// 最小元素数量约束。 /// 最大元素数量约束。 public YamlConfigArrayConstraints(int? minItems, int? maxItems) { MinItems = minItems; MaxItems = maxItems; } /// /// 获取最小元素数量约束。 /// public int? MinItems { get; } /// /// 获取最大元素数量约束。 /// public int? MaxItems { 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 }