using GFramework.Game.Abstractions.Config; namespace GFramework.Game.Config; /// /// 承载对象级 schema 关键字的解析与元数据校验逻辑。 /// 该 partial 将 minPropertiesmaxProperties、 /// dependentRequireddependentSchemasallOf /// 与 object-focused if / then / else /// 从主校验文件中拆出,降低超大文件继续堆叠对象关键字时的维护成本。 /// internal static partial class YamlConfigSchemaValidator { /// /// 解析对象节点支持的属性数量约束与对象关键字约束。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 对象字段路径。 /// Schema 节点。 /// 当前对象已声明的属性集合。 /// 对象约束模型;未声明时返回空。 private static YamlConfigObjectConstraints? ParseObjectConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element, IReadOnlyDictionary properties) { var minProperties = TryParseObjectPropertyCountConstraint( tableName, schemaPath, propertyPath, element, "minProperties"); var maxProperties = TryParseObjectPropertyCountConstraint( tableName, schemaPath, propertyPath, element, "maxProperties"); var dependentRequired = ParseDependentRequiredConstraints(tableName, schemaPath, propertyPath, element, properties); var dependentSchemas = ParseDependentSchemasConstraints(tableName, schemaPath, propertyPath, element, properties); var allOfSchemas = ParseAllOfConstraints(tableName, schemaPath, propertyPath, element, properties); var conditionalSchemas = ParseConditionalSchemasConstraints(tableName, schemaPath, propertyPath, element, properties); 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 && dependentRequired is null && dependentSchemas is null && allOfSchemas is null && conditionalSchemas is null ? null : new YamlConfigObjectConstraints( minProperties, maxProperties, dependentRequired, dependentSchemas, allOfSchemas, conditionalSchemas); } /// /// 解析对象节点声明的 dependentRequired 依赖关系。 /// 该关键字只表达“当触发字段出现时,还必须同时声明哪些同级字段”, /// 因此这里会把触发字段与依赖字段都限制在当前对象已声明的属性集合内, /// 避免运行时与工具链对无效键名各自做隐式容错。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 对象字段路径。 /// Schema 节点。 /// 当前对象已声明的属性集合。 /// 归一化后的依赖关系表;未声明或只有空依赖时返回空。 private static IReadOnlyDictionary>? ParseDependentRequiredConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element, IReadOnlyDictionary properties) { if (!element.TryGetProperty("dependentRequired", out var dependentRequiredElement)) { return null; } if (dependentRequiredElement.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'dependentRequired' as an object.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var dependentRequired = new Dictionary>(StringComparer.Ordinal); foreach (var dependency in dependentRequiredElement.EnumerateObject()) { if (!properties.ContainsKey(dependency.Name)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' for undeclared property '{dependency.Name}'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } if (dependency.Value.ValueKind != JsonValueKind.Array) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' as an array of sibling property names.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var dependencyTargets = new List(); var seenDependencyTargets = new HashSet(StringComparer.Ordinal); foreach (var dependencyTarget in dependency.Value.EnumerateArray()) { if (dependencyTarget.ValueKind != JsonValueKind.String) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentRequired' entries as strings.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var dependencyTargetName = dependencyTarget.GetString(); if (string.IsNullOrWhiteSpace(dependencyTargetName)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } if (!properties.ContainsKey(dependencyTargetName)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentRequired' target '{dependencyTargetName}' that is not declared in the same object schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } if (seenDependencyTargets.Add(dependencyTargetName)) { dependencyTargets.Add(dependencyTargetName); } } if (dependencyTargets.Count > 0) { dependentRequired[dependency.Name] = dependencyTargets; } } return dependentRequired.Count == 0 ? null : dependentRequired; } /// /// 解析对象节点声明的 dependentSchemas 条件 schema。 /// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释, /// 因此触发字段仍限制在当前对象已声明的属性内,而具体约束则继续复用现有递归节点解析逻辑。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 对象字段路径。 /// Schema 节点。 /// 当前对象已声明的属性集合。 /// 归一化后的触发字段到条件 schema 的映射;未声明时返回空。 private static IReadOnlyDictionary? ParseDependentSchemasConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element, IReadOnlyDictionary properties) { if (!element.TryGetProperty("dependentSchemas", out var dependentSchemasElement)) { return null; } if (dependentSchemasElement.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'dependentSchemas' as an object.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var dependentSchemas = new Dictionary(StringComparer.Ordinal); foreach (var dependency in dependentSchemasElement.EnumerateObject()) { if (!properties.ContainsKey(dependency.Name)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' declares 'dependentSchemas' for undeclared property '{dependency.Name}'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } if (dependency.Value.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'dependentSchemas' as an object-valued schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var dependencySchemaPath = BuildNestedSchemaPath(propertyPath, $"dependentSchemas:{dependency.Name}"); var dependencySchemaNode = ParseNode( tableName, schemaPath, dependencySchemaPath, dependency.Value); if (dependencySchemaNode.NodeType != YamlConfigSchemaPropertyType.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Property '{dependency.Name}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed 'dependentSchemas' schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(dependencySchemaPath)); } dependentSchemas[dependency.Name] = dependencySchemaNode; } return dependentSchemas.Count == 0 ? null : dependentSchemas; } /// /// 解析对象节点声明的 allOf 组合约束。 /// 当前实现仅接受 object-typed 内联 schema,并把每个条目当成 focused constraint block /// 叠加到当前对象上,而不是参与属性合并或改变生成类型形状。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 对象字段路径。 /// Schema 节点。 /// 父对象已声明的属性集合。 /// 归一化后的 allOf schema 列表;未声明或为空时返回空。 private static IReadOnlyList? ParseAllOfConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element, IReadOnlyDictionary properties) { if (!element.TryGetProperty("allOf", out var allOfElement)) { return null; } if (allOfElement.ValueKind != JsonValueKind.Array) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' as an array of object-valued schemas.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var allOfSchemas = new List(); var allOfIndex = 0; foreach (var allOfSchemaElement in allOfElement.EnumerateArray()) { if (allOfSchemaElement.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'allOf' entries as object-valued schemas.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]"); ValidateInlineObjectSchemaTargetsAgainstParentObject( tableName, schemaPath, propertyPath, allOfSchemaPath, $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'", allOfSchemaElement, properties); var allOfSchemaNode = ParseNode( tableName, schemaPath, allOfSchemaPath, allOfSchemaElement); if (allOfSchemaNode.NodeType != YamlConfigSchemaPropertyType.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf' for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(allOfSchemaPath)); } allOfSchemas.Add(allOfSchemaNode); allOfIndex++; } return allOfSchemas.Count == 0 ? null : allOfSchemas; } /// /// 解析对象节点声明的 object-focused if / then / else 条件约束。 /// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义, /// 既允许根据 sibling 值切换约束分支,又避免把条件 schema 扩展成新的生成类型形状。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 对象字段路径。 /// Schema 节点。 /// 父对象已声明的属性集合。 /// 归一化后的条件约束;未声明时返回空。 private static YamlConfigConditionalSchemas? ParseConditionalSchemasConstraints( string tableName, string schemaPath, string propertyPath, JsonElement element, IReadOnlyDictionary properties) { var hasIf = element.TryGetProperty("if", out var ifElement); var hasThen = element.TryGetProperty("then", out var thenElement); var hasElse = element.TryGetProperty("else", out var elseElement); if (!hasIf && !hasThen && !hasElse) { return null; } if (!hasIf) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare 'if' when using 'then' or 'else'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } if (!hasThen && !hasElse) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare at least one of 'then' or 'else' when using 'if'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if"); var ifSchemaNode = ParseConditionalObjectSchema( tableName, schemaPath, propertyPath, ifSchemaPath, "if", ifElement, properties); var thenSchemaNode = hasThen ? ParseConditionalObjectSchema( tableName, schemaPath, propertyPath, BuildNestedSchemaPath(propertyPath, "then"), "then", thenElement, properties) : null; var elseSchemaNode = hasElse ? ParseConditionalObjectSchema( tableName, schemaPath, propertyPath, BuildNestedSchemaPath(propertyPath, "else"), "else", elseElement, properties) : null; return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode); } /// /// 解析单个条件分支的 object-focused 内联 schema。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 父对象路径。 /// 当前条件分支路径。 /// 条件关键字名称。 /// 当前条件分支 schema。 /// 父对象已声明的属性集合。 /// 解析后的 object-typed schema。 private static YamlConfigSchemaNode ParseConditionalObjectSchema( string tableName, string schemaPath, string propertyPath, string conditionalSchemaPath, string keywordName, JsonElement conditionalSchemaElement, IReadOnlyDictionary properties) { if (conditionalSchemaElement.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare '{keywordName}' as an object-valued schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(propertyPath)); } ValidateInlineObjectSchemaTargetsAgainstParentObject( tableName, schemaPath, propertyPath, conditionalSchemaPath, $"'{keywordName}'", conditionalSchemaElement, properties); var conditionalSchemaNode = ParseNode( tableName, schemaPath, conditionalSchemaPath, conditionalSchemaElement); if (conditionalSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object) { return conditionalSchemaNode; } throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{DescribeObjectSchemaTarget(propertyPath)} in schema file '{schemaPath}' must declare an object-typed '{keywordName}' schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(conditionalSchemaPath)); } /// /// 验证 object-focused 内联 schema 只约束父对象已经声明过的同级字段。 /// 当前 shared subset 不会把 focused block 内字段并回父对象形状,因此这里会提前拒绝 /// “在 focused block 里引入父对象未声明字段”的不可满足 schema。 /// /// 所属配置表名称。 /// Schema 文件路径。 /// 父对象路径。 /// 当前内联 schema 路径。 /// 用于诊断文本的条目标签。 /// 当前内联 schema。 /// 父对象已声明的属性集合。 private static void ValidateInlineObjectSchemaTargetsAgainstParentObject( string tableName, string schemaPath, string propertyPath, string inlineSchemaPath, string entryLabel, JsonElement inlineSchemaElement, IReadOnlyDictionary properties) { if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement)) { if (inlinePropertiesElement.ValueKind != JsonValueKind.Object) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'properties' as an object-valued map.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } foreach (var property in inlinePropertiesElement.EnumerateObject()) { if (properties.ContainsKey(property.Name)) { continue; } throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' declares property '{property.Name}', but that property is not declared in the parent object schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } } if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement)) { return; } if (inlineRequiredElement.ValueKind != JsonValueKind.Array) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' as an array of property names.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } foreach (var requiredProperty in inlineRequiredElement.EnumerateArray()) { if (requiredProperty.ValueKind != JsonValueKind.String) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare 'required' entries as property-name strings.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } var requiredPropertyName = requiredProperty.GetString(); if (string.IsNullOrWhiteSpace(requiredPropertyName)) { throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank property names in 'required'.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } if (properties.ContainsKey(requiredPropertyName)) { continue; } throw ConfigLoadExceptionFactory.Create( ConfigLoadFailureKind.SchemaUnsupported, tableName, $"{entryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' requires property '{requiredPropertyName}', but that property is not declared in the parent object schema.", schemaPath: schemaPath, displayPath: GetDiagnosticPath(inlineSchemaPath)); } } /// /// 读取对象属性数量约束。 /// /// 所属配置表名称。 /// 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 作者声明的大小写一起改写。 /// /// 对象字段路径。 /// 可直接拼接到句中介词后的对象主体描述。 private static string DescribeObjectSchemaTargetInClause(string propertyPath) { return string.IsNullOrWhiteSpace(propertyPath) ? "root object" : $"property '{propertyPath}'"; } }