docs(config): 添加游戏内容配置系统完整文档

- 新增游戏内容配置系统详细介绍文档
- 包含 YAML 配置源文件和 JSON Schema 结构描述说明
- 提供推荐目录结构和 Schema 示例配置
- 添加官方启动帮助器 GameConfigBootstrap 使用指南
- 包含 Godot 文本配置桥接和运行时读取模板
- 提供 Architecture 推荐接入模板和热重载配置说明
- 添加运行时校验行为和开发期热重载功能说明
- 包含生成器接入约定和 VS Code 工具使用指南
- 新增 JavaScript 配置验证实现和格式校验模式
- 添加字符串格式校验包括 date、email、uuid 等类型
- 实现配置字段可编辑性检测和批量编辑功能支持
This commit is contained in:
GeWuYou 2026-04-16 09:26:56 +08:00
parent 6d5d9e2240
commit ba15d9d0f6
7 changed files with 123 additions and 19 deletions

View File

@ -838,6 +838,7 @@ public class YamlConfigLoaderTests
/// <param name="value">满足该 format 的 YAML 标量值。</param> /// <param name="value">满足该 format 的 YAML 标量值。</param>
[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("duration", "P2DT3H4M5.5S")]
[TestCase("email", "boss@example.com")] [TestCase("email", "boss@example.com")]
[TestCase("time", "08:30:00Z")] [TestCase("time", "08:30:00Z")]
[TestCase("uri", "https://example.com/loot-table")] [TestCase("uri", "https://example.com/loot-table")]
@ -892,6 +893,7 @@ public class YamlConfigLoaderTests
/// <param name="value">不满足该 format 的 YAML 标量值。</param> /// <param name="value">不满足该 format 的 YAML 标量值。</param>
[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("duration", "P1Y")]
[TestCase("email", "boss.example.com")] [TestCase("email", "boss.example.com")]
[TestCase("time", "08:30:00")] [TestCase("time", "08:30:00")]
[TestCase("uri", "/loot-table")] [TestCase("uri", "/loot-table")]
@ -988,6 +990,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("duration"));
Assert.That(exception.Message, Does.Contain("time")); Assert.That(exception.Message, Does.Contain("time"));
}); });
} }

View File

@ -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', 'time', 'uri', 'uuid'"; private const string SupportedStringFormatNames = "'date', 'date-time', 'duration', '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 SupportedDurationFormatRegex = new(
@"^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?$",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex SupportedTimeFormatRegex = new( private static readonly Regex SupportedTimeFormatRegex = new(
@"^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$", @"^(?<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);
@ -1707,6 +1711,10 @@ internal static class YamlConfigSchemaValidator
kind = YamlConfigStringFormatKind.DateTime; kind = YamlConfigStringFormatKind.DateTime;
return true; return true;
case "duration":
kind = YamlConfigStringFormatKind.Duration;
return true;
case "email": case "email":
kind = YamlConfigStringFormatKind.Email; kind = YamlConfigStringFormatKind.Email;
return true; return true;
@ -2325,6 +2333,7 @@ internal static class YamlConfigSchemaValidator
{ {
YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value), YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value),
YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value), YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value),
YamlConfigStringFormatKind.Duration => MatchesSupportedDurationFormat(value),
YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value), YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value),
YamlConfigStringFormatKind.Time => MatchesSupportedTimeFormat(value), YamlConfigStringFormatKind.Time => MatchesSupportedTimeFormat(value),
YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value), YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value),
@ -2371,6 +2380,44 @@ internal static class YamlConfigSchemaValidator
out _); out _);
} }
/// <summary>
/// 判断字符串是否满足共享支持的 <c>duration</c> 格式。
/// 当前共享子集只接受 day-time duration可声明 <c>D/H/M/S</c>,秒允许小数,
/// 但拒绝 <c>Y</c> / <c>M(月)</c> / <c>W</c> 等依赖日历语义的部分,
/// 避免不同宿主对“一个月/一年到底多长”出现解释漂移。
/// </summary>
/// <param name="value">待校验的字符串值。</param>
/// <returns>当前值是否是合法持续时间文本。</returns>
private static bool MatchesSupportedDurationFormat(string value)
{
var match = SupportedDurationFormatRegex.Match(value);
if (!match.Success)
{
return false;
}
var hasDayComponent = match.Groups["days"].Success;
var hasHourComponent = match.Groups["hours"].Success;
var hasMinuteComponent = match.Groups["minutes"].Success;
var hasSecondComponent = match.Groups["seconds"].Success;
var hasAnyComponent = hasDayComponent || hasHourComponent || hasMinuteComponent || hasSecondComponent;
if (!hasAnyComponent)
{
return false;
}
var hasTimeSection = value.Contains('T', StringComparison.Ordinal);
if (hasTimeSection &&
!hasHourComponent &&
!hasMinuteComponent &&
!hasSecondComponent)
{
return false;
}
return true;
}
/// <summary> /// <summary>
/// 判断字符串是否满足共享支持的 <c>time</c> 格式。 /// 判断字符串是否满足共享支持的 <c>time</c> 格式。
/// 该格式固定要求显式时区偏移,并只接受 24 小时制的合法时分秒文本, /// 该格式固定要求显式时区偏移,并只接受 24 小时制的合法时分秒文本,
@ -3753,6 +3800,11 @@ internal enum YamlConfigStringFormatKind
/// </summary> /// </summary>
DateTime, DateTime,
/// <summary>
/// 表示 day-time duration 形式的持续时间。
/// </summary>
Duration,
/// <summary> /// <summary>
/// 表示基础电子邮件地址格式。 /// 表示基础电子邮件地址格式。
/// </summary> /// </summary>

