mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(config): 添加游戏内容配置系统完整文档
- 新增游戏内容配置系统详细介绍文档 - 包含 YAML 配置源文件支持说明 - 提供 JSON Schema 结构描述功能说明 - 说明一对象一文件的目录组织方式 - 介绍运行时只读查询功能特性 - 详细说明 Runtime / Generator / Tooling 共享支持的约束类型 - 提供 Source Generator 生成配置类型的完整说明 - 包含 VS Code 插件功能详细介绍 - 提供推荐目录结构和 Schema 示例 - 说明 YAML 示例格式和接入模板 - 详细说明 Godot 文本配置桥接功能 - 提供运行时读取模板和生成查询辅助说明 - 包含 Architecture 接入模板和热重载配置说明 - 详细说明运行时校验行为和跨表引用机制 - 提供开发期热重载功能完整配置指南 - 说明生成器接入约定和 VS Code 工具功能 - 包含当前限制和独立 Config Studio 评估说明 - 新增配置验证 JavaScript 工具实现
This commit is contained in:
parent
c2ee2209fd
commit
6d5d9e2240
@ -839,6 +839,7 @@ public class YamlConfigLoaderTests
|
|||||||
[TestCase("date", "2026-04-11")]
|
[TestCase("date", "2026-04-11")]
|
||||||
[TestCase("date-time", "2026-04-11T08:30:00Z")]
|
[TestCase("date-time", "2026-04-11T08:30:00Z")]
|
||||||
[TestCase("email", "boss@example.com")]
|
[TestCase("email", "boss@example.com")]
|
||||||
|
[TestCase("time", "08:30:00Z")]
|
||||||
[TestCase("uri", "https://example.com/loot-table")]
|
[TestCase("uri", "https://example.com/loot-table")]
|
||||||
[TestCase("uuid", "123e4567-e89b-12d3-a456-426614174000")]
|
[TestCase("uuid", "123e4567-e89b-12d3-a456-426614174000")]
|
||||||
public async Task LoadAsync_Should_Accept_Supported_String_Format(
|
public async Task LoadAsync_Should_Accept_Supported_String_Format(
|
||||||
@ -892,6 +893,7 @@ public class YamlConfigLoaderTests
|
|||||||
[TestCase("date", "2026-02-30")]
|
[TestCase("date", "2026-02-30")]
|
||||||
[TestCase("date-time", "2026-04-11T08:30:00")]
|
[TestCase("date-time", "2026-04-11T08:30:00")]
|
||||||
[TestCase("email", "boss.example.com")]
|
[TestCase("email", "boss.example.com")]
|
||||||
|
[TestCase("time", "08:30:00")]
|
||||||
[TestCase("uri", "/loot-table")]
|
[TestCase("uri", "/loot-table")]
|
||||||
[TestCase("uuid", "123e4567e89b12d3a456426614174000")]
|
[TestCase("uuid", "123e4567e89b12d3a456426614174000")]
|
||||||
public void LoadAsync_Should_Throw_When_String_Does_Not_Match_Supported_Format(
|
public void LoadAsync_Should_Throw_When_String_Does_Not_Match_Supported_Format(
|
||||||
@ -986,6 +988,7 @@ public class YamlConfigLoaderTests
|
|||||||
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("ipv4"));
|
Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("ipv4"));
|
||||||
Assert.That(exception.Message, Does.Contain("unsupported string format"));
|
Assert.That(exception.Message, Does.Contain("unsupported string format"));
|
||||||
Assert.That(exception.Message, Does.Contain("date-time"));
|
Assert.That(exception.Message, Does.Contain("date-time"));
|
||||||
|
Assert.That(exception.Message, Does.Contain("time"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
// The runtime intentionally uses the same culture-invariant regex semantics as the
|
// The runtime intentionally uses the same culture-invariant regex semantics as the
|
||||||
// JS tooling so grouping and backreferences behave consistently across environments.
|
// JS tooling so grouping and backreferences behave consistently across environments.
|
||||||
private const RegexOptions SupportedPatternRegexOptions = RegexOptions.CultureInvariant;
|
private const RegexOptions SupportedPatternRegexOptions = RegexOptions.CultureInvariant;
|
||||||
private const string SupportedStringFormatNames = "'date', 'date-time', 'email', 'uri', 'uuid'";
|
private const string SupportedStringFormatNames = "'date', 'date-time', 'email', 'time', 'uri', 'uuid'";
|
||||||
|
|
||||||
private static readonly Regex ExactDecimalPattern = new(
|
private static readonly Regex ExactDecimalPattern = new(
|
||||||
@"^(?<sign>[+-]?)(?:(?<integer>\d+)(?:\.(?<fraction>\d*))?|\.(?<fractionOnly>\d+))(?:[eE](?<exponent>[+-]?\d+))?$",
|
@"^(?<sign>[+-]?)(?:(?<integer>\d+)(?:\.(?<fraction>\d*))?|\.(?<fractionOnly>\d+))(?:[eE](?<exponent>[+-]?\d+))?$",
|
||||||
@ -36,6 +36,10 @@ internal static class YamlConfigSchemaValidator
|
|||||||
@"^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$",
|
@"^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$",
|
||||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex SupportedTimeFormatRegex = new(
|
||||||
|
@"^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
||||||
private static readonly Regex SupportedUriSchemeRegex = new(
|
private static readonly Regex SupportedUriSchemeRegex = new(
|
||||||
@"^[A-Za-z][A-Za-z0-9+\.-]*:",
|
@"^[A-Za-z][A-Za-z0-9+\.-]*:",
|
||||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
@ -1707,6 +1711,10 @@ internal static class YamlConfigSchemaValidator
|
|||||||
kind = YamlConfigStringFormatKind.Email;
|
kind = YamlConfigStringFormatKind.Email;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "time":
|
||||||
|
kind = YamlConfigStringFormatKind.Time;
|
||||||
|
return true;
|
||||||
|
|
||||||
case "uri":
|
case "uri":
|
||||||
kind = YamlConfigStringFormatKind.Uri;
|
kind = YamlConfigStringFormatKind.Uri;
|
||||||
return true;
|
return true;
|
||||||
@ -2318,6 +2326,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value),
|
YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value),
|
||||||
YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value),
|
YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value),
|
||||||
YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value),
|
YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value),
|
||||||
|
YamlConfigStringFormatKind.Time => MatchesSupportedTimeFormat(value),
|
||||||
YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value),
|
YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value),
|
||||||
YamlConfigStringFormatKind.Uuid => Guid.TryParseExact(value, "D", out _),
|
YamlConfigStringFormatKind.Uuid => Guid.TryParseExact(value, "D", out _),
|
||||||
_ => false
|
_ => false
|
||||||
@ -2362,6 +2371,40 @@ internal static class YamlConfigSchemaValidator
|
|||||||
out _);
|
out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否满足共享支持的 <c>time</c> 格式。
|
||||||
|
/// 该格式固定要求显式时区偏移,并只接受 24 小时制的合法时分秒文本,
|
||||||
|
/// 避免不同宿主对“time-only + offset”解析默认日期或时区时产生漂移。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">待校验的字符串值。</param>
|
||||||
|
/// <returns>当前值是否是合法时间文本。</returns>
|
||||||
|
private static bool MatchesSupportedTimeFormat(string value)
|
||||||
|
{
|
||||||
|
var match = SupportedTimeFormatRegex.Match(value);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hour = int.Parse(match.Groups["hour"].Value, CultureInfo.InvariantCulture);
|
||||||
|
var minute = int.Parse(match.Groups["minute"].Value, CultureInfo.InvariantCulture);
|
||||||
|
var second = int.Parse(match.Groups["second"].Value, CultureInfo.InvariantCulture);
|
||||||
|
if (hour > 23 || minute > 59 || second > 59)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = match.Groups["offset"].Value;
|
||||||
|
if (offset == "Z")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetHour = int.Parse(offset.AsSpan(1, 2), CultureInfo.InvariantCulture);
|
||||||
|
var offsetMinute = int.Parse(offset.AsSpan(4, 2), CultureInfo.InvariantCulture);
|
||||||
|
return offsetHour <= 23 && offsetMinute <= 59;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 判断字符串是否满足共享支持的 <c>uri</c> 格式。
|
/// 判断字符串是否满足共享支持的 <c>uri</c> 格式。
|
||||||
/// 这里要求输入显式包含 scheme,避免把普通路径意外解释成平台相关的绝对 URI。
|
/// 这里要求输入显式包含 scheme,避免把普通路径意外解释成平台相关的绝对 URI。
|
||||||
@ -3715,6 +3758,11 @@ internal enum YamlConfigStringFormatKind
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Email,
|
Email,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示带显式时区偏移的 RFC 3339 时间。
|
||||||
|
/// </summary>
|
||||||
|
Time,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示绝对 URI。
|
/// 表示绝对 URI。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -95,7 +95,7 @@ public class SchemaConfigGeneratorTests
|
|||||||
/// 验证共享支持的字符串 <c>format</c> 会写入生成 XML 文档。
|
/// 验证共享支持的字符串 <c>format</c> 会写入生成 XML 文档。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void Run_Should_Write_Supported_String_Format_Into_Generated_Documentation()
|
public void Run_Should_Write_Supported_Time_Format_Into_Generated_Documentation()
|
||||||
{
|
{
|
||||||
const string source = """
|
const string source = """
|
||||||
namespace TestApp
|
namespace TestApp
|
||||||
@ -109,12 +109,12 @@ public class SchemaConfigGeneratorTests
|
|||||||
const string schema = """
|
const string schema = """
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["id", "contactEmail"],
|
"required": ["id", "scheduleTime"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": { "type": "integer" },
|
"id": { "type": "integer" },
|
||||||
"contactEmail": {
|
"scheduleTime": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "email"
|
"format": "time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ public class SchemaConfigGeneratorTests
|
|||||||
StringComparer.Ordinal);
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
Assert.That(generatedSources["MonsterConfig.g.cs"], Does.Contain("Constraints: format = 'email'."));
|
Assert.That(generatedSources["MonsterConfig.g.cs"], Does.Contain("Constraints: format = 'time'."));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -178,6 +178,7 @@ public class SchemaConfigGeneratorTests
|
|||||||
Assert.That(diagnostic.GetMessage(), Does.Contain("address"));
|
Assert.That(diagnostic.GetMessage(), Does.Contain("address"));
|
||||||
Assert.That(diagnostic.GetMessage(), Does.Contain("ipv4"));
|
Assert.That(diagnostic.GetMessage(), Does.Contain("ipv4"));
|
||||||
Assert.That(diagnostic.GetMessage(), Does.Contain("date-time"));
|
Assert.That(diagnostic.GetMessage(), Does.Contain("date-time"));
|
||||||
|
Assert.That(diagnostic.GetMessage(), Does.Contain("time"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
private const string LookupIndexReferencePropertyMessage =
|
private const string LookupIndexReferencePropertyMessage =
|
||||||
"Reference properties are excluded from generated lookup indexes because they already carry cross-table semantics.";
|
"Reference properties are excluded from generated lookup indexes because they already carry cross-table semantics.";
|
||||||
|
|
||||||
private const string SupportedStringFormatNames = "'date', 'date-time', 'email', 'uri', and 'uuid'";
|
private const string SupportedStringFormatNames = "'date', 'date-time', 'email', 'time', 'uri', and 'uuid'";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
@ -708,6 +708,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"date" => true,
|
"date" => true,
|
||||||
"date-time" => true,
|
"date-time" => true,
|
||||||
"email" => true,
|
"email" => true,
|
||||||
|
"time" => true,
|
||||||
"uri" => true,
|
"uri" => true,
|
||||||
"uuid" => true,
|
"uuid" => true,
|
||||||
_ => false
|
_ => false
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
- JSON Schema 作为结构描述
|
- JSON Schema 作为结构描述
|
||||||
- 一对象一文件的目录组织
|
- 一对象一文件的目录组织
|
||||||
- 运行时只读查询
|
- 运行时只读查询
|
||||||
- Runtime / Generator / Tooling 共享支持 `const`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`email`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`
|
- Runtime / Generator / Tooling 共享支持 `const`、`minimum`、`maximum`、`exclusiveMinimum`、`exclusiveMaximum`、`multipleOf`、`minLength`、`maxLength`、`pattern`、`format`(当前稳定子集:`date`、`date-time`、`email`、`time`、`uri`、`uuid`)、`minItems`、`maxItems`、`uniqueItems`、`contains`、`minContains`、`maxContains`、`minProperties`、`maxProperties`
|
||||||
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
||||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||||
|
|
||||||
@ -773,7 +773,7 @@ if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var re
|
|||||||
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前优先按运行时与 JS 共用的十进制精确整倍数判定处理常见十进制步进,并在必要时退回浮点容差兜底
|
- `multipleOf`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前优先按运行时与 JS 共用的十进制精确整倍数判定处理常见十进制步进,并在必要时退回浮点容差兜底
|
||||||
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
- `minLength` / `maxLength`:供运行时校验、VS Code 校验和生成代码 XML 文档复用
|
||||||
- `pattern`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用;当前按 C# `CultureInvariant` 与 JS Unicode `u` 模式解释,非法模式会在 schema 解析阶段直接报错
|
- `pattern`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用;当前按 C# `CultureInvariant` 与 JS Unicode `u` 模式解释,非法模式会在 schema 解析阶段直接报错
|
||||||
- `format`:当前只支持 Runtime / Generator / Tooling 三端都能稳定对齐的字符串子集 `date`、`date-time`、`email`、`uri`、`uuid`;运行时会拒绝不满足格式的值,VS Code 校验与表单 hint 会同步展示该约束,生成代码 XML 文档也会保留 `format = ...` 说明
|
- `format`:当前只支持 Runtime / Generator / Tooling 三端都能稳定对齐的字符串子集 `date`、`date-time`、`email`、`time`、`uri`、`uuid`;其中 `time` 固定要求显式时区偏移(例如 `08:30:00Z` 或 `08:30:00+08:00`),避免不同宿主对 time-only 文本隐式补日期或本地时区;运行时会拒绝不满足格式的值,VS Code 校验与表单 hint 会同步展示该约束,生成代码 XML 文档也会保留 `format = ...` 说明
|
||||||
- `minItems` / `maxItems`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
- `minItems` / `maxItems`:供运行时校验、VS Code 校验、表单提示和生成代码 XML 文档复用
|
||||||
- `uniqueItems`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象数组会按 schema 归一化后的结构比较重复项,而不是依赖 YAML 字段顺序
|
- `uniqueItems`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;对象数组会按 schema 归一化后的结构比较重复项,而不是依赖 YAML 字段顺序
|
||||||
- `contains` / `minContains` / `maxContains`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前会按同一套递归 schema 规则统计“有多少数组元素匹配 contains 子 schema”,其中仅声明 `contains` 时默认至少需要 1 个匹配元素
|
- `contains` / `minContains` / `maxContains`:供运行时校验、VS Code 校验、表单 hint 和生成代码 XML 文档复用;当前会按同一套递归 schema 规则统计“有多少数组元素匹配 contains 子 schema”,其中仅声明 `contains` 时默认至少需要 1 个匹配元素
|
||||||
|
|||||||
@ -14,7 +14,9 @@ const UuidFormatPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|||||||
const DateFormatPattern = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/u;
|
const DateFormatPattern = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/u;
|
||||||
const DateTimeFormatPattern =
|
const DateTimeFormatPattern =
|
||||||
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
|
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
|
||||||
const SupportedStringFormats = new Set(["date", "date-time", "email", "uri", "uuid"]);
|
const TimeFormatPattern =
|
||||||
|
/^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u;
|
||||||
|
const SupportedStringFormats = new Set(["date", "date-time", "email", "time", "uri", "uuid"]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two strings using the same UTF-16 code-unit ordering as C#'s
|
* Compare two strings using the same UTF-16 code-unit ordering as C#'s
|
||||||
@ -482,7 +484,7 @@ function normalizeSchemaStringFormat(value, schemaType, displayPath) {
|
|||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Schema property '${displayPath}' declares unsupported string format '${value}'. ` +
|
`Schema property '${displayPath}' declares unsupported string format '${value}'. ` +
|
||||||
"Supported formats are 'date', 'date-time', 'email', 'uri', and 'uuid'.");
|
"Supported formats are 'date', 'date-time', 'email', 'time', 'uri', and 'uuid'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -628,6 +630,8 @@ function matchesSchemaStringFormat(scalarValue, formatName) {
|
|||||||
return matchesSchemaDateTimeFormat(scalarValue);
|
return matchesSchemaDateTimeFormat(scalarValue);
|
||||||
case "email":
|
case "email":
|
||||||
return EmailFormatPattern.test(scalarValue);
|
return EmailFormatPattern.test(scalarValue);
|
||||||
|
case "time":
|
||||||
|
return matchesSchemaTimeFormat(scalarValue);
|
||||||
case "uri":
|
case "uri":
|
||||||
return matchesSchemaUriFormat(scalarValue);
|
return matchesSchemaUriFormat(scalarValue);
|
||||||
case "uuid":
|
case "uuid":
|
||||||
@ -691,6 +695,35 @@ function matchesSchemaDateTimeFormat(scalarValue) {
|
|||||||
return offsetHour <= 23 && offsetMinute <= 59;
|
return offsetHour <= 23 && offsetMinute <= 59;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate one RFC 3339 full-time string with explicit timezone offset.
|
||||||
|
*
|
||||||
|
* @param {string} scalarValue Scalar value from YAML.
|
||||||
|
* @returns {boolean} True when the value is structurally valid.
|
||||||
|
*/
|
||||||
|
function matchesSchemaTimeFormat(scalarValue) {
|
||||||
|
const match = TimeFormatPattern.exec(scalarValue);
|
||||||
|
if (!match || !match.groups) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hour = Number.parseInt(match.groups.hour, 10);
|
||||||
|
const minute = Number.parseInt(match.groups.minute, 10);
|
||||||
|
const second = Number.parseInt(match.groups.second, 10);
|
||||||
|
if (hour > 23 || minute > 59 || second > 59) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = match.groups.offset;
|
||||||
|
if (offset === "Z") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetHour = Number.parseInt(offset.slice(1, 3), 10);
|
||||||
|
const offsetMinute = Number.parseInt(offset.slice(4, 6), 10);
|
||||||
|
return offsetHour <= 23 && offsetMinute <= 59;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate one absolute URI string using the platform URL parser.
|
* Validate one absolute URI string using the platform URL parser.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -679,6 +679,10 @@ test("validateParsedConfig should enforce supported string formats", () => {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "email"
|
"format": "email"
|
||||||
},
|
},
|
||||||
|
"dailyResetAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "time"
|
||||||
|
},
|
||||||
"catalogUri": {
|
"catalogUri": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uri"
|
"format": "uri"
|
||||||
@ -695,19 +699,21 @@ releaseDate: 2026-02-30
|
|||||||
ancientReleaseDate: 0000-01-01
|
ancientReleaseDate: 0000-01-01
|
||||||
publishedAt: 2026-04-11T08:30:00
|
publishedAt: 2026-04-11T08:30:00
|
||||||
contactEmail: boss.example.com
|
contactEmail: boss.example.com
|
||||||
|
dailyResetAt: 08:30:00
|
||||||
catalogUri: /loot-table
|
catalogUri: /loot-table
|
||||||
configId: 123e4567e89b12d3a456426614174000
|
configId: 123e4567e89b12d3a456426614174000
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const diagnostics = validateParsedConfig(schema, yaml);
|
const diagnostics = validateParsedConfig(schema, yaml);
|
||||||
|
|
||||||
assert.equal(diagnostics.length, 6);
|
assert.equal(diagnostics.length, 7);
|
||||||
assert.match(diagnostics[0].message, /format 'date'|字符串格式“date”/u);
|
assert.match(diagnostics[0].message, /format 'date'|字符串格式“date”/u);
|
||||||
assert.match(diagnostics[1].message, /format 'date'|字符串格式“date”/u);
|
assert.match(diagnostics[1].message, /format 'date'|字符串格式“date”/u);
|
||||||
assert.match(diagnostics[2].message, /format 'date-time'|字符串格式“date-time”/u);
|
assert.match(diagnostics[2].message, /format 'date-time'|字符串格式“date-time”/u);
|
||||||
assert.match(diagnostics[3].message, /format 'email'|字符串格式“email”/u);
|
assert.match(diagnostics[3].message, /format 'email'|字符串格式“email”/u);
|
||||||
assert.match(diagnostics[4].message, /format 'uri'|字符串格式“uri”/u);
|
assert.match(diagnostics[4].message, /format 'time'|字符串格式“time”/u);
|
||||||
assert.match(diagnostics[5].message, /format 'uuid'|字符串格式“uuid”/u);
|
assert.match(diagnostics[5].message, /format 'uri'|字符串格式“uri”/u);
|
||||||
|
assert.match(diagnostics[6].message, /format 'uuid'|字符串格式“uuid”/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should accept supported string formats", () => {
|
test("validateParsedConfig should accept supported string formats", () => {
|
||||||
@ -727,6 +733,10 @@ test("validateParsedConfig should accept supported string formats", () => {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "email"
|
"format": "email"
|
||||||
},
|
},
|
||||||
|
"dailyResetAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "time"
|
||||||
|
},
|
||||||
"catalogUri": {
|
"catalogUri": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uri"
|
"format": "uri"
|
||||||
@ -742,6 +752,7 @@ test("validateParsedConfig should accept supported string formats", () => {
|
|||||||
releaseDate: 2026-04-11
|
releaseDate: 2026-04-11
|
||||||
publishedAt: 2026-04-11T08:30:00Z
|
publishedAt: 2026-04-11T08:30:00Z
|
||||||
contactEmail: boss@example.com
|
contactEmail: boss@example.com
|
||||||
|
dailyResetAt: 08:30:00Z
|
||||||
catalogUri: https://example.com/loot-table
|
catalogUri: https://example.com/loot-table
|
||||||
configId: 123e4567-e89b-12d3-a456-426614174000
|
configId: 123e4567-e89b-12d3-a456-426614174000
|
||||||
`);
|
`);
|
||||||
@ -1406,7 +1417,7 @@ test("parseSchemaContent should capture supported string format metadata", () =>
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid"
|
"format": "time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1414,7 +1425,7 @@ test("parseSchemaContent should capture supported string format metadata", () =>
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
assert.equal(schema.properties.contactEmail.format, "email");
|
assert.equal(schema.properties.contactEmail.format, "email");
|
||||||
assert.equal(schema.properties.aliases.items.format, "uuid");
|
assert.equal(schema.properties.aliases.items.format, "time");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseSchemaContent should capture multipleOf and uniqueItems metadata", () => {
|
test("parseSchemaContent should capture multipleOf and uniqueItems metadata", () => {
|
||||||
@ -1632,7 +1643,7 @@ test("parseSchemaContent should reject unsupported string format declarations",
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
/unsupported string format 'ipv4'/u
|
/unsupported string format 'ipv4'.*'time'/u
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user