mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(game): 添加游戏内容配置系统和YAML配置校验器
- 实现面向静态游戏内容的AI-First配置方案,支持怪物、物品、技能、任务等数据管理 - 集成YAML作为配置源文件格式,JSON Schema作为结构描述标准 - 提供一对象一文件的目录组织结构和运行时只读查询功能 - 实现Source Generator生成配置类型、表包装和注册/访问辅助代码 - 添加VS Code插件支持配置浏览、raw编辑、schema打开和递归校验功能 - 创建YamlConfigSchemaValidator类提供YAML与JSON Schema的运行时校验能力 - 支持嵌套对象、对象数组、标量数组的递归校验和深层约束检查 - 实现跨表引用验证和配置热重载功能 - 提供详细的错误诊断信息和开发期工具链支持
This commit is contained in:
parent
f63714f1e1
commit
0e538738df
@ -77,6 +77,11 @@ public enum ConfigLoadFailureKind
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
EnumValueNotAllowed,
|
EnumValueNotAllowed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 标量值违反了 schema 声明的最小值、最大值或长度约束。
|
||||||
|
/// </summary>
|
||||||
|
ConstraintViolation,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// YAML 可被读取,但无法成功反序列化到目标 CLR 类型。
|
/// YAML 可被读取,但无法成功反序列化到目标 CLR 类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -301,6 +301,104 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证数值最小值与最大值约束会在运行时被统一拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_Number_Violates_Minimum_Or_Maximum()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
hp: 101
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "hp"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"hp": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigStub>("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.ConstraintViolation));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("hp"));
|
||||||
|
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("101"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("100"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证字符串最小长度与最大长度约束会在运行时被统一拒绝。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void LoadAsync_Should_Throw_When_String_Violates_MinLength_Or_MaxLength()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Sl
|
||||||
|
hp: 10
|
||||||
|
""");
|
||||||
|
CreateSchemaFile(
|
||||||
|
"schemas/monster.schema.json",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "name", "hp"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 12
|
||||||
|
},
|
||||||
|
"hp": { "type": "integer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigStub>("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.ConstraintViolation));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("name"));
|
||||||
|
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("Sl"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("at least 3 characters"));
|
||||||
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -296,6 +296,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
itemNode: null,
|
itemNode: null,
|
||||||
referenceTableName: null,
|
referenceTableName: null,
|
||||||
allowedValues: null,
|
allowedValues: null,
|
||||||
|
constraints: null,
|
||||||
schemaPath);
|
schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +364,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
itemNode,
|
itemNode,
|
||||||
referenceTableName: null,
|
referenceTableName: null,
|
||||||
allowedValues: null,
|
allowedValues: null,
|
||||||
|
constraints: null,
|
||||||
schemaPath);
|
schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,6 +394,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
itemNode: null,
|
itemNode: null,
|
||||||
referenceTableName,
|
referenceTableName,
|
||||||
ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"),
|
ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"),
|
||||||
|
ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
|
||||||
schemaPath);
|
schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,6 +677,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
detail: $"Allowed values: {string.Join(", ", schemaNode.AllowedValues)}.");
|
detail: $"Allowed values: {string.Join(", ", schemaNode.AllowedValues)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schemaNode.Constraints is not null)
|
||||||
|
{
|
||||||
|
ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode);
|
||||||
|
}
|
||||||
|
|
||||||
if (schemaNode.ReferenceTableName != null)
|
if (schemaNode.ReferenceTableName != null)
|
||||||
{
|
{
|
||||||
references.Add(
|
references.Add(
|
||||||
@ -730,6 +738,246 @@ internal static class YamlConfigSchemaValidator
|
|||||||
return allowedValues;
|
return allowedValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析标量字段支持的范围与长度约束。
|
||||||
|
/// 当前共享子集只支持 `integer/number` 上的 `minimum/maximum` 和 `string` 上的 `minLength/maxLength`。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="nodeType">标量类型。</param>
|
||||||
|
/// <returns>解析后的约束模型;未声明时返回空。</returns>
|
||||||
|
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 minLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minLength");
|
||||||
|
var maxLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maxLength");
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && !minLength.HasValue && !maxLength.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new YamlConfigScalarConstraints(minimum, maximum, minLength, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取数值区间约束。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="nodeType">字段类型。</param>
|
||||||
|
/// <param name="keywordName">关键字名称。</param>
|
||||||
|
/// <returns>数值约束;未声明时返回空。</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取字符串长度约束。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="nodeType">字段类型。</param>
|
||||||
|
/// <param name="keywordName">关键字名称。</param>
|
||||||
|
/// <returns>长度约束;未声明时返回空。</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验标量值是否满足范围与长度约束。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
|
/// <param name="displayPath">字段路径。</param>
|
||||||
|
/// <param name="rawValue">原始 YAML 标量值。</param>
|
||||||
|
/// <param name="normalizedValue">归一化后的比较值。</param>
|
||||||
|
/// <param name="schemaNode">标量 schema 节点。</param>
|
||||||
|
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:
|
||||||
|
var numericValue = double.Parse(normalizedValue, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
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.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)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析跨表引用目标表名称。
|
/// 解析跨表引用目标表名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1037,6 +1285,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
/// <param name="itemNode">数组元素节点。</param>
|
/// <param name="itemNode">数组元素节点。</param>
|
||||||
/// <param name="referenceTableName">目标引用表名称。</param>
|
/// <param name="referenceTableName">目标引用表名称。</param>
|
||||||
/// <param name="allowedValues">标量允许值集合。</param>
|
/// <param name="allowedValues">标量允许值集合。</param>
|
||||||
|
/// <param name="constraints">标量范围与长度约束。</param>
|
||||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||||
public YamlConfigSchemaNode(
|
public YamlConfigSchemaNode(
|
||||||
YamlConfigSchemaPropertyType nodeType,
|
YamlConfigSchemaPropertyType nodeType,
|
||||||
@ -1045,6 +1294,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
YamlConfigSchemaNode? itemNode,
|
YamlConfigSchemaNode? itemNode,
|
||||||
string? referenceTableName,
|
string? referenceTableName,
|
||||||
IReadOnlyCollection<string>? allowedValues,
|
IReadOnlyCollection<string>? allowedValues,
|
||||||
|
YamlConfigScalarConstraints? constraints,
|
||||||
string schemaPathHint)
|
string schemaPathHint)
|
||||||
{
|
{
|
||||||
NodeType = nodeType;
|
NodeType = nodeType;
|
||||||
@ -1053,6 +1303,7 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
ItemNode = itemNode;
|
ItemNode = itemNode;
|
||||||
ReferenceTableName = referenceTableName;
|
ReferenceTableName = referenceTableName;
|
||||||
AllowedValues = allowedValues;
|
AllowedValues = allowedValues;
|
||||||
|
Constraints = constraints;
|
||||||
SchemaPathHint = schemaPathHint;
|
SchemaPathHint = schemaPathHint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1086,6 +1337,11 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<string>? AllowedValues { get; }
|
public IReadOnlyCollection<string>? AllowedValues { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取标量范围与长度约束;未声明时返回空。
|
||||||
|
/// </summary>
|
||||||
|
public YamlConfigScalarConstraints? Constraints { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取用于诊断显示的 schema 路径提示。
|
/// 获取用于诊断显示的 schema 路径提示。
|
||||||
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
|
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
|
||||||
@ -1107,10 +1363,57 @@ internal sealed class YamlConfigSchemaNode
|
|||||||
ItemNode,
|
ItemNode,
|
||||||
referenceTableName,
|
referenceTableName,
|
||||||
AllowedValues,
|
AllowedValues,
|
||||||
|
Constraints,
|
||||||
SchemaPathHint);
|
SchemaPathHint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一个标量节点上声明的数值范围或字符串长度约束。
|
||||||
|
/// 该模型让运行时、热重载和跨文件诊断都能复用同一份最小约束信息。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class YamlConfigScalarConstraints
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化标量约束模型。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minimum">最小值约束。</param>
|
||||||
|
/// <param name="maximum">最大值约束。</param>
|
||||||
|
/// <param name="minLength">最小长度约束。</param>
|
||||||
|
/// <param name="maxLength">最大长度约束。</param>
|
||||||
|
public YamlConfigScalarConstraints(
|
||||||
|
double? minimum,
|
||||||
|
double? maximum,
|
||||||
|
int? minLength,
|
||||||
|
int? maxLength)
|
||||||
|
{
|
||||||
|
Minimum = minimum;
|
||||||
|
Maximum = maximum;
|
||||||
|
MinLength = minLength;
|
||||||
|
MaxLength = maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最小值约束。
|
||||||
|
/// </summary>
|
||||||
|
public double? Minimum { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最大值约束。
|
||||||
|
/// </summary>
|
||||||
|
public double? Maximum { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最小长度约束。
|
||||||
|
/// </summary>
|
||||||
|
public int? MinLength { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取最大长度约束。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxLength { get; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示单个 YAML 文件中提取出的跨表引用。
|
/// 表示单个 YAML 文件中提取出的跨表引用。
|
||||||
/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。
|
/// 该模型保留源文件、字段路径和目标表等诊断信息,以便加载器在批量校验失败时给出可定位的错误。
|
||||||
|
|||||||
@ -79,11 +79,15 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Monster Name",
|
"title": "Monster Name",
|
||||||
"description": "Localized monster display name.",
|
"description": "Localized monster display name.",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 16,
|
||||||
"default": "Slime",
|
"default": "Slime",
|
||||||
"enum": ["Slime", "Goblin"]
|
"enum": ["Slime", "Goblin"]
|
||||||
},
|
},
|
||||||
"hp": {
|
"hp": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 999,
|
||||||
"default": 10
|
"default": 10
|
||||||
},
|
},
|
||||||
"dropItems": {
|
"dropItems": {
|
||||||
@ -91,6 +95,8 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 12,
|
||||||
"enum": ["potion", "slime_gel"]
|
"enum": ["potion", "slime_gel"]
|
||||||
},
|
},
|
||||||
"default": ["potion"],
|
"default": ["potion"],
|
||||||
@ -103,6 +109,7 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
"properties": {
|
"properties": {
|
||||||
"gold": {
|
"gold": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
"default": 10
|
"default": 10
|
||||||
},
|
},
|
||||||
"currency": {
|
"currency": {
|
||||||
@ -123,7 +130,9 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
},
|
},
|
||||||
"monsterId": {
|
"monsterId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Monster reference id."
|
"description": "Monster reference id.",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ public sealed partial class MonsterConfig
|
|||||||
/// Schema property path: 'name'.
|
/// Schema property path: 'name'.
|
||||||
/// Display title: 'Monster Name'.
|
/// Display title: 'Monster Name'.
|
||||||
/// Allowed values: Slime, Goblin.
|
/// Allowed values: Slime, Goblin.
|
||||||
|
/// Constraints: minLength = 3, maxLength = 16.
|
||||||
/// Generated default initializer: = "Slime";
|
/// Generated default initializer: = "Slime";
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string Name { get; set; } = "Slime";
|
public string Name { get; set; } = "Slime";
|
||||||
@ -33,6 +34,7 @@ public sealed partial class MonsterConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Schema property path: 'hp'.
|
/// Schema property path: 'hp'.
|
||||||
|
/// Constraints: minimum = 1, maximum = 999.
|
||||||
/// Generated default initializer: = 10;
|
/// Generated default initializer: = 10;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public int? Hp { get; set; } = 10;
|
public int? Hp { get; set; } = 10;
|
||||||
@ -44,6 +46,7 @@ public sealed partial class MonsterConfig
|
|||||||
/// Schema property path: 'dropItems'.
|
/// Schema property path: 'dropItems'.
|
||||||
/// Allowed values: potion, slime_gel.
|
/// Allowed values: potion, slime_gel.
|
||||||
/// References config table: 'item'.
|
/// References config table: 'item'.
|
||||||
|
/// Item constraints: minLength = 3, maxLength = 12.
|
||||||
/// Generated default initializer: = new string[] { "potion" };
|
/// Generated default initializer: = new string[] { "potion" };
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public global::System.Collections.Generic.IReadOnlyList<string> DropItems { get; set; } = new string[] { "potion" };
|
public global::System.Collections.Generic.IReadOnlyList<string> DropItems { get; set; } = new string[] { "potion" };
|
||||||
@ -77,6 +80,7 @@ public sealed partial class MonsterConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Schema property path: 'reward.gold'.
|
/// Schema property path: 'reward.gold'.
|
||||||
|
/// Constraints: minimum = 0.
|
||||||
/// Generated default initializer: = 10;
|
/// Generated default initializer: = 10;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public int Gold { get; set; } = 10;
|
public int Gold { get; set; } = 10;
|
||||||
@ -112,6 +116,7 @@ public sealed partial class MonsterConfig
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Schema property path: 'phases[].monsterId'.
|
/// Schema property path: 'phases[].monsterId'.
|
||||||
|
/// Constraints: minLength = 2, maxLength = 32.
|
||||||
/// Generated default initializer: = string.Empty;
|
/// Generated default initializer: = string.Empty;
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string MonsterId { get; set; } = string.Empty;
|
public string MonsterId { get; set; } = string.Empty;
|
||||||
|
|||||||
@ -271,6 +271,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
isRequired ? "int" : "int?",
|
isRequired ? "int" : "int?",
|
||||||
TryBuildScalarInitializer(property.Value, "integer"),
|
TryBuildScalarInitializer(property.Value, "integer"),
|
||||||
TryBuildEnumDocumentation(property.Value, "integer"),
|
TryBuildEnumDocumentation(property.Value, "integer"),
|
||||||
|
TryBuildConstraintDocumentation(property.Value, "integer"),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
null)));
|
null)));
|
||||||
@ -289,6 +290,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
isRequired ? "double" : "double?",
|
isRequired ? "double" : "double?",
|
||||||
TryBuildScalarInitializer(property.Value, "number"),
|
TryBuildScalarInitializer(property.Value, "number"),
|
||||||
TryBuildEnumDocumentation(property.Value, "number"),
|
TryBuildEnumDocumentation(property.Value, "number"),
|
||||||
|
TryBuildConstraintDocumentation(property.Value, "number"),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
null)));
|
null)));
|
||||||
@ -307,6 +309,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
isRequired ? "bool" : "bool?",
|
isRequired ? "bool" : "bool?",
|
||||||
TryBuildScalarInitializer(property.Value, "boolean"),
|
TryBuildScalarInitializer(property.Value, "boolean"),
|
||||||
TryBuildEnumDocumentation(property.Value, "boolean"),
|
TryBuildEnumDocumentation(property.Value, "boolean"),
|
||||||
|
TryBuildConstraintDocumentation(property.Value, "boolean"),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
null)));
|
null)));
|
||||||
@ -326,6 +329,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
TryBuildScalarInitializer(property.Value, "string") ??
|
TryBuildScalarInitializer(property.Value, "string") ??
|
||||||
(isRequired ? " = string.Empty;" : null),
|
(isRequired ? " = string.Empty;" : null),
|
||||||
TryBuildEnumDocumentation(property.Value, "string"),
|
TryBuildEnumDocumentation(property.Value, "string"),
|
||||||
|
TryBuildConstraintDocumentation(property.Value, "string"),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
null)));
|
null)));
|
||||||
@ -367,6 +371,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
isRequired ? " = new();" : null,
|
isRequired ? " = new();" : null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
objectSpec,
|
objectSpec,
|
||||||
null)));
|
null)));
|
||||||
|
|
||||||
@ -450,6 +455,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
|
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
|
||||||
$" = global::System.Array.Empty<{itemClrType}>();",
|
$" = global::System.Array.Empty<{itemClrType}>();",
|
||||||
TryBuildEnumDocumentation(itemsElement, itemType),
|
TryBuildEnumDocumentation(itemsElement, itemType),
|
||||||
|
null,
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
new SchemaTypeSpec(
|
new SchemaTypeSpec(
|
||||||
@ -458,6 +464,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
itemClrType,
|
itemClrType,
|
||||||
null,
|
null,
|
||||||
TryBuildEnumDocumentation(itemsElement, itemType),
|
TryBuildEnumDocumentation(itemsElement, itemType),
|
||||||
|
TryBuildConstraintDocumentation(itemsElement, itemType),
|
||||||
refTableName,
|
refTableName,
|
||||||
null,
|
null,
|
||||||
null))));
|
null))));
|
||||||
@ -500,6 +507,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
new SchemaTypeSpec(
|
new SchemaTypeSpec(
|
||||||
SchemaNodeKind.Object,
|
SchemaNodeKind.Object,
|
||||||
"object",
|
"object",
|
||||||
@ -507,6 +515,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
objectSpec,
|
objectSpec,
|
||||||
null))));
|
null))));
|
||||||
|
|
||||||
@ -872,12 +881,26 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
$"{indent}/// Allowed values: {EscapeXmlDocumentation(property.TypeSpec.EnumDocumentation!)}.");
|
$"{indent}/// Allowed values: {EscapeXmlDocumentation(property.TypeSpec.EnumDocumentation!)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(property.TypeSpec.ConstraintDocumentation))
|
||||||
|
{
|
||||||
|
builder.AppendLine(
|
||||||
|
$"{indent}/// Constraints: {EscapeXmlDocumentation(property.TypeSpec.ConstraintDocumentation!)}.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(property.TypeSpec.RefTableName))
|
if (!string.IsNullOrWhiteSpace(property.TypeSpec.RefTableName))
|
||||||
{
|
{
|
||||||
builder.AppendLine(
|
builder.AppendLine(
|
||||||
$"{indent}/// References config table: '{EscapeXmlDocumentation(property.TypeSpec.RefTableName!)}'.");
|
$"{indent}/// References config table: '{EscapeXmlDocumentation(property.TypeSpec.RefTableName!)}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var itemConstraintDocumentation = property.TypeSpec.ItemTypeSpec?.ConstraintDocumentation;
|
||||||
|
if (property.TypeSpec.Kind == SchemaNodeKind.Array &&
|
||||||
|
!string.IsNullOrWhiteSpace(itemConstraintDocumentation))
|
||||||
|
{
|
||||||
|
builder.AppendLine(
|
||||||
|
$"{indent}/// Item constraints: {EscapeXmlDocumentation(itemConstraintDocumentation!)}.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(property.TypeSpec.Initializer))
|
if (!string.IsNullOrWhiteSpace(property.TypeSpec.Initializer))
|
||||||
{
|
{
|
||||||
builder.AppendLine(
|
builder.AppendLine(
|
||||||
@ -1084,6 +1107,82 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
return values.Count > 0 ? string.Join(", ", values) : null;
|
return values.Count > 0 ? string.Join(", ", values) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 shared schema 子集中的范围与长度约束整理成 XML 文档可读字符串。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="schemaType">标量类型。</param>
|
||||||
|
/// <returns>格式化后的约束说明。</returns>
|
||||||
|
private static string? TryBuildConstraintDocumentation(JsonElement element, string schemaType)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
|
||||||
|
if ((schemaType == "integer" || schemaType == "number") &&
|
||||||
|
TryGetFiniteNumber(element, "minimum", out var minimum))
|
||||||
|
{
|
||||||
|
parts.Add($"minimum = {minimum.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((schemaType == "integer" || schemaType == "number") &&
|
||||||
|
TryGetFiniteNumber(element, "maximum", out var maximum))
|
||||||
|
{
|
||||||
|
parts.Add($"maximum = {maximum.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaType == "string" &&
|
||||||
|
TryGetNonNegativeInt32(element, "minLength", out var minLength))
|
||||||
|
{
|
||||||
|
parts.Add($"minLength = {minLength.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schemaType == "string" &&
|
||||||
|
TryGetNonNegativeInt32(element, "maxLength", out var maxLength))
|
||||||
|
{
|
||||||
|
parts.Add($"maxLength = {maxLength.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.Count > 0 ? string.Join(", ", parts) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取有限数值元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="propertyName">元数据名称。</param>
|
||||||
|
/// <param name="value">读取到的数值。</param>
|
||||||
|
/// <returns>是否读取成功。</returns>
|
||||||
|
private static bool TryGetFiniteNumber(
|
||||||
|
JsonElement element,
|
||||||
|
string propertyName,
|
||||||
|
out double value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
return element.TryGetProperty(propertyName, out var metadataElement) &&
|
||||||
|
metadataElement.ValueKind == JsonValueKind.Number &&
|
||||||
|
metadataElement.TryGetDouble(out value) &&
|
||||||
|
!double.IsNaN(value) &&
|
||||||
|
!double.IsInfinity(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取非负整数元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">Schema 节点。</param>
|
||||||
|
/// <param name="propertyName">元数据名称。</param>
|
||||||
|
/// <param name="value">读取到的整数值。</param>
|
||||||
|
/// <returns>是否读取成功。</returns>
|
||||||
|
private static bool TryGetNonNegativeInt32(
|
||||||
|
JsonElement element,
|
||||||
|
string propertyName,
|
||||||
|
out int value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
return element.TryGetProperty(propertyName, out var metadataElement) &&
|
||||||
|
metadataElement.ValueKind == JsonValueKind.Number &&
|
||||||
|
metadataElement.TryGetInt32(out value) &&
|
||||||
|
value >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 组合逻辑字段路径。
|
/// 组合逻辑字段路径。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1221,6 +1320,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
/// <param name="ClrType">CLR 类型名。</param>
|
/// <param name="ClrType">CLR 类型名。</param>
|
||||||
/// <param name="Initializer">属性初始化器。</param>
|
/// <param name="Initializer">属性初始化器。</param>
|
||||||
/// <param name="EnumDocumentation">枚举文档说明。</param>
|
/// <param name="EnumDocumentation">枚举文档说明。</param>
|
||||||
|
/// <param name="ConstraintDocumentation">范围或长度约束说明。</param>
|
||||||
/// <param name="RefTableName">目标引用表名称。</param>
|
/// <param name="RefTableName">目标引用表名称。</param>
|
||||||
/// <param name="NestedObject">对象节点对应的嵌套类型。</param>
|
/// <param name="NestedObject">对象节点对应的嵌套类型。</param>
|
||||||
/// <param name="ItemTypeSpec">数组元素类型模型。</param>
|
/// <param name="ItemTypeSpec">数组元素类型模型。</param>
|
||||||
@ -1230,6 +1330,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
string ClrType,
|
string ClrType,
|
||||||
string? Initializer,
|
string? Initializer,
|
||||||
string? EnumDocumentation,
|
string? EnumDocumentation,
|
||||||
|
string? ConstraintDocumentation,
|
||||||
string? RefTableName,
|
string? RefTableName,
|
||||||
SchemaObjectSpec? NestedObject,
|
SchemaObjectSpec? NestedObject,
|
||||||
SchemaTypeSpec? ItemTypeSpec);
|
SchemaTypeSpec? ItemTypeSpec);
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
- JSON Schema 作为结构描述
|
- JSON Schema 作为结构描述
|
||||||
- 一对象一文件的目录组织
|
- 一对象一文件的目录组织
|
||||||
- 运行时只读查询
|
- 运行时只读查询
|
||||||
|
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`minLength`、`maxLength`
|
||||||
- Source Generator 生成配置类型、表包装和注册/访问辅助
|
- Source Generator 生成配置类型、表包装和注册/访问辅助
|
||||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||||
|
|
||||||
@ -119,6 +120,8 @@ var slime = monsterTable.Get(1);
|
|||||||
- 数组元素类型不匹配
|
- 数组元素类型不匹配
|
||||||
- 嵌套对象字段类型不匹配
|
- 嵌套对象字段类型不匹配
|
||||||
- 对象数组元素结构不匹配
|
- 对象数组元素结构不匹配
|
||||||
|
- 数值字段违反 `minimum` / `maximum`
|
||||||
|
- 字符串字段违反 `minLength` / `maxLength`
|
||||||
- 标量 `enum` 不匹配
|
- 标量 `enum` 不匹配
|
||||||
- 标量数组元素 `enum` 不匹配
|
- 标量数组元素 `enum` 不匹配
|
||||||
- 通过 `x-gframework-ref-table` 声明的跨表引用缺失目标行
|
- 通过 `x-gframework-ref-table` 声明的跨表引用缺失目标行
|
||||||
@ -151,6 +154,8 @@ var slime = monsterTable.Get(1);
|
|||||||
- `description`:供表单提示、生成代码 XML 文档和接入说明复用
|
- `description`:供表单提示、生成代码 XML 文档和接入说明复用
|
||||||
- `default`:供生成类型属性初始值和工具提示复用
|
- `default`:供生成类型属性初始值和工具提示复用
|
||||||
- `enum`:供运行时校验、VS Code 校验和表单枚举选择复用
|
- `enum`:供运行时校验、VS Code 校验和表单枚举选择复用
|
||||||
|
- `minimum` / `maximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
|
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
|
|
||||||
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
||||||
|
|
||||||
|
|||||||
@ -359,6 +359,26 @@ function normalizeSchemaEnumValues(value) {
|
|||||||
return normalized.length > 0 ? normalized : undefined;
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize one finite schema number for tooling metadata and comparisons.
|
||||||
|
*
|
||||||
|
* @param {unknown} value Raw schema value.
|
||||||
|
* @returns {number | undefined} Normalized finite number.
|
||||||
|
*/
|
||||||
|
function normalizeSchemaNumber(value) {
|
||||||
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize one non-negative integer schema value for length constraints.
|
||||||
|
*
|
||||||
|
* @param {unknown} value Raw schema value.
|
||||||
|
* @returns {number | undefined} Normalized non-negative integer.
|
||||||
|
*/
|
||||||
|
function normalizeSchemaNonNegativeInteger(value) {
|
||||||
|
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a schema default value into a compact string that can be shown in UI
|
* Convert a schema default value into a compact string that can be shown in UI
|
||||||
* metadata hints.
|
* metadata hints.
|
||||||
@ -437,6 +457,10 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
title: typeof value.title === "string" ? value.title : undefined,
|
title: typeof value.title === "string" ? value.title : undefined,
|
||||||
description: typeof value.description === "string" ? value.description : undefined,
|
description: typeof value.description === "string" ? value.description : undefined,
|
||||||
defaultValue: formatSchemaDefaultValue(value.default),
|
defaultValue: formatSchemaDefaultValue(value.default),
|
||||||
|
minimum: normalizeSchemaNumber(value.minimum),
|
||||||
|
maximum: normalizeSchemaNumber(value.maximum),
|
||||||
|
minLength: normalizeSchemaNonNegativeInteger(value.minLength),
|
||||||
|
maxLength: normalizeSchemaNonNegativeInteger(value.maxLength),
|
||||||
refTable: typeof value["x-gframework-ref-table"] === "string"
|
refTable: typeof value["x-gframework-ref-table"] === "string"
|
||||||
? value["x-gframework-ref-table"]
|
? value["x-gframework-ref-table"]
|
||||||
: undefined
|
: undefined
|
||||||
@ -481,6 +505,10 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
defaultValue: metadata.defaultValue,
|
defaultValue: metadata.defaultValue,
|
||||||
|
minimum: metadata.minimum,
|
||||||
|
maximum: metadata.maximum,
|
||||||
|
minLength: metadata.minLength,
|
||||||
|
maxLength: metadata.maxLength,
|
||||||
enumValues: normalizeSchemaEnumValues(value.enum),
|
enumValues: normalizeSchemaEnumValues(value.enum),
|
||||||
refTable: metadata.refTable
|
refTable: metadata.refTable
|
||||||
};
|
};
|
||||||
@ -557,6 +585,47 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scalarValue = unquoteScalar(yamlNode.value);
|
||||||
|
if (typeof schemaNode.minimum === "number" && Number(scalarValue) < schemaNode.minimum) {
|
||||||
|
diagnostics.push({
|
||||||
|
severity: "error",
|
||||||
|
message: localizeValidationMessage(ValidationMessageKeys.minimumViolation, localizer, {
|
||||||
|
displayPath,
|
||||||
|
value: String(schemaNode.minimum)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.maximum === "number" && Number(scalarValue) > schemaNode.maximum) {
|
||||||
|
diagnostics.push({
|
||||||
|
severity: "error",
|
||||||
|
message: localizeValidationMessage(ValidationMessageKeys.maximumViolation, localizer, {
|
||||||
|
displayPath,
|
||||||
|
value: String(schemaNode.maximum)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.minLength === "number" && scalarValue.length < schemaNode.minLength) {
|
||||||
|
diagnostics.push({
|
||||||
|
severity: "error",
|
||||||
|
message: localizeValidationMessage(ValidationMessageKeys.minLengthViolation, localizer, {
|
||||||
|
displayPath,
|
||||||
|
value: String(schemaNode.minLength)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof schemaNode.maxLength === "number" && scalarValue.length > schemaNode.maxLength) {
|
||||||
|
diagnostics.push({
|
||||||
|
severity: "error",
|
||||||
|
message: localizeValidationMessage(ValidationMessageKeys.maxLengthViolation, localizer, {
|
||||||
|
displayPath,
|
||||||
|
value: String(schemaNode.maxLength)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -641,6 +710,14 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`;
|
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`;
|
||||||
case ValidationMessageKeys.enumMismatch:
|
case ValidationMessageKeys.enumMismatch:
|
||||||
return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`;
|
return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`;
|
||||||
|
case ValidationMessageKeys.maximumViolation:
|
||||||
|
return `属性“${params.displayPath}”必须小于或等于 ${params.value}。`;
|
||||||
|
case ValidationMessageKeys.maxLengthViolation:
|
||||||
|
return `属性“${params.displayPath}”长度必须不超过 ${params.value} 个字符。`;
|
||||||
|
case ValidationMessageKeys.minimumViolation:
|
||||||
|
return `属性“${params.displayPath}”必须大于或等于 ${params.value}。`;
|
||||||
|
case ValidationMessageKeys.minLengthViolation:
|
||||||
|
return `属性“${params.displayPath}”长度必须至少为 ${params.value} 个字符。`;
|
||||||
case ValidationMessageKeys.expectedObject:
|
case ValidationMessageKeys.expectedObject:
|
||||||
return params.subject;
|
return params.subject;
|
||||||
case ValidationMessageKeys.missingRequired:
|
case ValidationMessageKeys.missingRequired:
|
||||||
@ -661,6 +738,14 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`;
|
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`;
|
||||||
case ValidationMessageKeys.enumMismatch:
|
case ValidationMessageKeys.enumMismatch:
|
||||||
return `Property '${params.displayPath}' must be one of: ${params.values}.`;
|
return `Property '${params.displayPath}' must be one of: ${params.values}.`;
|
||||||
|
case ValidationMessageKeys.maximumViolation:
|
||||||
|
return `Property '${params.displayPath}' must be less than or equal to ${params.value}.`;
|
||||||
|
case ValidationMessageKeys.maxLengthViolation:
|
||||||
|
return `Property '${params.displayPath}' must be at most ${params.value} characters long.`;
|
||||||
|
case ValidationMessageKeys.minimumViolation:
|
||||||
|
return `Property '${params.displayPath}' must be greater than or equal to ${params.value}.`;
|
||||||
|
case ValidationMessageKeys.minLengthViolation:
|
||||||
|
return `Property '${params.displayPath}' must be at least ${params.value} characters long.`;
|
||||||
case ValidationMessageKeys.expectedObject:
|
case ValidationMessageKeys.expectedObject:
|
||||||
return params.subject;
|
return params.subject;
|
||||||
case ValidationMessageKeys.missingRequired:
|
case ValidationMessageKeys.missingRequired:
|
||||||
|
|||||||
@ -1574,7 +1574,7 @@ function getScalarArrayValue(yamlNode) {
|
|||||||
/**
|
/**
|
||||||
* Render human-facing metadata hints for one schema field.
|
* Render human-facing metadata hints for one schema field.
|
||||||
*
|
*
|
||||||
* @param {{description?: string, defaultValue?: string, enumValues?: string[], items?: {enumValues?: string[]}, refTable?: string}} propertySchema Property schema metadata.
|
* @param {{description?: string, defaultValue?: string, minimum?: number, maximum?: number, minLength?: number, maxLength?: number, enumValues?: string[], items?: {enumValues?: string[], minimum?: number, maximum?: number, minLength?: number, maxLength?: number}, refTable?: string}} propertySchema Property schema metadata.
|
||||||
* @param {boolean} isArrayField Whether the field is an array.
|
* @param {boolean} isArrayField Whether the field is an array.
|
||||||
* @returns {string} HTML fragment.
|
* @returns {string} HTML fragment.
|
||||||
*/
|
*/
|
||||||
@ -1598,6 +1598,38 @@ function renderFieldHint(propertySchema, isArrayField) {
|
|||||||
hints.push(escapeHtml(localizer.t("webview.hint.allowed", {values: enumValues.join(", ")})));
|
hints.push(escapeHtml(localizer.t("webview.hint.allowed", {values: enumValues.join(", ")})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isArrayField && typeof propertySchema.minimum === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.minimum", {value: propertySchema.minimum})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isArrayField && typeof propertySchema.maximum === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.maximum", {value: propertySchema.maximum})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isArrayField && typeof propertySchema.minLength === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.minLength", {value: propertySchema.minLength})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isArrayField && typeof propertySchema.maxLength === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.maxLength", {value: propertySchema.maxLength})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrayField && propertySchema.items && typeof propertySchema.items.minimum === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.itemMinimum", {value: propertySchema.items.minimum})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrayField && propertySchema.items && typeof propertySchema.items.maximum === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.itemMaximum", {value: propertySchema.items.maximum})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrayField && propertySchema.items && typeof propertySchema.items.minLength === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.itemMinLength", {value: propertySchema.items.minLength})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArrayField && propertySchema.items && typeof propertySchema.items.maxLength === "number") {
|
||||||
|
hints.push(escapeHtml(localizer.t("webview.hint.itemMaxLength", {value: propertySchema.items.maxLength})));
|
||||||
|
}
|
||||||
|
|
||||||
if (propertySchema.refTable) {
|
if (propertySchema.refTable) {
|
||||||
hints.push(escapeHtml(localizer.t("webview.hint.refTable", {refTable: propertySchema.refTable})));
|
hints.push(escapeHtml(localizer.t("webview.hint.refTable", {refTable: propertySchema.refTable})));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,11 +105,23 @@ const enMessages = {
|
|||||||
"webview.array.hint": "One item per line. Expected type: {itemType}",
|
"webview.array.hint": "One item per line. Expected type: {itemType}",
|
||||||
"webview.hint.default": "Default: {value}",
|
"webview.hint.default": "Default: {value}",
|
||||||
"webview.hint.allowed": "Allowed: {values}",
|
"webview.hint.allowed": "Allowed: {values}",
|
||||||
|
"webview.hint.minimum": "Minimum: {value}",
|
||||||
|
"webview.hint.maximum": "Maximum: {value}",
|
||||||
|
"webview.hint.minLength": "Min length: {value}",
|
||||||
|
"webview.hint.maxLength": "Max length: {value}",
|
||||||
|
"webview.hint.itemMinimum": "Item minimum: {value}",
|
||||||
|
"webview.hint.itemMaximum": "Item maximum: {value}",
|
||||||
|
"webview.hint.itemMinLength": "Item min length: {value}",
|
||||||
|
"webview.hint.itemMaxLength": "Item max length: {value}",
|
||||||
"webview.hint.refTable": "Ref table: {refTable}",
|
"webview.hint.refTable": "Ref table: {refTable}",
|
||||||
"webview.unsupported.array": "Unsupported array shapes are currently raw-YAML-only in the form preview.",
|
"webview.unsupported.array": "Unsupported array shapes are currently raw-YAML-only in the form preview.",
|
||||||
"webview.unsupported.type": "{type} fields are currently raw-YAML-only.",
|
"webview.unsupported.type": "{type} fields are currently raw-YAML-only.",
|
||||||
"webview.unsupported.objectArrayMixed": "Object-array items must be mappings. Use raw YAML if the current file mixes scalar and object items.",
|
"webview.unsupported.objectArrayMixed": "Object-array items must be mappings. Use raw YAML if the current file mixes scalar and object items.",
|
||||||
"webview.unsupported.nestedObjectArray": "Nested object-array fields are currently raw-YAML-only inside the object-array editor.",
|
"webview.unsupported.nestedObjectArray": "Nested object-array fields are currently raw-YAML-only inside the object-array editor.",
|
||||||
|
[ValidationMessageKeys.maximumViolation]: "Property '{displayPath}' must be less than or equal to {value}.",
|
||||||
|
[ValidationMessageKeys.maxLengthViolation]: "Property '{displayPath}' must be at most {value} characters long.",
|
||||||
|
[ValidationMessageKeys.minimumViolation]: "Property '{displayPath}' must be greater than or equal to {value}.",
|
||||||
|
[ValidationMessageKeys.minLengthViolation]: "Property '{displayPath}' must be at least {value} characters long.",
|
||||||
[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.expectedObject]: "{subject} is expected to be an object.",
|
||||||
@ -179,11 +191,23 @@ const zhCnMessages = {
|
|||||||
"webview.array.hint": "每行一个元素。期望类型:{itemType}",
|
"webview.array.hint": "每行一个元素。期望类型:{itemType}",
|
||||||
"webview.hint.default": "默认值:{value}",
|
"webview.hint.default": "默认值:{value}",
|
||||||
"webview.hint.allowed": "允许值:{values}",
|
"webview.hint.allowed": "允许值:{values}",
|
||||||
|
"webview.hint.minimum": "最小值:{value}",
|
||||||
|
"webview.hint.maximum": "最大值:{value}",
|
||||||
|
"webview.hint.minLength": "最小长度:{value}",
|
||||||
|
"webview.hint.maxLength": "最大长度:{value}",
|
||||||
|
"webview.hint.itemMinimum": "元素最小值:{value}",
|
||||||
|
"webview.hint.itemMaximum": "元素最大值:{value}",
|
||||||
|
"webview.hint.itemMinLength": "元素最小长度:{value}",
|
||||||
|
"webview.hint.itemMaxLength": "元素最大长度:{value}",
|
||||||
"webview.hint.refTable": "引用表:{refTable}",
|
"webview.hint.refTable": "引用表:{refTable}",
|
||||||
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
|
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
|
||||||
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
|
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
|
||||||
"webview.unsupported.objectArrayMixed": "对象数组中的每一项都必须是映射对象。如果当前文件混用了标量项和对象项,请改用原始 YAML。",
|
"webview.unsupported.objectArrayMixed": "对象数组中的每一项都必须是映射对象。如果当前文件混用了标量项和对象项,请改用原始 YAML。",
|
||||||
"webview.unsupported.nestedObjectArray": "对象数组编辑器内暂不支持更深层的对象数组字段,请改用原始 YAML。",
|
"webview.unsupported.nestedObjectArray": "对象数组编辑器内暂不支持更深层的对象数组字段,请改用原始 YAML。",
|
||||||
|
[ValidationMessageKeys.maximumViolation]: "属性“{displayPath}”必须小于或等于 {value}。",
|
||||||
|
[ValidationMessageKeys.maxLengthViolation]: "属性“{displayPath}”长度必须不超过 {value} 个字符。",
|
||||||
|
[ValidationMessageKeys.minimumViolation]: "属性“{displayPath}”必须大于或等于 {value}。",
|
||||||
|
[ValidationMessageKeys.minLengthViolation]: "属性“{displayPath}”长度必须至少为 {value} 个字符。",
|
||||||
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
||||||
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
||||||
[ValidationMessageKeys.expectedObject]: "{subject}",
|
[ValidationMessageKeys.expectedObject]: "{subject}",
|
||||||
|
|||||||
@ -4,6 +4,10 @@ const ValidationMessageKeys = Object.freeze({
|
|||||||
expectedObject: "validation.expectedObject",
|
expectedObject: "validation.expectedObject",
|
||||||
expectedScalarShape: "validation.expectedScalarShape",
|
expectedScalarShape: "validation.expectedScalarShape",
|
||||||
expectedScalarValue: "validation.expectedScalarValue",
|
expectedScalarValue: "validation.expectedScalarValue",
|
||||||
|
maximumViolation: "validation.maximumViolation",
|
||||||
|
maxLengthViolation: "validation.maxLengthViolation",
|
||||||
|
minimumViolation: "validation.minimumViolation",
|
||||||
|
minLengthViolation: "validation.minLengthViolation",
|
||||||
missingRequired: "validation.missingRequired",
|
missingRequired: "validation.missingRequired",
|
||||||
unknownProperty: "validation.unknownProperty"
|
unknownProperty: "validation.unknownProperty"
|
||||||
});
|
});
|
||||||
|
|||||||
@ -190,6 +190,82 @@ reward:
|
|||||||
assert.match(diagnostics[0].message, /coin, gem/u);
|
assert.match(diagnostics[0].message, /coin, gem/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("validateParsedConfig should report numeric range and string length mismatches", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 8
|
||||||
|
},
|
||||||
|
"hp": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 10
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
name: Sl
|
||||||
|
hp: 12
|
||||||
|
tags:
|
||||||
|
- safe
|
||||||
|
- shield
|
||||||
|
`);
|
||||||
|
|
||||||
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
|
assert.equal(diagnostics.length, 3);
|
||||||
|
assert.match(diagnostics[0].message, /at least 3 characters|至少为 3 个字符/u);
|
||||||
|
assert.match(diagnostics[1].message, /less than or equal to 10|小于或等于 10/u);
|
||||||
|
assert.match(diagnostics[2].message, /tags\[1\]|shield/u);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseSchemaContent should capture scalar range and length metadata", () => {
|
||||||
|
const schema = parseSchemaContent(`
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 12
|
||||||
|
},
|
||||||
|
"hp": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 99
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.equal(schema.properties.name.minLength, 3);
|
||||||
|
assert.equal(schema.properties.name.maxLength, 12);
|
||||||
|
assert.equal(schema.properties.hp.minimum, 1);
|
||||||
|
assert.equal(schema.properties.hp.maximum, 99);
|
||||||
|
assert.equal(schema.properties.tags.items.minLength, 2);
|
||||||
|
assert.equal(schema.properties.tags.items.maxLength, 6);
|
||||||
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should localize diagnostics when Chinese UI is requested", () => {
|
test("validateParsedConfig should localize diagnostics when Chinese UI is requested", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user