From 1395b84439eb34c83c0cbc59ddf293556fd5d16a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:32:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(game):=20=E6=8B=86=E5=88=86=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=20schema=20=E5=85=B3=E9=94=AE=E5=AD=97=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 dependentRequired 与 dependentSchemas 的单项解析流程 - 重构 allOf 与条件 schema 的分支解析流程 - 优化 object-focused 内联 schema 的 properties 与 required 校验拆分 --- ...amlConfigSchemaValidator.ObjectKeywords.cs | 686 ++++++++++++------ 1 file changed, 470 insertions(+), 216 deletions(-) diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs index 4358d850..7572655f 100644 --- a/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs +++ b/GFramework.Game/Config/YamlConfigSchemaValidator.ObjectKeywords.cs @@ -104,67 +104,12 @@ internal static partial class YamlConfigSchemaValidator 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); - } - } - + var dependencyTargets = ParseDependentRequiredConstraint( + tableName, + schemaPath, + propertyPath, + dependency, + properties); if (dependencyTargets.Count > 0) { dependentRequired[dependency.Name] = dependencyTargets; @@ -176,6 +121,116 @@ internal static partial class YamlConfigSchemaValidator : dependentRequired; } + /// + /// 解析单个 dependentRequired 触发字段的依赖目标列表。 + /// 触发字段和目标字段必须都来自父对象已声明属性;重复目标会被去重以保持运行时约束稳定。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段声明。 + /// 当前对象已声明的属性集合。 + /// 去重后的依赖目标列表。 + private static IReadOnlyList ParseDependentRequiredConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonProperty dependency, + IReadOnlyDictionary properties) + { + 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()) + { + var dependencyTargetName = ParseDependentRequiredTargetName( + tableName, + schemaPath, + propertyPath, + dependency.Name, + dependencyTarget, + properties); + if (seenDependencyTargets.Add(dependencyTargetName)) + { + dependencyTargets.Add(dependencyTargetName); + } + } + + return dependencyTargets; + } + + /// + /// 读取并校验 dependentRequired 的单个目标字段名。 + /// 目标必须是非空字符串并已在同一个对象 schema 中声明,避免依赖关系指向不可满足字段。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段名称。 + /// 当前依赖目标节点。 + /// 当前对象已声明的属性集合。 + /// 已校验的依赖目标字段名。 + private static string ParseDependentRequiredTargetName( + string tableName, + string schemaPath, + string propertyPath, + string dependencyName, + JsonElement dependencyTarget, + IReadOnlyDictionary properties) + { + if (dependencyTarget.ValueKind != JsonValueKind.String) + { + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"Property '{dependencyName}' 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 '{dependencyName}' in {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' cannot declare blank 'dependentRequired' entries.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(propertyPath)); + } + + if (properties.ContainsKey(dependencyTargetName)) + { + return 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)); + } + /// /// 解析对象节点声明的 dependentSchemas 条件 schema。 /// 当前实现把它作为“当触发字段出现时,当前对象还必须额外满足一段内联 schema”来解释, @@ -212,43 +267,12 @@ internal static partial class YamlConfigSchemaValidator 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( + dependentSchemas[dependency.Name] = ParseDependentSchemaConstraint( 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; + propertyPath, + dependency, + properties); } return dependentSchemas.Count == 0 @@ -256,6 +280,62 @@ internal static partial class YamlConfigSchemaValidator : dependentSchemas; } + /// + /// 解析单个 dependentSchemas 触发字段关联的 object-typed schema。 + /// 触发字段必须属于当前对象,关联 schema 继续通过通用节点解析流程获得完整约束模型。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 当前触发字段声明。 + /// 当前对象已声明的属性集合。 + /// 解析后的 object-typed 条件 schema。 + private static YamlConfigSchemaNode ParseDependentSchemaConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonProperty dependency, + IReadOnlyDictionary properties) + { + 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) + { + return dependencySchemaNode; + } + + 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)); + } + /// /// 解析对象节点声明的 allOf 组合约束。 /// 当前实现仅接受 object-typed 内联 schema,并把每个条目当成 focused constraint block @@ -293,40 +373,13 @@ internal static partial class YamlConfigSchemaValidator 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( + var allOfSchemaNode = ParseAllOfSchemaConstraint( tableName, schemaPath, propertyPath, - allOfSchemaPath, - $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'", allOfSchemaElement, + allOfIndex, 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++; } @@ -336,6 +389,64 @@ internal static partial class YamlConfigSchemaValidator : allOfSchemas; } + /// + /// 解析 allOf 中的单个 object-focused schema 条目。 + /// 每个条目只允许约束父对象已声明的字段,并且必须保持 object-typed 语义。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前 allOf 条目。 + /// 当前 allOf 条目的零基索引。 + /// 父对象已声明的属性集合。 + /// 解析后的 object-typed schema。 + private static YamlConfigSchemaNode ParseAllOfSchemaConstraint( + string tableName, + string schemaPath, + string propertyPath, + JsonElement allOfSchemaElement, + int allOfIndex, + IReadOnlyDictionary properties) + { + 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 allOfEntryLabel = $"Entry #{(allOfIndex + 1).ToString(CultureInfo.InvariantCulture)} in 'allOf'"; + var allOfSchemaPath = BuildNestedSchemaPath(propertyPath, $"allOf[{allOfIndex.ToString(CultureInfo.InvariantCulture)}]"); + ValidateInlineObjectSchemaTargetsAgainstParentObject( + tableName, + schemaPath, + propertyPath, + allOfSchemaPath, + allOfEntryLabel, + allOfSchemaElement, + properties); + + var allOfSchemaNode = ParseNode( + tableName, + schemaPath, + allOfSchemaPath, + allOfSchemaElement); + if (allOfSchemaNode.NodeType == YamlConfigSchemaPropertyType.Object) + { + return allOfSchemaNode; + } + + throw ConfigLoadExceptionFactory.Create( + ConfigLoadFailureKind.SchemaUnsupported, + tableName, + $"{allOfEntryLabel} for {DescribeObjectSchemaTargetInClause(propertyPath)} of schema file '{schemaPath}' must declare an object-typed schema.", + schemaPath: schemaPath, + displayPath: GetDiagnosticPath(allOfSchemaPath)); + } + /// /// 解析对象节点声明的 object-focused if / then / else 条件约束。 /// 当前共享子集要求三段内联 schema 都保持 object-typed focused block 语义, @@ -362,25 +473,7 @@ internal static partial class YamlConfigSchemaValidator 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)); - } + ValidateConditionalSchemaKeywordPresence(tableName, schemaPath, propertyPath, hasIf, hasThen, hasElse); var ifSchemaPath = BuildNestedSchemaPath(propertyPath, "if"); var ifSchemaNode = ParseConditionalObjectSchema( @@ -391,31 +484,100 @@ internal static partial class YamlConfigSchemaValidator "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; + var thenSchemaNode = ParseOptionalConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + "then", + hasThen, + thenElement, + properties); + var elseSchemaNode = ParseOptionalConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + "else", + hasElse, + elseElement, + properties); return new YamlConfigConditionalSchemas(ifSchemaNode, thenSchemaNode, elseSchemaNode); } + /// + /// 校验 object-focused 条件关键字的组合关系。 + /// thenelse 只能跟随 if,而单独的 if 没有可执行分支。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 是否声明 if。 + /// 是否声明 then。 + /// 是否声明 else。 + private static void ValidateConditionalSchemaKeywordPresence( + string tableName, + string schemaPath, + string propertyPath, + bool hasIf, + bool hasThen, + bool hasElse) + { + 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) + { + return; + } + + 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)); + } + + /// + /// 解析可选的 thenelse 条件分支。 + /// 未声明的分支保留为空,声明的分支必须通过 object-focused schema 校验。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 对象字段路径。 + /// 条件关键字名称。 + /// 是否声明该条件关键字。 + /// 条件关键字对应的 schema 节点。 + /// 父对象已声明的属性集合。 + /// 解析后的条件分支;未声明时返回空。 + private static YamlConfigSchemaNode? ParseOptionalConditionalObjectSchema( + string tableName, + string schemaPath, + string propertyPath, + string keywordName, + bool hasKeyword, + JsonElement keywordElement, + IReadOnlyDictionary properties) + { + return hasKeyword + ? ParseConditionalObjectSchema( + tableName, + schemaPath, + propertyPath, + BuildNestedSchemaPath(propertyPath, keywordName), + keywordName, + keywordElement, + properties) + : null; + } + /// /// 解析单个条件分支的 object-focused 内联 schema。 /// @@ -494,34 +656,96 @@ internal static partial class YamlConfigSchemaValidator JsonElement inlineSchemaElement, IReadOnlyDictionary properties) { - if (inlineSchemaElement.TryGetProperty("properties", out var inlinePropertiesElement)) + ValidateInlineObjectSchemaPropertiesAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + inlineSchemaElement, + properties); + + ValidateInlineRequiredPropertiesAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + inlineSchemaElement, + properties); + } + + /// + /// 校验 object-focused 内联 schema 的 properties 只引用父对象字段。 + /// focused block 不负责声明新字段,所以任何父对象未声明字段都会在 schema 加载时被拒绝。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前内联 schema。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineObjectSchemaPropertiesAgainstParentObject( + 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)); - } + return; } + 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)); + } + } + + /// + /// 校验 object-focused 内联 schema 的 required 只引用父对象字段。 + /// 该校验在加载期暴露不可满足的条件块,而不是等到运行时才发现无效字段名。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前内联 schema。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineRequiredPropertiesAgainstParentObject( + string tableName, + string schemaPath, + string propertyPath, + string inlineSchemaPath, + string entryLabel, + JsonElement inlineSchemaElement, + IReadOnlyDictionary properties) + { if (!inlineSchemaElement.TryGetProperty("required", out var inlineRequiredElement)) { return; @@ -539,39 +763,69 @@ internal static partial class YamlConfigSchemaValidator 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; - } + ValidateInlineRequiredPropertyAgainstParentObject( + tableName, + schemaPath, + propertyPath, + inlineSchemaPath, + entryLabel, + requiredProperty, + properties); + } + } + /// + /// 校验 object-focused 内联 schema 的单个 required 字段名。 + /// 字段名必须是非空字符串并且属于父对象声明范围,保持条件块与父对象形状一致。 + /// + /// 所属配置表名称。 + /// Schema 文件路径。 + /// 父对象路径。 + /// 当前内联 schema 路径。 + /// 用于诊断文本的条目标签。 + /// 当前 required 条目。 + /// 父对象已声明的属性集合。 + private static void ValidateInlineRequiredPropertyAgainstParentObject( + string tableName, + string schemaPath, + string propertyPath, + string inlineSchemaPath, + string entryLabel, + JsonElement requiredProperty, + IReadOnlyDictionary properties) + { + if (requiredProperty.ValueKind != JsonValueKind.String) + { 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.", + $"{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)) + { + return; + } + + 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)); } ///