View File

@ -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_Time_Format_Into_Generated_Documentation() public void Run_Should_Write_Supported_Duration_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", "scheduleTime"], "required": ["id", "respawnDelay"],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"scheduleTime": { "respawnDelay": {
"type": "string", "type": "string",
"format": "time" "format": "duration"
} }
} }
} }
@ -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 = 'time'.")); Assert.That(generatedSources["MonsterConfig.g.cs"], Does.Contain("Constraints: format = 'duration'."));
} }
/// <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("duration"));
Assert.That(diagnostic.GetMessage(), Does.Contain("time")); Assert.That(diagnostic.GetMessage(), Does.Contain("time"));
}); });
} }

View File

@ -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', 'time', 'uri', and 'uuid'"; private const string SupportedStringFormatNames = "'date', 'date-time', 'duration', 'email', 'time', 'uri', and 'uuid'";
/// <inheritdoc /> /// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context) public void Initialize(IncrementalGeneratorInitializationContext context)
@ -707,6 +707,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
{ {
"date" => true, "date" => true,
"date-time" => true, "date-time" => true,
"duration" => true,
"email" => true, "email" => true,
"time" => true, "time" => true,
"uri" => true, "uri" => true,

View File

@ -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`、`time``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``duration`、`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,10 @@ 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``time``uri``uuid`;其中 `time` 固定要求显式时区偏移(例如 `08:30:00Z``08:30:00+08:00`),避免不同宿主对 time-only 文本隐式补日期或本地时区运行时会拒绝不满足格式的值VS Code 校验与表单 hint 会同步展示该约束,生成代码 XML 文档也会保留 `format = ...` 说明 - `format`:当前只支持 Runtime / Generator / Tooling 三端都能稳定对齐的字符串子集 `date``date-time``duration``email``time``uri``uuid`
- `duration`:当前只支持稳定的 day-time duration 子集,例如 `P2D``PT45M``P2DT3H4M5.5S`;为了避免跨宿主对日历语义解释漂移,暂不支持 `Y` / `M` / `W`
- `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 个匹配元素

View File

@ -14,9 +14,11 @@ 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 DurationFormatPattern =
/^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?$/u;
const TimeFormatPattern = const TimeFormatPattern =
/^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$/u; /^(?<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"]); const SupportedStringFormats = new Set(["date", "date-time", "duration", "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
@ -484,7 +486,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', 'time', 'uri', and 'uuid'."); "Supported formats are 'date', 'date-time', 'duration', 'email', 'time', 'uri', and 'uuid'.");
} }
/** /**
@ -628,6 +630,8 @@ function matchesSchemaStringFormat(scalarValue, formatName) {
return matchesSchemaDateFormat(scalarValue); return matchesSchemaDateFormat(scalarValue);
case "date-time": case "date-time":
return matchesSchemaDateTimeFormat(scalarValue); return matchesSchemaDateTimeFormat(scalarValue);
case "duration":
return matchesSchemaDurationFormat(scalarValue);
case "email": case "email":
return EmailFormatPattern.test(scalarValue); return EmailFormatPattern.test(scalarValue);
case "time": case "time":
@ -695,6 +699,35 @@ function matchesSchemaDateTimeFormat(scalarValue) {
return offsetHour <= 23 && offsetMinute <= 59; return offsetHour <= 23 && offsetMinute <= 59;
} }
/**
* Validate one shared day-time duration string.
*
* @param {string} scalarValue Scalar value from YAML.
* @returns {boolean} True when the value stays within the shared day-time subset.
*/
function matchesSchemaDurationFormat(scalarValue) {
const match = DurationFormatPattern.exec(scalarValue);
if (!match || !match.groups) {
return false;
}
const hasDayComponent = match.groups.days !== undefined;
const hasHourComponent = match.groups.hours !== undefined;
const hasMinuteComponent = match.groups.minutes !== undefined;
const hasSecondComponent = match.groups.seconds !== undefined;
const hasAnyComponent = hasDayComponent || hasHourComponent || hasMinuteComponent || hasSecondComponent;
if (!hasAnyComponent) {
return false;
}
const hasTimeSection = scalarValue.includes("T");
if (hasTimeSection && !hasHourComponent && !hasMinuteComponent && !hasSecondComponent) {
return false;
}
return true;
}
/** /**
* Validate one RFC 3339 full-time string with explicit timezone offset. * Validate one RFC 3339 full-time string with explicit timezone offset.
* *

View File

@ -675,6 +675,10 @@ test("validateParsedConfig should enforce supported string formats", () => {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"respawnDelay": {
"type": "string",
"format": "duration"
},
"contactEmail": { "contactEmail": {
"type": "string", "type": "string",
"format": "email" "format": "email"
@ -698,6 +702,7 @@ test("validateParsedConfig should enforce supported string formats", () => {
releaseDate: 2026-02-30 releaseDate: 2026-02-30
ancientReleaseDate: 0000-01-01 ancientReleaseDate: 0000-01-01
publishedAt: 2026-04-11T08:30:00 publishedAt: 2026-04-11T08:30:00
respawnDelay: P1Y
contactEmail: boss.example.com contactEmail: boss.example.com
dailyResetAt: 08:30:00 dailyResetAt: 08:30:00
catalogUri: /loot-table catalogUri: /loot-table
@ -706,14 +711,15 @@ configId: 123e4567e89b12d3a456426614174000
const diagnostics = validateParsedConfig(schema, yaml); const diagnostics = validateParsedConfig(schema, yaml);
assert.equal(diagnostics.length, 7); assert.equal(diagnostics.length, 8);
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 'duration'|字符串格式“duration”/u);
assert.match(diagnostics[4].message, /format 'time'|字符串格式“time”/u); assert.match(diagnostics[4].message, /format 'email'|字符串格式“email”/u);
assert.match(diagnostics[5].message, /format 'uri'|字符串格式“uri”/u); assert.match(diagnostics[5].message, /format 'time'|字符串格式“time”/u);
assert.match(diagnostics[6].message, /format 'uuid'|字符串格式“uuid”/u); assert.match(diagnostics[6].message, /format 'uri'|字符串格式“uri”/u);
assert.match(diagnostics[7].message, /format 'uuid'|字符串格式“uuid”/u);
}); });
test("validateParsedConfig should accept supported string formats", () => { test("validateParsedConfig should accept supported string formats", () => {
@ -729,6 +735,10 @@ test("validateParsedConfig should accept supported string formats", () => {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"respawnDelay": {
"type": "string",
"format": "duration"
},
"contactEmail": { "contactEmail": {
"type": "string", "type": "string",
"format": "email" "format": "email"
@ -751,6 +761,7 @@ test("validateParsedConfig should accept supported string formats", () => {
const yaml = parseTopLevelYaml(` const yaml = parseTopLevelYaml(`
releaseDate: 2026-04-11 releaseDate: 2026-04-11
publishedAt: 2026-04-11T08:30:00Z publishedAt: 2026-04-11T08:30:00Z
respawnDelay: P2DT3H4M5.5S
contactEmail: boss@example.com contactEmail: boss@example.com
dailyResetAt: 08:30:00Z dailyResetAt: 08:30:00Z
catalogUri: https://example.com/loot-table catalogUri: https://example.com/loot-table
@ -1417,7 +1428,7 @@ test("parseSchemaContent should capture supported string format metadata", () =>
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "string",
"format": "time" "format": "duration"
} }
} }
} }
@ -1425,7 +1436,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, "time"); assert.equal(schema.properties.aliases.items.format, "duration");
}); });
test("parseSchemaContent should capture multipleOf and uniqueItems metadata", () => { test("parseSchemaContent should capture multipleOf and uniqueItems metadata", () => {
@ -1643,7 +1654,7 @@ test("parseSchemaContent should reject unsupported string format declarations",
} }
} }
`), `),
/unsupported string format 'ipv4'.*'time'/u /unsupported string format 'ipv4'.*'duration'.*'time'/u
); );
}); });