mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
fix(ai-first-config): 收口 PR 306 审查遗留项
- 新增 Generator 与 Tooling 的 anyOf 和坏形状回归覆盖,补齐组合关键字与未知 type 拒绝 - 修复 VS Code 配置工具的 object-array 直属项收集与 contains 文案一致性问题 - 更新 README、Game 文档与工具说明,明确 additionalProperties 显式 false 边界与最小接入路径 - 补充 ai-plan 跟踪与 trace,记录 PR 306 open threads 收口结果和验证摘要
This commit is contained in:
parent
040bcb99e4
commit
e671646a74
@ -1843,6 +1843,54 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会显式拒绝当前共享子集尚未支持的 <c>anyOf</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf()
|
||||
{
|
||||
const string source = DummySource;
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "reward"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"reward": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["itemCount"],
|
||||
"properties": {
|
||||
"itemCount": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_015"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("reward"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("anyOf"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("does not support combinators that can change generated type shape"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器接受显式声明的 <c>additionalProperties: false</c>。
|
||||
/// </summary>
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
|
||||
如果你采用 AI-First 配置工作流,建议在进入实现前先确认两条边界:
|
||||
|
||||
- 当前共享子集接受闭合对象边界 `additionalProperties: false`
|
||||
- 当前共享子集接受闭合对象边界 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为不支持)
|
||||
- `oneOf` / `anyOf` 这类会改变生成类型形状的组合关键字当前会被 Runtime / Generator / Tooling 直接拒绝
|
||||
|
||||
更复杂的 schema shape 需要先回到 schema 设计与 raw YAML 维护路径,而不是假定编辑器工具存在隐藏支持。
|
||||
|
||||
@ -86,6 +86,7 @@
|
||||
- `2026-04-17` 之前的详细实现记录与定向验证命令已归档到历史 tracking / trace
|
||||
- active 跟踪文件只保留当前恢复点、当前状态和下一步,不再重复堆积已完成阶段的完整历史
|
||||
- 最近验证摘要:`2026-04-30` 已完成 Tooling / Docs reader-facing 收口与工具 parser 边界收紧,详细命令、批次背景与验证结果保留在 trace 的 `2026-04-30` 分阶段记录中
|
||||
- PR `#306` follow-up 摘要:已按 latest open review threads 补齐 Generator `anyOf` 对称回归、Tooling schema type 白名单、object-array 直系收集边界,以及 reader-facing docs 的显式 `additionalProperties: false` / adoption guidance 说明;细节和验证命令保留在 trace 的 `2026-04-30` 新增阶段记录中
|
||||
- PR review 跟进指针:当前分支的 latest review follow-up 与后续本地核验结论以 `ai-first-config-system-trace.md` 为准,active tracking 不再重复展开逐条命令历史
|
||||
|
||||
## 下一步
|
||||
|
||||
@ -205,3 +205,29 @@
|
||||
|
||||
1. 继续盘点 Runtime / Generator / Tooling 三端是否还有类似“工具宽松吞掉、主线不支持”的 schema 形状
|
||||
2. 若继续做 Tooling lane,优先补 reader-facing 示例或采用路径,而不是继续堆积边界清单
|
||||
|
||||
### 阶段:PR #306 open threads 收口(AI-FIRST-CONFIG-RP-003)
|
||||
|
||||
- 已重新抓取 PR `#306` 的 latest open review threads,并按“本地仍成立 / 已被当前分支吸收”重新核验
|
||||
- 本轮收口重点不是继续扩能力,而是把 open threads 中仍成立的三类问题一次性补齐:
|
||||
- Generator:补齐 `GF_ConfigSchema_015` 的 `anyOf` 对称负例,避免组合关键字只覆盖 `oneOf`
|
||||
- Tooling:拒绝未知显式 `type`、收窄 object-array 只遍历当前 editor 直属 items、统一 `contains` hint 文案
|
||||
- Docs:把 `additionalProperties: false` 的“必须显式设置为 false”写清,并为工具补最小接入示例、迁移提示与更准确的 raw YAML 回退条件
|
||||
- 本轮同时更新了 JS / .NET 回归测试与 active tracking,避免只修 review comment 不保留恢复点
|
||||
|
||||
### 验证
|
||||
|
||||
- 2026-04-30:`bun run test`(`tools/gframework-config-tool`)
|
||||
- 结果:通过(132 tests)
|
||||
- 备注:新增未知 schema `type` 拒绝、嵌套 object-array 不串层,以及 `contains` hint 文案回归
|
||||
- 2026-04-30:`dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~SchemaConfigGeneratorTests"`
|
||||
- 结果:通过(54 tests)
|
||||
- 备注:补齐 `Run_Should_Report_Diagnostic_When_Object_Schema_Declares_Unsupported_AnyOf`
|
||||
- 2026-04-30:`git diff --check`
|
||||
- 结果:通过
|
||||
- 备注:本轮代码与文档改动未引入空白或冲突标记问题
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 推送本轮修复后,重新抓取 PR `#306` review 状态,确认哪些 open threads 会被 GitHub 自动折叠或仍需人工回复
|
||||
2. 若还有残留 open threads,优先区分“远端未刷新 / 已过时评论 / 仍成立问题”,不要再把 review body 摘要和 latest open threads 混在一起处理
|
||||
|
||||
@ -13,7 +13,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
||||
2. 再进专题页确认安装、生命周期和推荐接线方式
|
||||
3. 最后回到源码中的 XML 文档核对具体契约
|
||||
|
||||
如果你在阅读 AI-First 配置工作流相关 API,先把 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 视为正式契约入口,再把 `VS Code` 配置工具视为辅助层。当前默认采用路径围绕共享 schema 子集展开,其中 `additionalProperties: false` 表示闭合对象边界,`oneOf` / `anyOf` 不在默认入口范围内;更复杂的 shape 应回到 raw YAML 与 schema 设计本体处理。
|
||||
如果你在阅读 AI-First 配置工作流相关 API,先把 `GFramework.Game` Runtime 与 `GFramework.Game.SourceGenerators` 视为正式契约入口,再把 `VS Code` 配置工具视为辅助层。当前默认采用路径围绕共享 schema 子集展开,其中 `additionalProperties: false` 表示闭合对象边界(需显式设置为 `false`);`oneOf` / `anyOf` 在 Runtime / Generator / Tooling 层面会被直接拒绝。更复杂的 shape 应回到 raw YAML 与 schema 设计本体处理。
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
|
||||
@ -109,6 +109,97 @@ Explorer + 表单预览。
|
||||
|
||||
日常采用时,建议把它理解为“优先用工具加速已支持子集的维护;遇到边界时立刻回到 schema + raw YAML 本体确认”。
|
||||
|
||||
### 最小接入示例与兼容 / 迁移说明
|
||||
|
||||
项目里至少需要准备三类内容:
|
||||
|
||||
- `config/<domain>/*.yaml`:实际配置文件
|
||||
- `schemas/<domain>.schema.json`:与该配置域对应的 schema
|
||||
- VS Code 工作区里的 `GFramework Config Tool` 扩展,以及与 schema 保持一致的 `x-gframework-ref-table` 引用约定
|
||||
|
||||
最小目录可以从下面这个形态起步:
|
||||
|
||||
```text
|
||||
GameProject/
|
||||
├─ config/
|
||||
│ └─ monster/
|
||||
│ └─ slime.yaml
|
||||
└─ schemas/
|
||||
└─ monster.schema.json
|
||||
```
|
||||
|
||||
最小 schema 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "name", "rarity", "dropItems"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"title": "Monster Id",
|
||||
"description": "Primary monster key.",
|
||||
"default": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Display Name",
|
||||
"minLength": 1
|
||||
},
|
||||
"rarity": {
|
||||
"type": "string",
|
||||
"enum": ["common", "elite", "boss"],
|
||||
"default": "common"
|
||||
},
|
||||
"spawnTime": {
|
||||
"type": "string",
|
||||
"format": "time"
|
||||
},
|
||||
"dropItems": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"rewardTableId": {
|
||||
"type": "string",
|
||||
"x-gframework-ref-table": "reward-table"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对应的 YAML 初始文件可以保持很小:
|
||||
|
||||
```yaml
|
||||
id: 1
|
||||
name: Slime
|
||||
rarity: common
|
||||
spawnTime: 08:30:00Z
|
||||
dropItems:
|
||||
- potion
|
||||
rewardTableId: starter-reward
|
||||
```
|
||||
|
||||
推荐接入顺序:
|
||||
|
||||
1. 在 VS Code 中打开包含 `config/` 和 `schemas/` 的工作区
|
||||
2. 如果目录不是默认值,先设置 `gframeworkConfig.configPath` 与 `gframeworkConfig.schemasPath`
|
||||
3. 通过 Explorer 打开目标 YAML 或 schema,先跑一次全量校验
|
||||
4. 对空 YAML 使用“基于 schema 的示例 YAML 初始化”,或直接从 raw YAML 开始录入
|
||||
5. 需要统一改同域顶层标量字段时,再进入批量编辑
|
||||
|
||||
迁移自纯 raw YAML 工作流时,至少先检查下面几件事:
|
||||
|
||||
- `additionalProperties` 是否显式设置为 `false`;省略或 `true` 不属于当前共享支持子集
|
||||
- schema 是否依赖 `oneOf` / `anyOf`;这些组合关键字会被 Runtime / Generator / Tooling 直接拒绝
|
||||
- 对象数组里是否混入标量项,或是否存在更深、更异构的数组结构
|
||||
- Runtime / Source Generator 是否已经接受这份 schema,而不是只有编辑器里“暂时看起来能写”
|
||||
|
||||
当 schema 仍在共享支持子集内,但某段编辑路径已经超出轻量表单可视化边界时,优先回到 raw YAML;不要把“工具暂时没有表单入口”误判成“运行时契约已放宽”。
|
||||
|
||||
## 推荐工作流
|
||||
|
||||
### 1. 浏览配置与 schema
|
||||
|
||||
@ -24,7 +24,7 @@ description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说
|
||||
|
||||
- 配置系统的 schema 支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
||||
- `DataRepository`、`UnifiedSettingsDataRepository` 和 `SaveRepository<TSaveData>` 负责的是数据怎么落盘、怎么回读、怎么组织槽位,而不是放宽配置契约
|
||||
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体继续处理,而不是期待 repository 层自动接管这些边界
|
||||
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体继续处理,而不是期待 repository 层自动接管这些边界
|
||||
|
||||
## 什么时候用哪个仓库
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@ IStorage storage = new FileStorage("GameData", serializer);
|
||||
这条工作流的正式契约,以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享支持的 schema
|
||||
子集为准。`VS Code` 配置工具主要负责编辑期提示和表单辅助,不单独扩展运行时可接受的 schema 形状。
|
||||
|
||||
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false` 这类已收口的对象边界,以及
|
||||
开始接入时,建议先把 schema 约束控制在共享子集内,并尽早确认像 `additionalProperties: false`(需显式设置为 `false`;省略或 `true` 视为非 `false`)这类已收口的对象边界,以及
|
||||
`oneOf` / `anyOf` 当前会被直接拒绝,而不是在工具里看起来“可以先写”。如果你的配置模型需要更深层的嵌套数组、联合分支或其他超出共享子集的复杂
|
||||
shape,优先回到 raw YAML 和 schema 设计本体处理,再决定是否拆分结构或调整约束方式。
|
||||
|
||||
|
||||
@ -148,14 +148,14 @@ var restored = serializer.Deserialize(json, data.GetType());
|
||||
|
||||
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [配置系统](./config-system.md)。
|
||||
|
||||
如果你在配置系统里进一步碰到更复杂的 schema shape,也要尽快回到配置系统主文档和 raw YAML / schema 本体继续设计。当前默认采用路径面向的是与 `GFramework.Game` Runtime 和 `Game.SourceGenerators` 对齐的共享 schema 子集,不是任意 `JSON Schema` 的全量支持。
|
||||
如果你在配置系统里进一步碰到更复杂的 schema shape,也要尽快回到配置系统主文档和 raw YAML / schema 本体继续设计。当前默认采用路径面向的是与 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 对齐的共享 schema 子集,不是任意 `JSON Schema` 的全量支持。
|
||||
|
||||
## 当前边界
|
||||
|
||||
- 当前公开默认实现只有 JSON,没有内建 MessagePack、Binary 或 ProtoBuf 实现
|
||||
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>` 或 `SaveRepository<TSaveData>`
|
||||
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
|
||||
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层
|
||||
- 如果配置设计依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他需要开放对象形状与联合分支的复杂约束,请直接按配置系统主文档回到 raw YAML / schema 方案处理,而不是把这些场景归到序列化层
|
||||
|
||||
## 继续阅读
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,
|
||||
如果你关注的是“配置内容最后怎么变成运行时设置”,这里也需要先分清职责:
|
||||
|
||||
- 配置 schema 的正式支持边界,仍以 `GFramework.Game` Runtime 和 `GFramework.Game.SourceGenerators` 当前共享的 schema 子集为准
|
||||
- `UnifiedSettingsDataRepository`、`SettingsModel<TRepository>` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties` 等配置边界
|
||||
- `UnifiedSettingsDataRepository`、`SettingsModel<TRepository>` 和 `SettingsSystem` 负责设置数据的加载、迁移、保存与应用,不负责放宽 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`)等配置边界
|
||||
- 一旦配置设计开始依赖更复杂的 schema shape,应直接回到 [配置系统](./config-system.md) 与 raw YAML / schema 本体处理,再决定设置层怎么消费这些结果
|
||||
|
||||
## 当前公开入口
|
||||
|
||||
@ -174,7 +174,7 @@ var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
|
||||
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
|
||||
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
|
||||
- 原子写入只覆盖单文件替换,不等于多文件事务
|
||||
- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`,或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理
|
||||
- 如果配置建模依赖 `oneOf`、`anyOf`、非 `false` 的 `additionalProperties`(例如省略或 `true`),或其他超出当前共享 schema 子集的复杂组合约束,这不是 `IStorage` 层能放宽的限制;应直接回到配置系统主文档与 raw YAML / schema 设计处理
|
||||
|
||||
## 继续阅读
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ GFramework 当前发布的生成器包是:
|
||||
当前不属于默认采用路径的典型情况包括:
|
||||
|
||||
- `oneOf`、`anyOf` 这类会改变生成类型形状的组合关键字
|
||||
- 非 `false` 的 `additionalProperties`
|
||||
- 非 `false` 的 `additionalProperties`(例如省略或 `true`)
|
||||
- 其他需要开放对象形状、联合分支或更自由属性合并的 schema 设计
|
||||
|
||||
这些场景当前不应被理解为“文档还没写到的隐藏支持”,而应被理解为:它们不在 `GFramework.Game` 现阶段共享配置契约内。
|
||||
|
||||
@ -109,11 +109,21 @@ This extension is an editor-side helper. It does not define the runtime contract
|
||||
5. Open the lightweight form preview or domain batch editing actions, then fall back to raw YAML for deeper nested edits
|
||||
when needed.
|
||||
|
||||
Minimal adoption checklist:
|
||||
|
||||
- Keep one workspace folder that contains both `config/` and `schemas/`
|
||||
- Place each config domain under `config/<domain>/*.yaml`
|
||||
- Place the matching schema at `schemas/<domain>.schema.json`
|
||||
- Use `x-gframework-ref-table` only on fields that should link to another config domain or reference file
|
||||
- Keep `additionalProperties` explicitly set to `false` when you need closed-object validation; omitting it or setting
|
||||
it to `true` is outside the supported subset
|
||||
|
||||
Use raw YAML directly when you need:
|
||||
|
||||
- deeper or more heterogeneous array shapes
|
||||
- object rules centered on `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else`
|
||||
- `contains` / `minContains` / `maxContains` verification on structures that are easier to reason about in source form
|
||||
- supported object rules such as `allOf`, `dependentSchemas`, or object-focused `if` / `then` / `else` only when they
|
||||
push the edit path beyond the lightweight form boundary
|
||||
- `contains` / `minContains` / `maxContains` when the structure is easier to reason about directly in YAML
|
||||
- schema designs outside the current shared subset, including `oneOf`, `anyOf`, or non-`false` `additionalProperties`
|
||||
|
||||
## Documentation
|
||||
|
||||
@ -19,6 +19,7 @@ const DurationFormatPattern =
|
||||
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", "duration", "email", "time", "uri", "uuid"]);
|
||||
const SupportedSchemaTypes = new Set(["object", "array", "string", "integer", "number", "boolean"]);
|
||||
|
||||
/**
|
||||
* Compare two strings using the same UTF-16 code-unit ordering as C#'s
|
||||
@ -1104,7 +1105,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
|
||||
validateUnsupportedOpenObjectKeyword(value, displayPath);
|
||||
|
||||
const type = typeof value.type === "string" ? value.type : "object";
|
||||
const type = resolveSupportedSchemaType(value.type, displayPath);
|
||||
const patternMetadata = normalizeSchemaPattern(value.pattern, displayPath);
|
||||
const stringFormat = normalizeSchemaStringFormat(value.format, type, displayPath);
|
||||
const negatedSchemaNode = parseNegatedSchemaNode(value.not, displayPath);
|
||||
@ -1298,13 +1299,7 @@ function validateUnsupportedOpenObjectKeyword(schemaNode, displayPath) {
|
||||
* @returns {SchemaNode} Parsed child schema node.
|
||||
*/
|
||||
function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||
const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName);
|
||||
if (childNode) {
|
||||
return childNode;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
|
||||
return parseArrayChildSchema(rawChild, displayPath, keywordName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1316,13 +1311,7 @@ function parseRequiredArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||
* @returns {SchemaNode | undefined} Parsed child schema node.
|
||||
*/
|
||||
function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||
const childNode = parseArrayChildSchema(rawChild, displayPath, keywordName);
|
||||
if (childNode) {
|
||||
return childNode;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
|
||||
return parseArrayChildSchema(rawChild, displayPath, keywordName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1333,18 +1322,40 @@ function parseOptionalArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||
* @param {unknown} rawChild Raw child schema node.
|
||||
* @param {string} displayPath Logical parent array path.
|
||||
* @param {"items" | "contains"} keywordName Child schema keyword.
|
||||
* @returns {SchemaNode | undefined} Parsed child schema node.
|
||||
* @returns {SchemaNode} Parsed child schema node.
|
||||
*/
|
||||
function parseArrayChildSchema(rawChild, displayPath, keywordName) {
|
||||
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) {
|
||||
return undefined;
|
||||
throw new Error(
|
||||
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
|
||||
}
|
||||
|
||||
if (typeof rawChild.type !== "string") {
|
||||
return undefined;
|
||||
throw new Error(
|
||||
`Schema property '${displayPath}' must declare '${keywordName}' as an object-valued schema with an explicit 'type'.`);
|
||||
}
|
||||
|
||||
return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath, keywordName));
|
||||
return parseSchemaNode(rawChild, joinArrayTemplatePath(displayPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one schema type while rejecting explicit strings that the shared
|
||||
* subset does not support.
|
||||
*
|
||||
* @param {unknown} rawType Raw schema type value.
|
||||
* @param {string} displayPath Logical property path.
|
||||
* @returns {"object" | "array" | "string" | "integer" | "number" | "boolean"} Supported schema type.
|
||||
*/
|
||||
function resolveSupportedSchemaType(rawType, displayPath) {
|
||||
if (typeof rawType !== "string") {
|
||||
return "object";
|
||||
}
|
||||
|
||||
if (!SupportedSchemaTypes.has(rawType)) {
|
||||
throw new Error(`Schema property '${displayPath}' declares unsupported type '${rawType}'.`);
|
||||
}
|
||||
|
||||
return rawType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1009,8 +1009,14 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
||||
current = current[segment];
|
||||
}
|
||||
}
|
||||
function getDirectObjectArrayItems(editor) {
|
||||
const itemsHost = editor.querySelector(":scope > [data-object-array-items]");
|
||||
return itemsHost
|
||||
? Array.from(itemsHost.querySelectorAll(":scope > [data-object-array-item]"))
|
||||
: [];
|
||||
}
|
||||
function renumberObjectArrayItems(editor) {
|
||||
const items = editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]");
|
||||
const items = getDirectObjectArrayItems(editor);
|
||||
items.forEach((item, index) => {
|
||||
const title = item.querySelector(".object-array-item-title");
|
||||
if (title) {
|
||||
@ -1023,7 +1029,7 @@ function renderFormHtml(fileName, schemaInfo, parsedYaml, options) {
|
||||
}
|
||||
function collectObjectArrayEditorItems(editor) {
|
||||
const items = [];
|
||||
for (const item of editor.querySelectorAll("[data-object-array-items] > [data-object-array-item]")) {
|
||||
for (const item of getDirectObjectArrayItems(editor)) {
|
||||
items.push(collectObjectArrayItemValue(item));
|
||||
}
|
||||
|
||||
|
||||
@ -247,7 +247,7 @@ const zhCnMessages = {
|
||||
"webview.hint.format": "格式:{value}",
|
||||
"webview.hint.minItems": "最少元素数:{value}",
|
||||
"webview.hint.maxItems": "最多元素数:{value}",
|
||||
"webview.hint.contains": "Contains 条件:{summary}",
|
||||
"webview.hint.contains": "contains 条件:{summary}",
|
||||
"webview.hint.minContains": "最少匹配数:{value}",
|
||||
"webview.hint.maxContains": "最多匹配数:{value}",
|
||||
"webview.hint.uniqueItems": "元素必须唯一",
|
||||
|
||||
@ -225,6 +225,21 @@ test("parseSchemaContent should reject unsupported additionalProperties forms",
|
||||
/unsupported 'additionalProperties' metadata/u);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should reject unsupported explicit schema types", () => {
|
||||
assert.throws(
|
||||
() => parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reward": {
|
||||
"type": "bogus"
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
/declares unsupported type 'bogus'/u);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should build object const comparable keys with ordinal property ordering", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
@ -2701,6 +2716,49 @@ test("applyFormUpdates should rewrite nested object arrays from structured form
|
||||
assert.match(updated, /^ value: true$/mu);
|
||||
});
|
||||
|
||||
test("applyFormUpdates should not mix nested object-array items into the parent array", () => {
|
||||
const updated = applyFormUpdates(
|
||||
[
|
||||
"phases:",
|
||||
" -",
|
||||
" wave: 1"
|
||||
].join("\n"),
|
||||
{
|
||||
objectArrays: {
|
||||
phases: [
|
||||
{
|
||||
wave: "1",
|
||||
spawns: [
|
||||
{
|
||||
monsterId: "slime"
|
||||
},
|
||||
{
|
||||
monsterId: "goblin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
wave: "2",
|
||||
spawns: [
|
||||
{
|
||||
monsterId: "bat"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal((updated.match(/^ -$/gmu) || []).length, 2);
|
||||
assert.equal((updated.match(/^ -$/gmu) || []).length, 3);
|
||||
assert.doesNotMatch(updated, /^ monsterId: slime$/mu);
|
||||
assert.doesNotMatch(updated, /^ monsterId: goblin$/mu);
|
||||
assert.match(updated, /^ -$/mu);
|
||||
assert.match(updated, /^ monsterId: slime$/mu);
|
||||
assert.match(updated, /^ monsterId: goblin$/mu);
|
||||
assert.match(updated, /^ monsterId: bat$/mu);
|
||||
});
|
||||
|
||||
test("applyFormUpdates should clear object arrays when the form removes all items", () => {
|
||||
const updated = applyFormUpdates(
|
||||
[
|
||||
|
||||
@ -111,7 +111,7 @@ test("buildContainsHintLines should use updated Chinese contains hint wording",
|
||||
localizer);
|
||||
|
||||
assert.deepEqual(lines, [
|
||||
"Contains 条件:string, 允许值:potion, elixir",
|
||||
"contains 条件:string, 允许值:potion, elixir",
|
||||
"最少匹配数:1",
|
||||
"最多匹配数:2"
|
||||
]);
|
||||
|
||||
@ -68,6 +68,9 @@ test("createLocalizer should expose contains-count validation keys", () => {
|
||||
assert.equal(
|
||||
chineseLocalizer.t(ValidationMessageKeys.maxContainsViolation, {displayPath: "dropRates", value: 1}),
|
||||
"属性“dropRates”最多只能包含 1 个匹配 contains 条件的元素。");
|
||||
assert.equal(
|
||||
chineseLocalizer.t("webview.hint.contains", {summary: "object, Required: itemCount"}),
|
||||
"contains 条件:object, Required: itemCount");
|
||||
});
|
||||
|
||||
test("createLocalizer should resolve dependentRequired through the explicit validation key", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user