From 33dd10869797ae5a3a2c2cba8589ec0fec52f4d9 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 16 Apr 2026 15:12:02 +0800
Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E9=AA=8C=E8=AF=81=E5=B7=A5=E5=85=B7=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了JSON Schema解析和YAML验证功能
- 添加了对象和数组枚举值的比较验证逻辑
- 实现了配置文件的采样生成功能
- 添加了批量编辑器的数值更新功能
- 实现了配置路径和注释提取功能
- 添加了多种数据格式验证支持包括日期、邮箱、UUID等
- 实现了常量和枚举值的元数据处理功能
- 添加了配置验证诊断信息生成功能
- 实现了表单更新应用到YAML的功能
- 添加了字符串排序比较算法确保工具一致性
---
.../Config/SchemaConfigGeneratorEnumTests.cs | 48 ++++++++++++++
.../MonsterConfig.ArrayObjectItemEnum.g.txt | 54 ++++++++++++++++
.../Config/SchemaConfigGenerator.cs | 5 +-
.../src/configValidation.js | 16 +++++
.../test/configValidation.enum.test.js | 64 +++++++++++++++++++
5 files changed, 185 insertions(+), 2 deletions(-)
create mode 100644 GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGeneratorEnum/MonsterConfig.ArrayObjectItemEnum.g.txt
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorEnumTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorEnumTests.cs
index 56e551c9..801a343c 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorEnumTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorEnumTests.cs
@@ -91,6 +91,54 @@ public class SchemaConfigGeneratorEnumTests
await AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
}
+ ///
+ /// 验证对象数组项 enum 文档回退输出与快照保持一致。
+ ///
+ [Test]
+ public async Task Snapshot_Should_Preserve_Array_Object_Item_Enum_Documentation_Fallback()
+ {
+ const string source = """
+ namespace TestApp
+ {
+ public sealed class Dummy
+ {
+ }
+ }
+ """;
+
+ const string schema = """
+ {
+ "type": "object",
+ "required": ["id", "phases"],
+ "properties": {
+ "id": { "type": "integer" },
+ "phases": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["wave", "monsterId"],
+ "properties": {
+ "wave": { "type": "integer" },
+ "monsterId": { "type": "string" }
+ },
+ "enum": [
+ { "wave": 1, "monsterId": "slime" },
+ { "wave": 2, "monsterId": "goblin" }
+ ]
+ }
+ }
+ }
+ }
+ """;
+
+ var result = SchemaGeneratorTestDriver.Run(
+ source,
+ ("monster.schema.json", schema));
+
+ Assert.That(result.Results.Single().Diagnostics, Is.Empty);
+ await AssertSnapshotAsync(result, "MonsterConfig.ArrayObjectItemEnum.g.txt");
+ }
+
///
/// 对单个生成文件执行快照断言。
///
diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGeneratorEnum/MonsterConfig.ArrayObjectItemEnum.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGeneratorEnum/MonsterConfig.ArrayObjectItemEnum.g.txt
new file mode 100644
index 00000000..3c9def56
--- /dev/null
+++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGeneratorEnum/MonsterConfig.ArrayObjectItemEnum.g.txt
@@ -0,0 +1,54 @@
+//
+#nullable enable
+
+namespace GFramework.Game.Config.Generated;
+
+///
+/// Auto-generated config type for schema file 'monster.schema.json'.
+/// This type is generated from JSON schema so runtime loading and editor tooling can share the same contract.
+///
+public sealed partial class MonsterConfig
+{
+ ///
+ /// Gets or sets the value mapped from schema property path 'id'.
+ ///
+ ///
+ /// Schema property path: 'id'.
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// Gets or sets the value mapped from schema property path 'phases'.
+ ///
+ ///
+ /// Schema property path: 'phases'.
+ /// Allowed values: { "wave": 1, "monsterId": "slime" }, { "wave": 2, "monsterId": "goblin" }.
+ /// Generated default initializer: = global::System.Array.Empty<PhasesItemConfig>();
+ ///
+ public global::System.Collections.Generic.IReadOnlyList Phases { get; set; } = global::System.Array.Empty();
+
+ ///
+ /// Auto-generated nested config type for schema property path 'phases[]'.
+ /// This nested type is generated so object-valued schema fields remain strongly typed in consumer code.
+ ///
+ public sealed partial class PhasesItemConfig
+ {
+ ///
+ /// Gets or sets the value mapped from schema property path 'phases[].wave'.
+ ///
+ ///
+ /// Schema property path: 'phases[].wave'.
+ ///
+ public int Wave { get; set; }
+
+ ///
+ /// Gets or sets the value mapped from schema property path 'phases[].monsterId'.
+ ///
+ ///
+ /// Schema property path: 'phases[].monsterId'.
+ /// Generated default initializer: = string.Empty;
+ ///
+ public string MonsterId { get; set; } = string.Empty;
+
+ }
+}
\ No newline at end of file
diff --git a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
index 369bcafa..a85cc5ae 100644
--- a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
+++ b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
@@ -857,7 +857,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
"array",
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
- TryBuildEnumDocumentation(property.Value, "array"),
+ TryBuildEnumDocumentation(property.Value, "array") ??
+ TryBuildEnumDocumentation(itemsElement, "object"),
TryBuildConstraintDocumentation(property.Value, "array"),
null,
null,
@@ -866,7 +867,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
"object",
objectSpec.ClassName,
null,
- null,
+ TryBuildEnumDocumentation(itemsElement, "object"),
null,
null,
objectSpec,
diff --git a/tools/gframework-config-tool/src/configValidation.js b/tools/gframework-config-tool/src/configValidation.js
index 3850f4eb..5038f22b 100644
--- a/tools/gframework-config-tool/src/configValidation.js
+++ b/tools/gframework-config-tool/src/configValidation.js
@@ -592,6 +592,10 @@ function applyEnumMetadata(schemaNode, rawEnum, displayPath) {
return schemaNode;
}
+ if (rawEnum.length === 0) {
+ throw new Error(`Schema property '${displayPath}' must declare 'enum' with at least one value.`);
+ }
+
const enumComparableValues = [];
const enumDisplayValues = [];
const enumValues = [];
@@ -614,6 +618,7 @@ function applyEnumMetadata(schemaNode, rawEnum, displayPath) {
return {
...schemaNode,
+ enumSampleValue: rawEnum[0],
enumValues: enumValues.length > 0 ? enumValues : undefined,
enumDisplayValues: enumDisplayValues.length > 0 ? enumDisplayValues : undefined,
enumComparableValues: enumComparableValues.length > 0 ? enumComparableValues : undefined
@@ -2584,6 +2589,10 @@ function createObjectNode() {
*/
function createSampleNodeFromSchema(schemaNode) {
if (!schemaNode || schemaNode.type === "object") {
+ if (schemaNode && schemaNode.enumSampleValue !== undefined) {
+ return createNodeFromFormValue(schemaNode.enumSampleValue);
+ }
+
const objectNode = createObjectNode();
for (const [key, propertySchema] of Object.entries(schemaNode && schemaNode.properties ? schemaNode.properties : {})) {
const childNode = createSampleNodeFromSchema(propertySchema);
@@ -2594,6 +2603,10 @@ function createSampleNodeFromSchema(schemaNode) {
}
if (schemaNode.type === "array") {
+ if (schemaNode.enumSampleValue !== undefined) {
+ return createNodeFromFormValue(schemaNode.enumSampleValue);
+ }
+
if (schemaNode.items.type === "object") {
return createArrayNode([createSampleNodeFromSchema(schemaNode.items)]);
}
@@ -2870,6 +2883,7 @@ module.exports = {
* title?: string,
* description?: string,
* defaultValue?: string,
+ * enumSampleValue?: unknown,
* enumDisplayValues?: string[],
* enumComparableValues?: string[],
* constValue?: string,
@@ -2882,6 +2896,7 @@ module.exports = {
* title?: string,
* description?: string,
* defaultValue?: string,
+ * enumSampleValue?: unknown,
* enumDisplayValues?: string[],
* enumComparableValues?: string[],
* constValue?: string,
@@ -2902,6 +2917,7 @@ module.exports = {
* title?: string,
* description?: string,
* defaultValue?: string,
+ * enumSampleValue?: unknown,
* constValue?: string,
* constDisplayValue?: string,
* constComparableValue?: string,
diff --git a/tools/gframework-config-tool/test/configValidation.enum.test.js b/tools/gframework-config-tool/test/configValidation.enum.test.js
index 164bea05..6569d588 100644
--- a/tools/gframework-config-tool/test/configValidation.enum.test.js
+++ b/tools/gframework-config-tool/test/configValidation.enum.test.js
@@ -1,6 +1,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const {
+ createSampleConfigYaml,
parseSchemaContent,
parseTopLevelYaml,
validateParsedConfig
@@ -161,3 +162,66 @@ dropLevels:
assert.match(diagnostics[0].message, /dropLevels\[1\]/u);
assert.doesNotMatch(diagnostics[0].message, /must be one of|必须是以下值之一/u);
});
+
+test("createSampleConfigYaml should reuse object and array enum payloads for valid samples", () => {
+ const schema = parseSchemaContent(`
+ {
+ "type": "object",
+ "required": ["reward", "phases"],
+ "properties": {
+ "reward": {
+ "type": "object",
+ "required": ["gold", "itemId"],
+ "properties": {
+ "gold": { "type": "integer" },
+ "itemId": { "type": "string" }
+ },
+ "enum": [
+ { "gold": 10, "itemId": "potion" }
+ ]
+ },
+ "phases": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["wave", "monsterId"],
+ "properties": {
+ "wave": { "type": "integer" },
+ "monsterId": { "type": "string" }
+ },
+ "enum": [
+ { "wave": 1, "monsterId": "slime" }
+ ]
+ }
+ }
+ }
+ }
+ `);
+
+ const sample = createSampleConfigYaml(schema);
+ const yaml = parseTopLevelYaml(sample);
+ const diagnostics = validateParsedConfig(schema, yaml);
+
+ assert.equal(diagnostics.length, 0);
+ assert.match(sample, /^reward:$/mu);
+ assert.match(sample, /^ gold: 10$/mu);
+ assert.match(sample, /^ itemId: potion$/mu);
+ assert.match(sample, /^phases:$/mu);
+ assert.match(sample, /^ -$/mu);
+ assert.match(sample, /^ wave: 1$/mu);
+ assert.match(sample, /^ monsterId: slime$/mu);
+});
+
+test("parseSchemaContent should reject empty enum arrays", () => {
+ assert.throws(() => parseSchemaContent(`
+ {
+ "type": "object",
+ "properties": {
+ "rarity": {
+ "type": "string",
+ "enum": []
+ }
+ }
+ }
+ `), /must declare 'enum' with at least one value/u);
+});