mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(config): 添加配置验证和YAML解析功能
- 实现了JSON schema解析器和验证器 - 添加了YAML文档解析和注释提取功能 - 创建了配置验证诊断系统支持中英文本地化 - 实现了批量编辑器可编辑字段收集功能 - 添加了配置文件示例生成功能 - 实现了表单更新应用到YAML的功能 - 添加了精确十进制算术运算支持multipleOf约束检查 - 实现了YAML标量值格式化和引用处理 - 创建了完整的配置验证消息本地化系统
This commit is contained in:
parent
06d048f38a
commit
7931b41589
@ -1357,6 +1357,113 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象字段将 <c>minProperties</c> 声明为非法值时,会在 schema 解析阶段被拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Object_Property_Count_Constraint_Is_Not_NonNegative_Integer()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "reward"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": -1,
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterNestedConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("minProperties"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("non-negative integer"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象字段将 <c>minProperties</c> 声明为大于 <c>maxProperties</c> 时,会在 schema 解析阶段被拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Object_Property_Count_Constraints_Are_Inverted()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "reward"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 3,
|
||||||
|
"maxProperties": 2,
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"currency": { "type": "string" },
|
||||||
|
"tier": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterNestedConfigStub>("monster", "monster", "schemas/monster.schema.json",
|
||||||
|
static config => config.Id);
|
||||||
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.SchemaUnsupported));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("reward"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("minProperties"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("greater than 'maxProperties'"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证对象数组中的嵌套字段也会按 schema 递归校验。
|
/// 验证对象数组中的嵌套字段也会按 schema 递归校验。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -964,10 +964,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (minProperties.HasValue && maxProperties.HasValue && minProperties.Value > maxProperties.Value)
|
if (minProperties.HasValue && maxProperties.HasValue && minProperties.Value > maxProperties.Value)
|
||||||
{
|
{
|
||||||
|
var targetDescription = DescribeObjectSchemaTarget(propertyPath);
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
ConfigLoadFailureKind.SchemaUnsupported,
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
tableName,
|
tableName,
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minProperties' greater than 'maxProperties'.",
|
$"{targetDescription} in schema file '{schemaPath}' declares 'minProperties' greater than 'maxProperties'.",
|
||||||
schemaPath: schemaPath,
|
schemaPath: schemaPath,
|
||||||
displayPath: GetDiagnosticPath(propertyPath));
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
@ -1232,10 +1233,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
!constraintElement.TryGetInt32(out var constraintValue) ||
|
!constraintElement.TryGetInt32(out var constraintValue) ||
|
||||||
constraintValue < 0)
|
constraintValue < 0)
|
||||||
{
|
{
|
||||||
|
var targetDescription = DescribeObjectSchemaTarget(propertyPath);
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
ConfigLoadFailureKind.SchemaUnsupported,
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
tableName,
|
tableName,
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
|
$"{targetDescription} in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
|
||||||
schemaPath: schemaPath,
|
schemaPath: schemaPath,
|
||||||
displayPath: GetDiagnosticPath(propertyPath));
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
@ -1243,6 +1245,19 @@ internal static class YamlConfigSchemaValidator
|
|||||||
return constraintValue;
|
return constraintValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为对象级 schema 关键字构造稳定的诊断主体。
|
||||||
|
/// 根对象不会再显示为空字符串属性名,避免坏 schema 诊断出现 <c>Property ''</c> 之类的文本。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyPath">对象字段路径。</param>
|
||||||
|
/// <returns>用于错误消息的对象主体描述。</returns>
|
||||||
|
private static string DescribeObjectSchemaTarget(string propertyPath)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(propertyPath)
|
||||||
|
? "Root object"
|
||||||
|
: $"Property '{propertyPath}'";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取数组去重约束。
|
/// 读取数组去重约束。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -314,6 +314,48 @@ public class SchemaConfigGeneratorTests
|
|||||||
Does.Contain("Throwing here would permanently poison the cached index for this wrapper instance."));
|
Does.Contain("Throwing here would permanently poison the cached index for this wrapper instance."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证生成器对 <c>required</c> 名称保持大小写敏感,避免与运行时 validator 对同一 schema 产生分歧。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Treat_Required_Property_Names_As_Case_Sensitive()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
namespace TestApp
|
||||||
|
{
|
||||||
|
public sealed class Dummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string schema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "Name"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", schema));
|
||||||
|
|
||||||
|
var generatedSources = result.Results
|
||||||
|
.Single()
|
||||||
|
.GeneratedSources
|
||||||
|
.ToDictionary(
|
||||||
|
static sourceResult => sourceResult.HintName,
|
||||||
|
static sourceResult => sourceResult.SourceText.ToString(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
|
Assert.That(generatedSources["MonsterConfig.g.cs"], Does.Contain("public string? Name { get; set; }"));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证 schema 顶层自定义配置目录元数据不能逃逸配置根目录。
|
/// 验证 schema 顶层自定义配置目录元数据不能逃逸配置根目录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -220,7 +220,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
Path.GetFileName(filePath)));
|
Path.GetFileName(filePath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var requiredProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||||
if (element.TryGetProperty("required", out var requiredElement) &&
|
if (element.TryGetProperty("required", out var requiredElement) &&
|
||||||
requiredElement.ValueKind == JsonValueKind.Array)
|
requiredElement.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -958,24 +958,20 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
*/
|
*/
|
||||||
function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) {
|
function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) {
|
||||||
if (!yamlNode || yamlNode.kind !== "object") {
|
if (!yamlNode || yamlNode.kind !== "object") {
|
||||||
const subject = displayPath.length === 0
|
|
||||||
? localizer && localizer.isChinese
|
|
||||||
? "根对象应为对象。"
|
|
||||||
: "Root object is expected to be an object."
|
|
||||||
: localizer && localizer.isChinese
|
|
||||||
? `属性“${displayPath}”应为对象。`
|
|
||||||
: `Property '${displayPath}' is expected to be an object.`;
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage(ValidationMessageKeys.expectedObject, localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.expectedObject, localizer, {
|
||||||
subject,
|
|
||||||
displayPath
|
displayPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const propertyCount = Array.isArray(yamlNode.entries) ? yamlNode.entries.length : 0;
|
const propertyCount = yamlNode.map instanceof Map
|
||||||
|
? yamlNode.map.size
|
||||||
|
: Array.isArray(yamlNode.entries)
|
||||||
|
? new Set(yamlNode.entries.map((entry) => entry.key)).size
|
||||||
|
: 0;
|
||||||
|
|
||||||
for (const requiredProperty of schemaNode.required) {
|
for (const requiredProperty of schemaNode.required) {
|
||||||
if (!yamlNode.map.has(requiredProperty)) {
|
if (!yamlNode.map.has(requiredProperty)) {
|
||||||
@ -1090,6 +1086,10 @@ function buildComparableNodeValue(schemaNode, yamlNode) {
|
|||||||
* @returns {string} Localized validation message.
|
* @returns {string} Localized validation message.
|
||||||
*/
|
*/
|
||||||
function localizeValidationMessage(key, localizer, params) {
|
function localizeValidationMessage(key, localizer, params) {
|
||||||
|
if (key === ValidationMessageKeys.expectedObject) {
|
||||||
|
return formatExpectedObjectMessage(params.displayPath, Boolean(localizer && localizer.isChinese));
|
||||||
|
}
|
||||||
|
|
||||||
if (key === ValidationMessageKeys.minPropertiesViolation) {
|
if (key === ValidationMessageKeys.minPropertiesViolation) {
|
||||||
return formatObjectPropertyCountMessage(
|
return formatObjectPropertyCountMessage(
|
||||||
params.displayPath,
|
params.displayPath,
|
||||||
@ -1130,8 +1130,6 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `属性“${params.displayPath}”最多只能包含 ${params.value} 个元素。`;
|
return `属性“${params.displayPath}”最多只能包含 ${params.value} 个元素。`;
|
||||||
case ValidationMessageKeys.maxLengthViolation:
|
case ValidationMessageKeys.maxLengthViolation:
|
||||||
return `属性“${params.displayPath}”长度必须不超过 ${params.value} 个字符。`;
|
return `属性“${params.displayPath}”长度必须不超过 ${params.value} 个字符。`;
|
||||||
case ValidationMessageKeys.maxPropertiesViolation:
|
|
||||||
return formatObjectPropertyCountMessage(params.displayPath, params.value, "max", true);
|
|
||||||
case ValidationMessageKeys.minimumViolation:
|
case ValidationMessageKeys.minimumViolation:
|
||||||
return `属性“${params.displayPath}”必须大于或等于 ${params.value}。`;
|
return `属性“${params.displayPath}”必须大于或等于 ${params.value}。`;
|
||||||
case ValidationMessageKeys.multipleOfViolation:
|
case ValidationMessageKeys.multipleOfViolation:
|
||||||
@ -1140,14 +1138,10 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `属性“${params.displayPath}”至少需要包含 ${params.value} 个元素。`;
|
return `属性“${params.displayPath}”至少需要包含 ${params.value} 个元素。`;
|
||||||
case ValidationMessageKeys.minLengthViolation:
|
case ValidationMessageKeys.minLengthViolation:
|
||||||
return `属性“${params.displayPath}”长度必须至少为 ${params.value} 个字符。`;
|
return `属性“${params.displayPath}”长度必须至少为 ${params.value} 个字符。`;
|
||||||
case ValidationMessageKeys.minPropertiesViolation:
|
|
||||||
return formatObjectPropertyCountMessage(params.displayPath, params.value, "min", true);
|
|
||||||
case ValidationMessageKeys.patternViolation:
|
case ValidationMessageKeys.patternViolation:
|
||||||
return `属性“${params.displayPath}”必须匹配正则模式“${params.value}”。`;
|
return `属性“${params.displayPath}”必须匹配正则模式“${params.value}”。`;
|
||||||
case ValidationMessageKeys.uniqueItemsViolation:
|
case ValidationMessageKeys.uniqueItemsViolation:
|
||||||
return `属性“${params.displayPath}”与更早的数组元素 ${params.duplicatePath} 重复;该数组要求元素唯一。`;
|
return `属性“${params.displayPath}”与更早的数组元素 ${params.duplicatePath} 重复;该数组要求元素唯一。`;
|
||||||
case ValidationMessageKeys.expectedObject:
|
|
||||||
return params.subject;
|
|
||||||
case ValidationMessageKeys.missingRequired:
|
case ValidationMessageKeys.missingRequired:
|
||||||
return `缺少必填属性“${params.displayPath}”。`;
|
return `缺少必填属性“${params.displayPath}”。`;
|
||||||
case ValidationMessageKeys.unknownProperty:
|
case ValidationMessageKeys.unknownProperty:
|
||||||
@ -1176,8 +1170,6 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `Property '${params.displayPath}' must contain at most ${params.value} items.`;
|
return `Property '${params.displayPath}' must contain at most ${params.value} items.`;
|
||||||
case ValidationMessageKeys.maxLengthViolation:
|
case ValidationMessageKeys.maxLengthViolation:
|
||||||
return `Property '${params.displayPath}' must be at most ${params.value} characters long.`;
|
return `Property '${params.displayPath}' must be at most ${params.value} characters long.`;
|
||||||
case ValidationMessageKeys.maxPropertiesViolation:
|
|
||||||
return formatObjectPropertyCountMessage(params.displayPath, params.value, "max", false);
|
|
||||||
case ValidationMessageKeys.minimumViolation:
|
case ValidationMessageKeys.minimumViolation:
|
||||||
return `Property '${params.displayPath}' must be greater than or equal to ${params.value}.`;
|
return `Property '${params.displayPath}' must be greater than or equal to ${params.value}.`;
|
||||||
case ValidationMessageKeys.multipleOfViolation:
|
case ValidationMessageKeys.multipleOfViolation:
|
||||||
@ -1186,14 +1178,10 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `Property '${params.displayPath}' must contain at least ${params.value} items.`;
|
return `Property '${params.displayPath}' must contain at least ${params.value} items.`;
|
||||||
case ValidationMessageKeys.minLengthViolation:
|
case ValidationMessageKeys.minLengthViolation:
|
||||||
return `Property '${params.displayPath}' must be at least ${params.value} characters long.`;
|
return `Property '${params.displayPath}' must be at least ${params.value} characters long.`;
|
||||||
case ValidationMessageKeys.minPropertiesViolation:
|
|
||||||
return formatObjectPropertyCountMessage(params.displayPath, params.value, "min", false);
|
|
||||||
case ValidationMessageKeys.patternViolation:
|
case ValidationMessageKeys.patternViolation:
|
||||||
return `Property '${params.displayPath}' must match pattern '${params.value}'.`;
|
return `Property '${params.displayPath}' must match pattern '${params.value}'.`;
|
||||||
case ValidationMessageKeys.uniqueItemsViolation:
|
case ValidationMessageKeys.uniqueItemsViolation:
|
||||||
return `Property '${params.displayPath}' duplicates earlier array item '${params.duplicatePath}', but uniqueItems is required.`;
|
return `Property '${params.displayPath}' duplicates earlier array item '${params.duplicatePath}', but uniqueItems is required.`;
|
||||||
case ValidationMessageKeys.expectedObject:
|
|
||||||
return params.subject;
|
|
||||||
case ValidationMessageKeys.missingRequired:
|
case ValidationMessageKeys.missingRequired:
|
||||||
return `Required property '${params.displayPath}' is missing.`;
|
return `Required property '${params.displayPath}' is missing.`;
|
||||||
case ValidationMessageKeys.unknownProperty:
|
case ValidationMessageKeys.unknownProperty:
|
||||||
@ -1203,6 +1191,26 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format one object-shape expectation diagnostic.
|
||||||
|
*
|
||||||
|
* @param {string} displayPath Logical object path, or empty for the root object.
|
||||||
|
* @param {boolean} isChinese Whether Chinese text should be produced.
|
||||||
|
* @returns {string} Formatted message.
|
||||||
|
*/
|
||||||
|
function formatExpectedObjectMessage(displayPath, isChinese) {
|
||||||
|
const isRoot = !displayPath;
|
||||||
|
if (isChinese) {
|
||||||
|
return isRoot
|
||||||
|
? "根对象应为对象。"
|
||||||
|
: `属性“${displayPath}”应为对象。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRoot
|
||||||
|
? "Root object is expected to be an object."
|
||||||
|
: `Property '${displayPath}' is expected to be an object.`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format one object-property-count validation message.
|
* Format one object-property-count validation message.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -136,17 +136,14 @@ const enMessages = {
|
|||||||
[ValidationMessageKeys.maximumViolation]: "Property '{displayPath}' must be less than or equal to {value}.",
|
[ValidationMessageKeys.maximumViolation]: "Property '{displayPath}' must be less than or equal to {value}.",
|
||||||
[ValidationMessageKeys.maxItemsViolation]: "Property '{displayPath}' must contain at most {value} items.",
|
[ValidationMessageKeys.maxItemsViolation]: "Property '{displayPath}' must contain at most {value} items.",
|
||||||
[ValidationMessageKeys.maxLengthViolation]: "Property '{displayPath}' must be at most {value} characters long.",
|
[ValidationMessageKeys.maxLengthViolation]: "Property '{displayPath}' must be at most {value} characters long.",
|
||||||
[ValidationMessageKeys.maxPropertiesViolation]: "Property '{displayPath}' must contain at most {value} properties.",
|
|
||||||
[ValidationMessageKeys.minimumViolation]: "Property '{displayPath}' must be greater than or equal to {value}.",
|
[ValidationMessageKeys.minimumViolation]: "Property '{displayPath}' must be greater than or equal to {value}.",
|
||||||
[ValidationMessageKeys.multipleOfViolation]: "Property '{displayPath}' must be a multiple of {value}.",
|
[ValidationMessageKeys.multipleOfViolation]: "Property '{displayPath}' must be a multiple of {value}.",
|
||||||
[ValidationMessageKeys.minItemsViolation]: "Property '{displayPath}' must contain at least {value} items.",
|
[ValidationMessageKeys.minItemsViolation]: "Property '{displayPath}' must contain at least {value} items.",
|
||||||
[ValidationMessageKeys.minLengthViolation]: "Property '{displayPath}' must be at least {value} characters long.",
|
[ValidationMessageKeys.minLengthViolation]: "Property '{displayPath}' must be at least {value} characters long.",
|
||||||
[ValidationMessageKeys.minPropertiesViolation]: "Property '{displayPath}' must contain at least {value} properties.",
|
|
||||||
[ValidationMessageKeys.patternViolation]: "Property '{displayPath}' must match pattern '{value}'.",
|
[ValidationMessageKeys.patternViolation]: "Property '{displayPath}' must match pattern '{value}'.",
|
||||||
[ValidationMessageKeys.uniqueItemsViolation]: "Property '{displayPath}' duplicates earlier array item '{duplicatePath}', but uniqueItems is required.",
|
[ValidationMessageKeys.uniqueItemsViolation]: "Property '{displayPath}' duplicates earlier array item '{duplicatePath}', but uniqueItems is required.",
|
||||||
[ValidationMessageKeys.enumMismatch]: "Property '{displayPath}' must be one of: {values}.",
|
[ValidationMessageKeys.enumMismatch]: "Property '{displayPath}' must be one of: {values}.",
|
||||||
[ValidationMessageKeys.expectedArray]: "Property '{displayPath}' is expected to be an array.",
|
[ValidationMessageKeys.expectedArray]: "Property '{displayPath}' is expected to be an array.",
|
||||||
[ValidationMessageKeys.expectedObject]: "{subject} is expected to be an object.",
|
|
||||||
[ValidationMessageKeys.expectedScalarShape]: "Property '{displayPath}' is expected to be '{schemaType}', but the current YAML shape is '{yamlKind}'.",
|
[ValidationMessageKeys.expectedScalarShape]: "Property '{displayPath}' is expected to be '{schemaType}', but the current YAML shape is '{yamlKind}'.",
|
||||||
[ValidationMessageKeys.expectedScalarValue]: "Property '{displayPath}' is expected to be '{schemaType}', but the current scalar value is incompatible.",
|
[ValidationMessageKeys.expectedScalarValue]: "Property '{displayPath}' is expected to be '{schemaType}', but the current scalar value is incompatible.",
|
||||||
[ValidationMessageKeys.missingRequired]: "Required property '{displayPath}' is missing.",
|
[ValidationMessageKeys.missingRequired]: "Required property '{displayPath}' is missing.",
|
||||||
@ -244,17 +241,14 @@ const zhCnMessages = {
|
|||||||
[ValidationMessageKeys.maximumViolation]: "属性“{displayPath}”必须小于或等于 {value}。",
|
[ValidationMessageKeys.maximumViolation]: "属性“{displayPath}”必须小于或等于 {value}。",
|
||||||
[ValidationMessageKeys.maxItemsViolation]: "属性“{displayPath}”最多只能包含 {value} 个元素。",
|
[ValidationMessageKeys.maxItemsViolation]: "属性“{displayPath}”最多只能包含 {value} 个元素。",
|
||||||
[ValidationMessageKeys.maxLengthViolation]: "属性“{displayPath}”长度必须不超过 {value} 个字符。",
|
[ValidationMessageKeys.maxLengthViolation]: "属性“{displayPath}”长度必须不超过 {value} 个字符。",
|
||||||
[ValidationMessageKeys.maxPropertiesViolation]: "对象属性“{displayPath}”最多只能包含 {value} 个子属性。",
|
|
||||||
[ValidationMessageKeys.minimumViolation]: "属性“{displayPath}”必须大于或等于 {value}。",
|
[ValidationMessageKeys.minimumViolation]: "属性“{displayPath}”必须大于或等于 {value}。",
|
||||||
[ValidationMessageKeys.multipleOfViolation]: "属性“{displayPath}”必须是 {value} 的整数倍。",
|
[ValidationMessageKeys.multipleOfViolation]: "属性“{displayPath}”必须是 {value} 的整数倍。",
|
||||||
[ValidationMessageKeys.minItemsViolation]: "属性“{displayPath}”至少需要包含 {value} 个元素。",
|
[ValidationMessageKeys.minItemsViolation]: "属性“{displayPath}”至少需要包含 {value} 个元素。",
|
||||||
[ValidationMessageKeys.minLengthViolation]: "属性“{displayPath}”长度必须至少为 {value} 个字符。",
|
[ValidationMessageKeys.minLengthViolation]: "属性“{displayPath}”长度必须至少为 {value} 个字符。",
|
||||||
[ValidationMessageKeys.minPropertiesViolation]: "对象属性“{displayPath}”至少需要包含 {value} 个子属性。",
|
|
||||||
[ValidationMessageKeys.patternViolation]: "属性“{displayPath}”必须匹配正则模式“{value}”。",
|
[ValidationMessageKeys.patternViolation]: "属性“{displayPath}”必须匹配正则模式“{value}”。",
|
||||||
[ValidationMessageKeys.uniqueItemsViolation]: "属性“{displayPath}”与更早的数组元素“{duplicatePath}”重复;该数组要求元素唯一。",
|
[ValidationMessageKeys.uniqueItemsViolation]: "属性“{displayPath}”与更早的数组元素“{duplicatePath}”重复;该数组要求元素唯一。",
|
||||||
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
||||||
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
||||||
[ValidationMessageKeys.expectedObject]: "{subject}",
|
|
||||||
[ValidationMessageKeys.expectedScalarShape]: "属性“{displayPath}”应为“{schemaType}”,但当前 YAML 结构是“{yamlKind}”。",
|
[ValidationMessageKeys.expectedScalarShape]: "属性“{displayPath}”应为“{schemaType}”,但当前 YAML 结构是“{yamlKind}”。",
|
||||||
[ValidationMessageKeys.expectedScalarValue]: "属性“{displayPath}”应为“{schemaType}”,但当前标量值不兼容。",
|
[ValidationMessageKeys.expectedScalarValue]: "属性“{displayPath}”应为“{schemaType}”,但当前标量值不兼容。",
|
||||||
[ValidationMessageKeys.missingRequired]: "缺少必填属性“{displayPath}”。",
|
[ValidationMessageKeys.missingRequired]: "缺少必填属性“{displayPath}”。",
|
||||||
|
|||||||
@ -343,6 +343,34 @@ reward:
|
|||||||
assert.ok(messages.some((message) => /reward.*at most 2 properties|reward.*最多只能包含 2 个子属性/u.test(message)));
|
assert.ok(messages.some((message) => /reward.*at most 2 properties|reward.*最多只能包含 2 个子属性/u.test(message)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should count unique object properties for property-count constraints", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 2,
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" },
|
||||||
|
"currency": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
reward:
|
||||||
|
gold: 10
|
||||||
|
gold: 20
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 1);
|
||||||
|
assert.match(diagnostics[0].message, /reward.*at least 2 properties|reward.*至少需要包含 2 个子属性/u);
|
||||||
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should report multipleOf and uniqueItems violations", () => {
|
test("validateParsedConfig should report multipleOf and uniqueItems violations", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
@ -735,6 +763,30 @@ id: 1
|
|||||||
assert.match(diagnostics[1].message, /未在匹配的 schema 中声明/u);
|
assert.match(diagnostics[1].message, /未在匹配的 schema 中声明/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should localize expected object diagnostics when Chinese UI is requested", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"reward": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"gold": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
reward: 1
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml, {isChinese: true});
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 1);
|
||||||
|
assert.equal(diagnostics[0].message, "属性“reward”应为对象。");
|
||||||
|
});
|
||||||
|
|
||||||
test("applyFormUpdates should update nested scalar and scalar-array paths", () => {
|
test("applyFormUpdates should update nested scalar and scalar-array paths", () => {
|
||||||
const updated = applyFormUpdates(
|
const updated = applyFormUpdates(
|
||||||
[
|
[
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user