mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
docs(config): 添加游戏内容配置系统文档和验证工具
- 新增游戏内容配置系统完整文档,涵盖 YAML 配置、JSON Schema 结构、目录组织 - 实现配置系统的运行时查询、类型生成、VS Code 插件集成等功能说明 - 添加 Schema 示例、YAML 示例和推荐接入模板 - 提供运行时读取、热重载、批处理编辑等功能的使用指南 - 实现配置校验行为、跨表引用、诊断对象等核心功能文档 - 集成开发期工具支持,包括表单编辑、批量更新、注释渲染等能力 - 添加架构接入模板和生产部署相关建议
This commit is contained in:
parent
67149ab2b2
commit
c732285dfb
@ -412,6 +412,55 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证开区间数值边界约束会在运行时被统一拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Number_Violates_Exclusive_Minimum_Or_Exclusive_Maximum()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
hp: 10
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "hp"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"hp": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 10,
|
||||
"exclusiveMaximum": 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("10"));
|
||||
Assert.That(exception.Message, Does.Contain("greater than 10"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串最小长度与最大长度约束会在运行时被统一拒绝。
|
||||
/// </summary>
|
||||
@ -461,6 +510,111 @@ public class YamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证字符串正则模式约束会在运行时被统一拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_String_Does_Not_Match_Pattern()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: slime
|
||||
hp: 10
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "hp"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z][a-z]+$"
|
||||
},
|
||||
"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("slime"));
|
||||
Assert.That(exception.Message, Does.Contain("regular expression"));
|
||||
Assert.That(exception.Message, Does.Contain("^[A-Z][a-z]+$"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证数组元素数量约束会在运行时被统一拒绝。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Throw_When_Array_Violates_MinItems_Or_MaxItems()
|
||||
{
|
||||
CreateConfigFile(
|
||||
"monster/slime.yaml",
|
||||
"""
|
||||
id: 1
|
||||
name: Slime
|
||||
dropRates:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
""");
|
||||
CreateSchemaFile(
|
||||
"schemas/monster.schema.json",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "dropRates"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" },
|
||||
"dropRates": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var loader = new YamlConfigLoader(_rootPath)
|
||||
.RegisterTable<int, MonsterConfigIntegerArrayStub>("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("dropRates"));
|
||||
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("4"));
|
||||
Assert.That(exception.Message, Does.Contain("at most 3 items"));
|
||||
Assert.That(registry.Count, Is.EqualTo(0));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
|
||||
/// </summary>
|
||||
@ -1594,4 +1748,4 @@ public class YamlConfigLoaderTests
|
||||
/// <param name="Id">配置主键。</param>
|
||||
/// <param name="Name">配置名称。</param>
|
||||
private sealed record ExistingConfigStub(int Id, string Name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GFramework.Game.Abstractions.Config;
|
||||
|
||||
namespace GFramework.Game.Config;
|
||||
@ -297,6 +298,7 @@ internal static class YamlConfigSchemaValidator
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
arrayConstraints: null,
|
||||
schemaPath);
|
||||
}
|
||||
|
||||
@ -365,6 +367,7 @@ internal static class YamlConfigSchemaValidator
|
||||
referenceTableName: null,
|
||||
allowedValues: null,
|
||||
constraints: null,
|
||||
arrayConstraints: ParseArrayConstraints(tableName, schemaPath, propertyPath, element),
|
||||
schemaPath);
|
||||
}
|
||||
|
||||
@ -395,6 +398,7 @@ internal static class YamlConfigSchemaValidator
|
||||
referenceTableName,
|
||||
ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"),
|
||||
ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
|
||||
arrayConstraints: null,
|
||||
schemaPath);
|
||||
}
|
||||
|
||||
@ -580,6 +584,11 @@ internal static class YamlConfigSchemaValidator
|
||||
displayPath: GetDiagnosticPath(displayPath));
|
||||
}
|
||||
|
||||
if (schemaNode.ArrayConstraints is not null)
|
||||
{
|
||||
ValidateArrayConstraints(tableName, yamlPath, displayPath, sequenceNode.Children.Count, schemaNode);
|
||||
}
|
||||
|
||||
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
||||
{
|
||||
ValidateNode(
|
||||
@ -739,8 +748,10 @@ internal static class YamlConfigSchemaValidator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析标量字段支持的范围与长度约束。
|
||||
/// 当前共享子集只支持 `integer/number` 上的 `minimum/maximum` 和 `string` 上的 `minLength/maxLength`。
|
||||
/// 解析标量字段支持的范围、长度与模式约束。
|
||||
/// 当前共享子集支持:
|
||||
/// `integer/number` 上的 `minimum/maximum/exclusiveMinimum/exclusiveMaximum`,
|
||||
/// 以及 `string` 上的 `minLength/maxLength/pattern`。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
@ -757,8 +768,13 @@ internal static class YamlConfigSchemaValidator
|
||||
{
|
||||
var minimum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minimum");
|
||||
var maximum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maximum");
|
||||
var exclusiveMinimum =
|
||||
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMinimum");
|
||||
var exclusiveMaximum =
|
||||
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMaximum");
|
||||
var minLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minLength");
|
||||
var maxLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maxLength");
|
||||
var pattern = TryParsePatternConstraint(tableName, schemaPath, propertyPath, element, nodeType);
|
||||
|
||||
if (minimum.HasValue && maximum.HasValue && minimum.Value > maximum.Value)
|
||||
{
|
||||
@ -770,6 +786,15 @@ internal static class YamlConfigSchemaValidator
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
ValidateNumericConstraintRange(
|
||||
tableName,
|
||||
schemaPath,
|
||||
propertyPath,
|
||||
minimum,
|
||||
maximum,
|
||||
exclusiveMinimum,
|
||||
exclusiveMaximum);
|
||||
|
||||
if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
@ -780,12 +805,62 @@ internal static class YamlConfigSchemaValidator
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (!minimum.HasValue && !maximum.HasValue && !minLength.HasValue && !maxLength.HasValue)
|
||||
if (!minimum.HasValue &&
|
||||
!maximum.HasValue &&
|
||||
!exclusiveMinimum.HasValue &&
|
||||
!exclusiveMaximum.HasValue &&
|
||||
!minLength.HasValue &&
|
||||
!maxLength.HasValue &&
|
||||
pattern is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new YamlConfigScalarConstraints(minimum, maximum, minLength, maxLength);
|
||||
return new YamlConfigScalarConstraints(
|
||||
minimum,
|
||||
maximum,
|
||||
exclusiveMinimum,
|
||||
exclusiveMaximum,
|
||||
minLength,
|
||||
maxLength,
|
||||
pattern,
|
||||
pattern is null
|
||||
? null
|
||||
: new Regex(
|
||||
pattern,
|
||||
RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析数组节点支持的元素数量约束。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">数组字段路径。</param>
|
||||
/// <param name="element">Schema 节点。</param>
|
||||
/// <returns>数组约束模型;未声明时返回空。</returns>
|
||||
private static YamlConfigArrayConstraints? ParseArrayConstraints(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element)
|
||||
{
|
||||
var minItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minItems");
|
||||
var maxItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxItems");
|
||||
|
||||
if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minItems' greater than 'maxItems'.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
return !minItems.HasValue && !maxItems.HasValue
|
||||
? null
|
||||
: new YamlConfigArrayConstraints(minItems, maxItems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -886,6 +961,180 @@ internal static class YamlConfigSchemaValidator
|
||||
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>
|
||||
/// <returns>正则模式;未声明时返回空。</returns>
|
||||
private static string? TryParsePatternConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element,
|
||||
YamlConfigSchemaPropertyType nodeType)
|
||||
{
|
||||
if (!element.TryGetProperty("pattern", out var patternElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nodeType != YamlConfigSchemaPropertyType.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'pattern', but only 'string' scalar types support regular-expression constraints.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
if (patternElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'pattern' as a string.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
var pattern = patternElement.GetString() ?? string.Empty;
|
||||
try
|
||||
{
|
||||
_ = new Regex(pattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' declares an invalid 'pattern' regular expression.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath),
|
||||
rawValue: pattern,
|
||||
innerException: exception);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取数组元素数量约束。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">字段路径。</param>
|
||||
/// <param name="element">Schema 节点。</param>
|
||||
/// <param name="keywordName">关键字名称。</param>
|
||||
/// <returns>数组元素数量约束;未声明时返回空。</returns>
|
||||
private static int? TryParseArrayLengthConstraint(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
JsonElement element,
|
||||
string keywordName)
|
||||
{
|
||||
if (!element.TryGetProperty(keywordName, out var constraintElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (constraintElement.ValueKind != JsonValueKind.Number ||
|
||||
!constraintElement.TryGetInt32(out var constraintValue) ||
|
||||
constraintValue < 0)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
|
||||
return constraintValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验数值上下界组合不会形成空区间。
|
||||
/// 这里把闭区间与开区间统一折算为最强边界,避免 schema 进入“无任何合法值”的状态。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||
/// <param name="propertyPath">字段路径。</param>
|
||||
/// <param name="minimum">闭区间最小值。</param>
|
||||
/// <param name="maximum">闭区间最大值。</param>
|
||||
/// <param name="exclusiveMinimum">开区间最小值。</param>
|
||||
/// <param name="exclusiveMaximum">开区间最大值。</param>
|
||||
private static void ValidateNumericConstraintRange(
|
||||
string tableName,
|
||||
string schemaPath,
|
||||
string propertyPath,
|
||||
double? minimum,
|
||||
double? maximum,
|
||||
double? exclusiveMinimum,
|
||||
double? exclusiveMaximum)
|
||||
{
|
||||
var hasLowerBound = false;
|
||||
var lowerBound = double.MinValue;
|
||||
var isLowerBoundExclusive = false;
|
||||
|
||||
if (minimum.HasValue)
|
||||
{
|
||||
hasLowerBound = true;
|
||||
lowerBound = minimum.Value;
|
||||
}
|
||||
|
||||
if (exclusiveMinimum.HasValue &&
|
||||
(!hasLowerBound ||
|
||||
exclusiveMinimum.Value > lowerBound ||
|
||||
(exclusiveMinimum.Value.Equals(lowerBound) && !isLowerBoundExclusive)))
|
||||
{
|
||||
hasLowerBound = true;
|
||||
lowerBound = exclusiveMinimum.Value;
|
||||
isLowerBoundExclusive = true;
|
||||
}
|
||||
|
||||
var hasUpperBound = false;
|
||||
var upperBound = double.MaxValue;
|
||||
var isUpperBoundExclusive = false;
|
||||
|
||||
if (maximum.HasValue)
|
||||
{
|
||||
hasUpperBound = true;
|
||||
upperBound = maximum.Value;
|
||||
}
|
||||
|
||||
if (exclusiveMaximum.HasValue &&
|
||||
(!hasUpperBound ||
|
||||
exclusiveMaximum.Value < upperBound ||
|
||||
(exclusiveMaximum.Value.Equals(upperBound) && !isUpperBoundExclusive)))
|
||||
{
|
||||
hasUpperBound = true;
|
||||
upperBound = exclusiveMaximum.Value;
|
||||
isUpperBoundExclusive = true;
|
||||
}
|
||||
|
||||
if (!hasLowerBound || !hasUpperBound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (lowerBound > upperBound ||
|
||||
(lowerBound.Equals(upperBound) && (isLowerBoundExclusive || isUpperBoundExclusive)))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.SchemaUnsupported,
|
||||
tableName,
|
||||
$"Property '{propertyPath}' in schema file '{schemaPath}' declares numeric constraints that do not leave any valid value range.",
|
||||
schemaPath: schemaPath,
|
||||
displayPath: GetDiagnosticPath(propertyPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验标量值是否满足范围与长度约束。
|
||||
/// </summary>
|
||||
@ -943,6 +1192,20 @@ internal static class YamlConfigSchemaValidator
|
||||
$"Minimum allowed value: {constraints.Minimum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||
}
|
||||
|
||||
if (constraints.ExclusiveMinimum.HasValue && numericValue <= constraints.ExclusiveMinimum.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.ConstraintViolation,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' must be greater than {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: rawValue,
|
||||
detail:
|
||||
$"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||
}
|
||||
|
||||
if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
@ -957,6 +1220,20 @@ internal static class YamlConfigSchemaValidator
|
||||
$"Maximum allowed value: {constraints.Maximum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||
}
|
||||
|
||||
if (constraints.ExclusiveMaximum.HasValue && numericValue >= constraints.ExclusiveMaximum.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.ConstraintViolation,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' must be less than {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: rawValue,
|
||||
detail:
|
||||
$"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case YamlConfigSchemaPropertyType.String:
|
||||
@ -988,6 +1265,20 @@ internal static class YamlConfigSchemaValidator
|
||||
detail: $"Maximum length: {constraints.MaxLength.Value}.");
|
||||
}
|
||||
|
||||
if (constraints.PatternRegex is not null &&
|
||||
!constraints.PatternRegex.IsMatch(rawValue))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.ConstraintViolation,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' must match regular expression '{constraints.Pattern}', but the current YAML scalar value is '{rawValue}'.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: rawValue,
|
||||
detail: $"Expected pattern: {constraints.Pattern}.");
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
@ -1002,6 +1293,54 @@ internal static class YamlConfigSchemaValidator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验数组值是否满足元素数量约束。
|
||||
/// </summary>
|
||||
/// <param name="tableName">所属配置表名称。</param>
|
||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||
/// <param name="displayPath">字段路径。</param>
|
||||
/// <param name="itemCount">当前数组元素数量。</param>
|
||||
/// <param name="schemaNode">数组 schema 节点。</param>
|
||||
private static void ValidateArrayConstraints(
|
||||
string tableName,
|
||||
string yamlPath,
|
||||
string displayPath,
|
||||
int itemCount,
|
||||
YamlConfigSchemaNode schemaNode)
|
||||
{
|
||||
var constraints = schemaNode.ArrayConstraints;
|
||||
if (constraints is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (constraints.MinItems.HasValue && itemCount < constraints.MinItems.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.ConstraintViolation,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' must contain at least {constraints.MinItems.Value} items, but the current YAML sequence contains {itemCount}.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
|
||||
detail: $"Minimum item count: {constraints.MinItems.Value}.");
|
||||
}
|
||||
|
||||
if (constraints.MaxItems.HasValue && itemCount > constraints.MaxItems.Value)
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.ConstraintViolation,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' must contain at most {constraints.MaxItems.Value} items, but the current YAML sequence contains {itemCount}.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
|
||||
detail: $"Maximum item count: {constraints.MaxItems.Value}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析跨表引用目标表名称。
|
||||
/// </summary>
|
||||
@ -1323,6 +1662,7 @@ internal sealed class YamlConfigSchemaNode
|
||||
/// <param name="referenceTableName">目标引用表名称。</param>
|
||||
/// <param name="allowedValues">标量允许值集合。</param>
|
||||
/// <param name="constraints">标量范围与长度约束。</param>
|
||||
/// <param name="arrayConstraints">数组元素数量约束。</param>
|
||||
/// <param name="schemaPathHint">用于错误信息的 schema 文件路径提示。</param>
|
||||
public YamlConfigSchemaNode(
|
||||
YamlConfigSchemaPropertyType nodeType,
|
||||
@ -1332,6 +1672,7 @@ internal sealed class YamlConfigSchemaNode
|
||||
string? referenceTableName,
|
||||
IReadOnlyCollection<string>? allowedValues,
|
||||
YamlConfigScalarConstraints? constraints,
|
||||
YamlConfigArrayConstraints? arrayConstraints,
|
||||
string schemaPathHint)
|
||||
{
|
||||
NodeType = nodeType;
|
||||
@ -1341,6 +1682,7 @@ internal sealed class YamlConfigSchemaNode
|
||||
ReferenceTableName = referenceTableName;
|
||||
AllowedValues = allowedValues;
|
||||
Constraints = constraints;
|
||||
ArrayConstraints = arrayConstraints;
|
||||
SchemaPathHint = schemaPathHint;
|
||||
}
|
||||
|
||||
@ -1379,6 +1721,11 @@ internal sealed class YamlConfigSchemaNode
|
||||
/// </summary>
|
||||
public YamlConfigScalarConstraints? Constraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数组元素数量约束;未声明时返回空。
|
||||
/// </summary>
|
||||
public YamlConfigArrayConstraints? ArrayConstraints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于诊断显示的 schema 路径提示。
|
||||
/// 当前节点本身不记录独立路径,因此对象校验会回退到所属根 schema 路径。
|
||||
@ -1401,6 +1748,7 @@ internal sealed class YamlConfigSchemaNode
|
||||
referenceTableName,
|
||||
AllowedValues,
|
||||
Constraints,
|
||||
ArrayConstraints,
|
||||
SchemaPathHint);
|
||||
}
|
||||
}
|
||||
@ -1416,18 +1764,30 @@ internal sealed class YamlConfigScalarConstraints
|
||||
/// </summary>
|
||||
/// <param name="minimum">最小值约束。</param>
|
||||
/// <param name="maximum">最大值约束。</param>
|
||||
/// <param name="exclusiveMinimum">开区间最小值约束。</param>
|
||||
/// <param name="exclusiveMaximum">开区间最大值约束。</param>
|
||||
/// <param name="minLength">最小长度约束。</param>
|
||||
/// <param name="maxLength">最大长度约束。</param>
|
||||
/// <param name="pattern">正则模式约束。</param>
|
||||
/// <param name="patternRegex">已编译的正则表达式。</param>
|
||||
public YamlConfigScalarConstraints(
|
||||
double? minimum,
|
||||
double? maximum,
|
||||
double? exclusiveMinimum,
|
||||
double? exclusiveMaximum,
|
||||
int? minLength,
|
||||
int? maxLength)
|
||||
int? maxLength,
|
||||
string? pattern,
|
||||
Regex? patternRegex)
|
||||
{
|
||||
Minimum = minimum;
|
||||
Maximum = maximum;
|
||||
ExclusiveMinimum = exclusiveMinimum;
|
||||
ExclusiveMaximum = exclusiveMaximum;
|
||||
MinLength = minLength;
|
||||
MaxLength = maxLength;
|
||||
Pattern = pattern;
|
||||
PatternRegex = patternRegex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1440,6 +1800,16 @@ internal sealed class YamlConfigScalarConstraints
|
||||
/// </summary>
|
||||
public double? Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最小值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMinimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取开区间最大值约束。
|
||||
/// </summary>
|
||||
public double? ExclusiveMaximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小长度约束。
|
||||
/// </summary>
|
||||
@ -1449,6 +1819,44 @@ internal sealed class YamlConfigScalarConstraints
|
||||
/// 获取最大长度约束。
|
||||
/// </summary>
|
||||
public int? MaxLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取正则模式约束原文。
|
||||
/// </summary>
|
||||
public string? Pattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取已编译的正则表达式。
|
||||
/// </summary>
|
||||
public Regex? PatternRegex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一个数组节点上声明的元素数量约束。
|
||||
/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。
|
||||
/// </summary>
|
||||
internal sealed class YamlConfigArrayConstraints
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化数组约束模型。
|
||||
/// </summary>
|
||||
/// <param name="minItems">最小元素数量约束。</param>
|
||||
/// <param name="maxItems">最大元素数量约束。</param>
|
||||
public YamlConfigArrayConstraints(int? minItems, int? maxItems)
|
||||
{
|
||||
MinItems = minItems;
|
||||
MaxItems = maxItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最小元素数量约束。
|
||||
/// </summary>
|
||||
public int? MinItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取最大元素数量约束。
|
||||
/// </summary>
|
||||
public int? MaxItems { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1558,4 +1966,4 @@ internal enum YamlConfigSchemaPropertyType
|
||||
/// 数组类型。
|
||||
/// </summary>
|
||||
Array
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,6 +81,7 @@ public class SchemaConfigGeneratorSnapshotTests
|
||||
"description": "Localized monster display name.",
|
||||
"minLength": 3,
|
||||
"maxLength": 16,
|
||||
"pattern": "^[A-Z][a-z]+$",
|
||||
"default": "Slime",
|
||||
"enum": ["Slime", "Goblin"]
|
||||
},
|
||||
@ -88,11 +89,15 @@ public class SchemaConfigGeneratorSnapshotTests
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 999,
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 1000,
|
||||
"default": 10
|
||||
},
|
||||
"dropItems": {
|
||||
"description": "Referenced drop ids.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
|
||||
@ -24,7 +24,7 @@ public sealed partial class MonsterConfig
|
||||
/// Schema property path: 'name'.
|
||||
/// Display title: 'Monster Name'.
|
||||
/// Allowed values: Slime, Goblin.
|
||||
/// Constraints: minLength = 3, maxLength = 16.
|
||||
/// Constraints: minLength = 3, maxLength = 16, pattern = '^[A-Z][a-z]+$'.
|
||||
/// Generated default initializer: = "Slime";
|
||||
/// </remarks>
|
||||
public string Name { get; set; } = "Slime";
|
||||
@ -34,7 +34,7 @@ public sealed partial class MonsterConfig
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Schema property path: 'hp'.
|
||||
/// Constraints: minimum = 1, maximum = 999.
|
||||
/// Constraints: minimum = 1, exclusiveMinimum = 0, maximum = 999, exclusiveMaximum = 1000.
|
||||
/// Generated default initializer: = 10;
|
||||
/// </remarks>
|
||||
public int? Hp { get; set; } = 10;
|
||||
@ -45,6 +45,7 @@ public sealed partial class MonsterConfig
|
||||
/// <remarks>
|
||||
/// Schema property path: 'dropItems'.
|
||||
/// Allowed values: potion, slime_gel.
|
||||
/// Constraints: minItems = 1, maxItems = 3.
|
||||
/// References config table: 'item'.
|
||||
/// Item constraints: minLength = 3, maxLength = 12.
|
||||
/// Generated default initializer: = new string[] { "potion" };
|
||||
|
||||
@ -477,7 +477,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
TryBuildArrayInitializer(property.Value, itemType, itemClrType) ??
|
||||
$" = global::System.Array.Empty<{itemClrType}>();",
|
||||
TryBuildEnumDocumentation(itemsElement, itemType),
|
||||
null,
|
||||
TryBuildConstraintDocumentation(property.Value, "array"),
|
||||
refTableName,
|
||||
null,
|
||||
new SchemaTypeSpec(
|
||||
@ -527,7 +527,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
|
||||
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
|
||||
null,
|
||||
null,
|
||||
TryBuildConstraintDocumentation(property.Value, "array"),
|
||||
null,
|
||||
null,
|
||||
new SchemaTypeSpec(
|
||||
@ -1876,7 +1876,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 shared schema 子集中的范围与长度约束整理成 XML 文档可读字符串。
|
||||
/// 将 shared schema 子集中的范围、长度、模式与数组数量约束整理成 XML 文档可读字符串。
|
||||
/// </summary>
|
||||
/// <param name="element">Schema 节点。</param>
|
||||
/// <param name="schemaType">标量类型。</param>
|
||||
@ -1891,12 +1891,24 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
parts.Add($"minimum = {minimum.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if ((schemaType == "integer" || schemaType == "number") &&
|
||||
TryGetFiniteNumber(element, "exclusiveMinimum", out var exclusiveMinimum))
|
||||
{
|
||||
parts.Add($"exclusiveMinimum = {exclusiveMinimum.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if ((schemaType == "integer" || schemaType == "number") &&
|
||||
TryGetFiniteNumber(element, "maximum", out var maximum))
|
||||
{
|
||||
parts.Add($"maximum = {maximum.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if ((schemaType == "integer" || schemaType == "number") &&
|
||||
TryGetFiniteNumber(element, "exclusiveMaximum", out var exclusiveMaximum))
|
||||
{
|
||||
parts.Add($"exclusiveMaximum = {exclusiveMaximum.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if (schemaType == "string" &&
|
||||
TryGetNonNegativeInt32(element, "minLength", out var minLength))
|
||||
{
|
||||
@ -1909,6 +1921,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
||||
parts.Add($"maxLength = {maxLength.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if (schemaType == "string" &&
|
||||
element.TryGetProperty("pattern", out var patternElement) &&
|
||||
patternElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
parts.Add($"pattern = '{patternElement.GetString() ?? string.Empty}'");
|
||||
}
|
||||
|
||||
if (schemaType == "array" &&
|
||||
TryGetNonNegativeInt32(element, "minItems", out var minItems))
|
||||
{
|
||||
parts.Add($"minItems = {minItems.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
if (schemaType == "array" &&
|
||||
TryGetNonNegativeInt32(element, "maxItems", out var maxItems))
|
||||
{
|
||||
parts.Add($"maxItems = {maxItems.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
return parts.Count > 0 ? string.Join(", ", parts) : null;
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
- JSON Schema 作为结构描述
|
||||
- 一对象一文件的目录组织
|
||||
- 运行时只读查询
|
||||
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`minLength`、`maxLength`
|
||||
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`minLength`、`maxLength`、`pattern`、`minItems`、`maxItems`
|
||||
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||
|
||||
@ -553,7 +553,10 @@ var loader = new YamlConfigLoader("config-root")
|
||||
- 嵌套对象字段类型不匹配
|
||||
- 对象数组元素结构不匹配
|
||||
- 数值字段违反 `minimum` / `maximum`
|
||||
- 数值字段违反 `exclusiveMinimum` / `exclusiveMaximum`
|
||||
- 字符串字段违反 `minLength` / `maxLength`
|
||||
- 字符串字段违反 `pattern`
|
||||
- 数组字段违反 `minItems` / `maxItems`
|
||||
- 标量 `enum` 不匹配
|
||||
- 标量数组元素 `enum` 不匹配
|
||||
- 通过 `x-gframework-ref-table` 声明的跨表引用缺失目标行
|
||||
@ -602,7 +605,10 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
|
||||
- `default`:供生成类型属性初始值和工具提示复用
|
||||
- `enum`:供运行时校验、VS Code 校验和表单枚举选择复用
|
||||
- `minimum` / `maximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||
- `exclusiveMinimum` / `exclusiveMaximum`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||
- `pattern`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
||||
- `minItems` / `maxItems`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
||||
|
||||
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
||||
|
||||
|
||||
@ -379,6 +379,26 @@ function normalizeSchemaNonNegativeInteger(value) {
|
||||
return Number.isInteger(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one schema pattern string when the regular expression can be
|
||||
* compiled by the local tooling runtime.
|
||||
*
|
||||
* @param {unknown} value Raw schema value.
|
||||
* @returns {string | undefined} Normalized pattern string.
|
||||
*/
|
||||
function normalizeSchemaPattern(value) {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
void new RegExp(value);
|
||||
return value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a schema default value into a compact string that can be shown in UI
|
||||
* metadata hints.
|
||||
@ -410,6 +430,25 @@ function formatSchemaDefaultValue(value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test one scalar value against one schema pattern string.
|
||||
*
|
||||
* @param {string} scalarValue Scalar value from YAML.
|
||||
* @param {string | undefined} pattern Schema pattern string.
|
||||
* @returns {boolean} True when the value matches or no pattern is declared.
|
||||
*/
|
||||
function matchesSchemaPattern(scalarValue, pattern) {
|
||||
if (typeof pattern !== "string") {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return new RegExp(pattern).test(scalarValue);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a scalar value for YAML output.
|
||||
*
|
||||
@ -458,9 +497,14 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
description: typeof value.description === "string" ? value.description : undefined,
|
||||
defaultValue: formatSchemaDefaultValue(value.default),
|
||||
minimum: normalizeSchemaNumber(value.minimum),
|
||||
exclusiveMinimum: normalizeSchemaNumber(value.exclusiveMinimum),
|
||||
maximum: normalizeSchemaNumber(value.maximum),
|
||||
exclusiveMaximum: normalizeSchemaNumber(value.exclusiveMaximum),
|
||||
minLength: normalizeSchemaNonNegativeInteger(value.minLength),
|
||||
maxLength: normalizeSchemaNonNegativeInteger(value.maxLength),
|
||||
pattern: normalizeSchemaPattern(value.pattern),
|
||||
minItems: normalizeSchemaNonNegativeInteger(value.minItems),
|
||||
maxItems: normalizeSchemaNonNegativeInteger(value.maxItems),
|
||||
refTable: typeof value["x-gframework-ref-table"] === "string"
|
||||
? value["x-gframework-ref-table"]
|
||||
: undefined
|
||||
@ -494,6 +538,8 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
defaultValue: metadata.defaultValue,
|
||||
minItems: metadata.minItems,
|
||||
maxItems: metadata.maxItems,
|
||||
refTable: metadata.refTable,
|
||||
items: itemNode
|
||||
};
|
||||
@ -508,15 +554,24 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
minimum: type === "integer" || type === "number"
|
||||
? metadata.minimum
|
||||
: undefined,
|
||||
exclusiveMinimum: type === "integer" || type === "number"
|
||||
? metadata.exclusiveMinimum
|
||||
: undefined,
|
||||
maximum: type === "integer" || type === "number"
|
||||
? metadata.maximum
|
||||
: undefined,
|
||||
exclusiveMaximum: type === "integer" || type === "number"
|
||||
? metadata.exclusiveMaximum
|
||||
: undefined,
|
||||
minLength: type === "string"
|
||||
? metadata.minLength
|
||||
: undefined,
|
||||
maxLength: type === "string"
|
||||
? metadata.maxLength
|
||||
: undefined,
|
||||
pattern: type === "string"
|
||||
? metadata.pattern
|
||||
: undefined,
|
||||
enumValues: normalizeSchemaEnumValues(value.enum),
|
||||
refTable: metadata.refTable
|
||||
};
|
||||
@ -548,6 +603,28 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof schemaNode.minItems === "number" &&
|
||||
yamlNode.items.length < schemaNode.minItems) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.minItemsViolation, localizer, {
|
||||
displayPath,
|
||||
value: String(schemaNode.minItems)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof schemaNode.maxItems === "number" &&
|
||||
yamlNode.items.length > schemaNode.maxItems) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.maxItemsViolation, localizer, {
|
||||
displayPath,
|
||||
value: String(schemaNode.maxItems)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
||||
validateNode(
|
||||
schemaNode.items,
|
||||
@ -597,6 +674,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
const scalarValue = unquoteScalar(yamlNode.value);
|
||||
const supportsNumericConstraints = schemaNode.type === "integer" || schemaNode.type === "number";
|
||||
const supportsLengthConstraints = schemaNode.type === "string";
|
||||
const supportsPatternConstraints = schemaNode.type === "string";
|
||||
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.minimum === "number" &&
|
||||
@ -610,6 +688,18 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.exclusiveMinimum === "number" &&
|
||||
Number(scalarValue) <= schemaNode.exclusiveMinimum) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.exclusiveMinimumViolation, localizer, {
|
||||
displayPath,
|
||||
value: String(schemaNode.exclusiveMinimum)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.maximum === "number" &&
|
||||
Number(scalarValue) > schemaNode.maximum) {
|
||||
@ -622,6 +712,18 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.exclusiveMaximum === "number" &&
|
||||
Number(scalarValue) >= schemaNode.exclusiveMaximum) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.exclusiveMaximumViolation, localizer, {
|
||||
displayPath,
|
||||
value: String(schemaNode.exclusiveMaximum)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsLengthConstraints &&
|
||||
typeof schemaNode.minLength === "number" &&
|
||||
scalarValue.length < schemaNode.minLength) {
|
||||
@ -645,6 +747,17 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsPatternConstraints &&
|
||||
!matchesSchemaPattern(scalarValue, schemaNode.pattern)) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.patternViolation, localizer, {
|
||||
displayPath,
|
||||
value: schemaNode.pattern
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -729,14 +842,24 @@ function localizeValidationMessage(key, localizer, params) {
|
||||
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`;
|
||||
case ValidationMessageKeys.enumMismatch:
|
||||
return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`;
|
||||
case ValidationMessageKeys.exclusiveMaximumViolation:
|
||||
return `属性“${params.displayPath}”必须小于 ${params.value}。`;
|
||||
case ValidationMessageKeys.exclusiveMinimumViolation:
|
||||
return `属性“${params.displayPath}”必须大于 ${params.value}。`;
|
||||
case ValidationMessageKeys.maximumViolation:
|
||||
return `属性“${params.displayPath}”必须小于或等于 ${params.value}。`;
|
||||
case ValidationMessageKeys.maxItemsViolation:
|
||||
return `属性“${params.displayPath}”最多只能包含 ${params.value} 个元素。`;
|
||||
case ValidationMessageKeys.maxLengthViolation:
|
||||
return `属性“${params.displayPath}”长度必须不超过 ${params.value} 个字符。`;
|
||||
case ValidationMessageKeys.minimumViolation:
|
||||
return `属性“${params.displayPath}”必须大于或等于 ${params.value}。`;
|
||||
case ValidationMessageKeys.minItemsViolation:
|
||||
return `属性“${params.displayPath}”至少需要包含 ${params.value} 个元素。`;
|
||||
case ValidationMessageKeys.minLengthViolation:
|
||||
return `属性“${params.displayPath}”长度必须至少为 ${params.value} 个字符。`;
|
||||
case ValidationMessageKeys.patternViolation:
|
||||
return `属性“${params.displayPath}”必须匹配正则模式“${params.value}”。`;
|
||||
case ValidationMessageKeys.expectedObject:
|
||||
return params.subject;
|
||||
case ValidationMessageKeys.missingRequired:
|
||||
@ -757,14 +880,24 @@ function localizeValidationMessage(key, localizer, params) {
|
||||
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`;
|
||||
case ValidationMessageKeys.enumMismatch:
|
||||
return `Property '${params.displayPath}' must be one of: ${params.values}.`;
|
||||
case ValidationMessageKeys.exclusiveMaximumViolation:
|
||||
return `Property '${params.displayPath}' must be less than ${params.value}.`;
|
||||
case ValidationMessageKeys.exclusiveMinimumViolation:
|
||||
return `Property '${params.displayPath}' must be greater than ${params.value}.`;
|
||||
case ValidationMessageKeys.maximumViolation:
|
||||
return `Property '${params.displayPath}' must be less than or equal to ${params.value}.`;
|
||||
case ValidationMessageKeys.maxItemsViolation:
|
||||
return `Property '${params.displayPath}' must contain at most ${params.value} items.`;
|
||||
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.minItemsViolation:
|
||||
return `Property '${params.displayPath}' must contain at least ${params.value} items.`;
|
||||
case ValidationMessageKeys.minLengthViolation:
|
||||
return `Property '${params.displayPath}' must be at least ${params.value} characters long.`;
|
||||
case ValidationMessageKeys.patternViolation:
|
||||
return `Property '${params.displayPath}' must match pattern '${params.value}'.`;
|
||||
case ValidationMessageKeys.expectedObject:
|
||||
return params.subject;
|
||||
case ValidationMessageKeys.missingRequired:
|
||||
@ -1418,6 +1551,8 @@ module.exports = {
|
||||
* title?: string,
|
||||
* description?: string,
|
||||
* defaultValue?: string,
|
||||
* minItems?: number,
|
||||
* maxItems?: number,
|
||||
* refTable?: string,
|
||||
* items: SchemaNode
|
||||
* } | {
|
||||
@ -1426,6 +1561,13 @@ module.exports = {
|
||||
* title?: string,
|
||||
* description?: string,
|
||||
* defaultValue?: string,
|
||||
* minimum?: number,
|
||||
* exclusiveMinimum?: number,
|
||||
* maximum?: number,
|
||||
* exclusiveMaximum?: number,
|
||||
* minLength?: number,
|
||||
* maxLength?: number,
|
||||
* pattern?: string,
|
||||
* enumValues?: string[],
|
||||
* refTable?: string
|
||||
* }} SchemaNode
|
||||
|
||||
@ -1574,7 +1574,7 @@ function getScalarArrayValue(yamlNode) {
|
||||
/**
|
||||
* Render human-facing metadata hints for one schema field.
|
||||
*
|
||||
* @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 {{description?: string, defaultValue?: string, minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, minLength?: number, maxLength?: number, pattern?: string, minItems?: number, maxItems?: number, enumValues?: string[], items?: {enumValues?: string[], minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, minLength?: number, maxLength?: number, pattern?: string}, refTable?: string}} propertySchema Property schema metadata.
|
||||
* @param {boolean} isArrayField Whether the field is an array.
|
||||
* @returns {string} HTML fragment.
|
||||
*/
|
||||
@ -1602,10 +1602,18 @@ function renderFieldHint(propertySchema, isArrayField) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.minimum", {value: propertySchema.minimum})));
|
||||
}
|
||||
|
||||
if (!isArrayField && typeof propertySchema.exclusiveMinimum === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.exclusiveMinimum", {value: propertySchema.exclusiveMinimum})));
|
||||
}
|
||||
|
||||
if (!isArrayField && typeof propertySchema.maximum === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.maximum", {value: propertySchema.maximum})));
|
||||
}
|
||||
|
||||
if (!isArrayField && typeof propertySchema.exclusiveMaximum === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.exclusiveMaximum", {value: propertySchema.exclusiveMaximum})));
|
||||
}
|
||||
|
||||
if (!isArrayField && typeof propertySchema.minLength === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.minLength", {value: propertySchema.minLength})));
|
||||
}
|
||||
@ -1614,14 +1622,34 @@ function renderFieldHint(propertySchema, isArrayField) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.maxLength", {value: propertySchema.maxLength})));
|
||||
}
|
||||
|
||||
if (!isArrayField && propertySchema.pattern) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.pattern", {value: propertySchema.pattern})));
|
||||
}
|
||||
|
||||
if (isArrayField && typeof propertySchema.minItems === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.minItems", {value: propertySchema.minItems})));
|
||||
}
|
||||
|
||||
if (isArrayField && typeof propertySchema.maxItems === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.maxItems", {value: propertySchema.maxItems})));
|
||||
}
|
||||
|
||||
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.exclusiveMinimum === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.itemExclusiveMinimum", {value: propertySchema.items.exclusiveMinimum})));
|
||||
}
|
||||
|
||||
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.exclusiveMaximum === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.itemExclusiveMaximum", {value: propertySchema.items.exclusiveMaximum})));
|
||||
}
|
||||
|
||||
if (isArrayField && propertySchema.items && typeof propertySchema.items.minLength === "number") {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.itemMinLength", {value: propertySchema.items.minLength})));
|
||||
}
|
||||
@ -1630,6 +1658,10 @@ function renderFieldHint(propertySchema, isArrayField) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.itemMaxLength", {value: propertySchema.items.maxLength})));
|
||||
}
|
||||
|
||||
if (isArrayField && propertySchema.items && propertySchema.items.pattern) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.itemPattern", {value: propertySchema.items.pattern})));
|
||||
}
|
||||
|
||||
if (propertySchema.refTable) {
|
||||
hints.push(escapeHtml(localizer.t("webview.hint.refTable", {refTable: propertySchema.refTable})));
|
||||
}
|
||||
|
||||
@ -106,22 +106,35 @@ const enMessages = {
|
||||
"webview.hint.default": "Default: {value}",
|
||||
"webview.hint.allowed": "Allowed: {values}",
|
||||
"webview.hint.minimum": "Minimum: {value}",
|
||||
"webview.hint.exclusiveMinimum": "Exclusive minimum: {value}",
|
||||
"webview.hint.maximum": "Maximum: {value}",
|
||||
"webview.hint.exclusiveMaximum": "Exclusive maximum: {value}",
|
||||
"webview.hint.minLength": "Min length: {value}",
|
||||
"webview.hint.maxLength": "Max length: {value}",
|
||||
"webview.hint.pattern": "Pattern: {value}",
|
||||
"webview.hint.minItems": "Min items: {value}",
|
||||
"webview.hint.maxItems": "Max items: {value}",
|
||||
"webview.hint.itemMinimum": "Item minimum: {value}",
|
||||
"webview.hint.itemExclusiveMinimum": "Item exclusive minimum: {value}",
|
||||
"webview.hint.itemMaximum": "Item maximum: {value}",
|
||||
"webview.hint.itemExclusiveMaximum": "Item exclusive maximum: {value}",
|
||||
"webview.hint.itemMinLength": "Item min length: {value}",
|
||||
"webview.hint.itemMaxLength": "Item max length: {value}",
|
||||
"webview.hint.itemPattern": "Item pattern: {value}",
|
||||
"webview.hint.refTable": "Ref table: {refTable}",
|
||||
"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.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.",
|
||||
[ValidationMessageKeys.exclusiveMaximumViolation]: "Property '{displayPath}' must be less than {value}.",
|
||||
[ValidationMessageKeys.exclusiveMinimumViolation]: "Property '{displayPath}' must be greater than {value}.",
|
||||
[ValidationMessageKeys.maximumViolation]: "Property '{displayPath}' must be less than or equal to {value}.",
|
||||
[ValidationMessageKeys.maxItemsViolation]: "Property '{displayPath}' must contain at most {value} items.",
|
||||
[ValidationMessageKeys.maxLengthViolation]: "Property '{displayPath}' must be at most {value} characters long.",
|
||||
[ValidationMessageKeys.minimumViolation]: "Property '{displayPath}' must be greater than or equal to {value}.",
|
||||
[ValidationMessageKeys.minItemsViolation]: "Property '{displayPath}' must contain at least {value} items.",
|
||||
[ValidationMessageKeys.minLengthViolation]: "Property '{displayPath}' must be at least {value} characters long.",
|
||||
[ValidationMessageKeys.patternViolation]: "Property '{displayPath}' must match pattern '{value}'.",
|
||||
[ValidationMessageKeys.enumMismatch]: "Property '{displayPath}' must be one of: {values}.",
|
||||
[ValidationMessageKeys.expectedArray]: "Property '{displayPath}' is expected to be an array.",
|
||||
[ValidationMessageKeys.expectedObject]: "{subject} is expected to be an object.",
|
||||
@ -192,22 +205,35 @@ const zhCnMessages = {
|
||||
"webview.hint.default": "默认值:{value}",
|
||||
"webview.hint.allowed": "允许值:{values}",
|
||||
"webview.hint.minimum": "最小值:{value}",
|
||||
"webview.hint.exclusiveMinimum": "开区间最小值:{value}",
|
||||
"webview.hint.maximum": "最大值:{value}",
|
||||
"webview.hint.exclusiveMaximum": "开区间最大值:{value}",
|
||||
"webview.hint.minLength": "最小长度:{value}",
|
||||
"webview.hint.maxLength": "最大长度:{value}",
|
||||
"webview.hint.pattern": "正则模式:{value}",
|
||||
"webview.hint.minItems": "最少元素数:{value}",
|
||||
"webview.hint.maxItems": "最多元素数:{value}",
|
||||
"webview.hint.itemMinimum": "元素最小值:{value}",
|
||||
"webview.hint.itemExclusiveMinimum": "元素开区间最小值:{value}",
|
||||
"webview.hint.itemMaximum": "元素最大值:{value}",
|
||||
"webview.hint.itemExclusiveMaximum": "元素开区间最大值:{value}",
|
||||
"webview.hint.itemMinLength": "元素最小长度:{value}",
|
||||
"webview.hint.itemMaxLength": "元素最大长度:{value}",
|
||||
"webview.hint.itemPattern": "元素正则模式:{value}",
|
||||
"webview.hint.refTable": "引用表:{refTable}",
|
||||
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
|
||||
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
|
||||
"webview.unsupported.objectArrayMixed": "对象数组中的每一项都必须是映射对象。如果当前文件混用了标量项和对象项,请改用原始 YAML。",
|
||||
"webview.unsupported.nestedObjectArray": "对象数组编辑器内暂不支持更深层的对象数组字段,请改用原始 YAML。",
|
||||
[ValidationMessageKeys.exclusiveMaximumViolation]: "属性“{displayPath}”必须小于 {value}。",
|
||||
[ValidationMessageKeys.exclusiveMinimumViolation]: "属性“{displayPath}”必须大于 {value}。",
|
||||
[ValidationMessageKeys.maximumViolation]: "属性“{displayPath}”必须小于或等于 {value}。",
|
||||
[ValidationMessageKeys.maxItemsViolation]: "属性“{displayPath}”最多只能包含 {value} 个元素。",
|
||||
[ValidationMessageKeys.maxLengthViolation]: "属性“{displayPath}”长度必须不超过 {value} 个字符。",
|
||||
[ValidationMessageKeys.minimumViolation]: "属性“{displayPath}”必须大于或等于 {value}。",
|
||||
[ValidationMessageKeys.minItemsViolation]: "属性“{displayPath}”至少需要包含 {value} 个元素。",
|
||||
[ValidationMessageKeys.minLengthViolation]: "属性“{displayPath}”长度必须至少为 {value} 个字符。",
|
||||
[ValidationMessageKeys.patternViolation]: "属性“{displayPath}”必须匹配正则模式“{value}”。",
|
||||
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
||||
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
||||
[ValidationMessageKeys.expectedObject]: "{subject}",
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
const ValidationMessageKeys = Object.freeze({
|
||||
enumMismatch: "validation.enumMismatch",
|
||||
exclusiveMaximumViolation: "validation.exclusiveMaximumViolation",
|
||||
exclusiveMinimumViolation: "validation.exclusiveMinimumViolation",
|
||||
expectedArray: "validation.expectedArray",
|
||||
expectedObject: "validation.expectedObject",
|
||||
expectedScalarShape: "validation.expectedScalarShape",
|
||||
expectedScalarValue: "validation.expectedScalarValue",
|
||||
maximumViolation: "validation.maximumViolation",
|
||||
maxItemsViolation: "validation.maxItemsViolation",
|
||||
maxLengthViolation: "validation.maxLengthViolation",
|
||||
minimumViolation: "validation.minimumViolation",
|
||||
minItemsViolation: "validation.minItemsViolation",
|
||||
minLengthViolation: "validation.minLengthViolation",
|
||||
missingRequired: "validation.missingRequired",
|
||||
patternViolation: "validation.patternViolation",
|
||||
unknownProperty: "validation.unknownProperty"
|
||||
});
|
||||
|
||||
|
||||
@ -231,6 +231,46 @@ tags:
|
||||
assert.match(diagnostics[2].message, /tags\[1\]|shield/u);
|
||||
});
|
||||
|
||||
test("validateParsedConfig should report exclusive bounds, pattern, and array item-count mismatches", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z][a-z]+$"
|
||||
},
|
||||
"hp": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 10,
|
||||
"exclusiveMaximum": 20
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const yaml = parseTopLevelYaml(`
|
||||
name: slime
|
||||
hp: 10
|
||||
tags:
|
||||
- onlyOne
|
||||
`);
|
||||
|
||||
const diagnostics = validateParsedConfig(schema, yaml);
|
||||
|
||||
assert.equal(diagnostics.length, 3);
|
||||
assert.match(diagnostics[0].message, /pattern|正则模式/u);
|
||||
assert.match(diagnostics[1].message, /greater than 10|大于 10/u);
|
||||
assert.match(diagnostics[2].message, /at least 2 items|至少需要包含 2 个元素/u);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should capture scalar range and length metadata", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
@ -266,6 +306,41 @@ test("parseSchemaContent should capture scalar range and length metadata", () =>
|
||||
assert.equal(schema.properties.tags.items.maxLength, 6);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should capture exclusive bounds, pattern, and array item-count metadata", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z][a-z]+$"
|
||||
},
|
||||
"hp": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 1,
|
||||
"exclusiveMaximum": 99
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 4,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
assert.equal(schema.properties.name.pattern, "^[A-Z][a-z]+$");
|
||||
assert.equal(schema.properties.hp.exclusiveMinimum, 1);
|
||||
assert.equal(schema.properties.hp.exclusiveMaximum, 99);
|
||||
assert.equal(schema.properties.tags.minItems, 2);
|
||||
assert.equal(schema.properties.tags.maxItems, 4);
|
||||
assert.equal(schema.properties.tags.items.pattern, "^[a-z]+$");
|
||||
});
|
||||
|
||||
test("parseSchemaContent should ignore mismatched constraint metadata on unsupported scalar types", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user