mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 13:14:30 +08:00
feat(config): 添加配置验证工具功能
- 实现了JSON Schema解析和YAML验证功能 - 添加了对象和数组枚举值的比较验证逻辑 - 实现了配置文件的采样生成功能 - 添加了批量编辑器的数值更新功能 - 实现了配置路径和注释提取功能 - 添加了多种数据格式验证支持包括日期、邮箱、UUID等 - 实现了常量和枚举值的元数据处理功能 - 添加了配置验证诊断信息生成功能 - 实现了表单更新应用到YAML的功能 - 添加了字符串排序比较算法确保工具一致性
This commit is contained in:
parent
a8cb82e2f1
commit
33dd108697
@ -91,6 +91,54 @@ public class SchemaConfigGeneratorEnumTests
|
|||||||
await AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
|
await AssertSnapshotAsync(result, "MonsterConfig.ArrayItemEnum.g.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证对象数组项 <c>enum</c> 文档回退输出与快照保持一致。
|
||||||
|
/// </summary>
|
||||||
|
[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");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 对单个生成文件执行快照断言。
|
/// 对单个生成文件执行快照断言。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MonsterConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property path 'id'.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Schema property path: 'id'.
|
||||||
|
/// </remarks>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property path 'phases'.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Schema property path: 'phases'.
|
||||||
|
/// Allowed values: { "wave": 1, "monsterId": "slime" }, { "wave": 2, "monsterId": "goblin" }.
|
||||||
|
/// Generated default initializer: = global::System.Array.Empty<PhasesItemConfig>();
|
||||||
|
/// </remarks>
|
||||||
|
public global::System.Collections.Generic.IReadOnlyList<PhasesItemConfig> Phases { get; set; } = global::System.Array.Empty<PhasesItemConfig>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class PhasesItemConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property path 'phases[].wave'.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Schema property path: 'phases[].wave'.
|
||||||
|
/// </remarks>
|
||||||
|
public int Wave { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value mapped from schema property path 'phases[].monsterId'.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Schema property path: 'phases[].monsterId'.
|
||||||
|
/// Generated default initializer: = string.Empty;
|
||||||
|
/// </remarks>
|
||||||
|
public string MonsterId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -857,7 +857,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"array",
|
"array",
|
||||||
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
|
$"global::System.Collections.Generic.IReadOnlyList<{objectSpec.ClassName}>",
|
||||||
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
|
$" = global::System.Array.Empty<{objectSpec.ClassName}>();",
|
||||||
TryBuildEnumDocumentation(property.Value, "array"),
|
TryBuildEnumDocumentation(property.Value, "array") ??
|
||||||
|
TryBuildEnumDocumentation(itemsElement, "object"),
|
||||||
TryBuildConstraintDocumentation(property.Value, "array"),
|
TryBuildConstraintDocumentation(property.Value, "array"),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -866,7 +867,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
"object",
|
"object",
|
||||||
objectSpec.ClassName,
|
objectSpec.ClassName,
|
||||||
null,
|
null,
|
||||||
null,
|
TryBuildEnumDocumentation(itemsElement, "object"),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
objectSpec,
|
objectSpec,
|
||||||
|
|||||||
@ -592,6 +592,10 @@ function applyEnumMetadata(schemaNode, rawEnum, displayPath) {
|
|||||||
return schemaNode;
|
return schemaNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rawEnum.length === 0) {
|
||||||
|
throw new Error(`Schema property '${displayPath}' must declare 'enum' with at least one value.`);
|
||||||
|
}
|
||||||
|
|
||||||
const enumComparableValues = [];
|
const enumComparableValues = [];
|
||||||
const enumDisplayValues = [];
|
const enumDisplayValues = [];
|
||||||
const enumValues = [];
|
const enumValues = [];
|
||||||
@ -614,6 +618,7 @@ function applyEnumMetadata(schemaNode, rawEnum, displayPath) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...schemaNode,
|
...schemaNode,
|
||||||
|
enumSampleValue: rawEnum[0],
|
||||||
enumValues: enumValues.length > 0 ? enumValues : undefined,
|
enumValues: enumValues.length > 0 ? enumValues : undefined,
|
||||||
enumDisplayValues: enumDisplayValues.length > 0 ? enumDisplayValues : undefined,
|
enumDisplayValues: enumDisplayValues.length > 0 ? enumDisplayValues : undefined,
|
||||||
enumComparableValues: enumComparableValues.length > 0 ? enumComparableValues : undefined
|
enumComparableValues: enumComparableValues.length > 0 ? enumComparableValues : undefined
|
||||||
@ -2584,6 +2589,10 @@ function createObjectNode() {
|
|||||||
*/
|
*/
|
||||||
function createSampleNodeFromSchema(schemaNode) {
|
function createSampleNodeFromSchema(schemaNode) {
|
||||||
if (!schemaNode || schemaNode.type === "object") {
|
if (!schemaNode || schemaNode.type === "object") {
|
||||||
|
if (schemaNode && schemaNode.enumSampleValue !== undefined) {
|
||||||
|
return createNodeFromFormValue(schemaNode.enumSampleValue);
|
||||||
|
}
|
||||||
|
|
||||||
const objectNode = createObjectNode();
|
const objectNode = createObjectNode();
|
||||||
for (const [key, propertySchema] of Object.entries(schemaNode && schemaNode.properties ? schemaNode.properties : {})) {
|
for (const [key, propertySchema] of Object.entries(schemaNode && schemaNode.properties ? schemaNode.properties : {})) {
|
||||||
const childNode = createSampleNodeFromSchema(propertySchema);
|
const childNode = createSampleNodeFromSchema(propertySchema);
|
||||||
@ -2594,6 +2603,10 @@ function createSampleNodeFromSchema(schemaNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.type === "array") {
|
if (schemaNode.type === "array") {
|
||||||
|
if (schemaNode.enumSampleValue !== undefined) {
|
||||||
|
return createNodeFromFormValue(schemaNode.enumSampleValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (schemaNode.items.type === "object") {
|
if (schemaNode.items.type === "object") {
|
||||||
return createArrayNode([createSampleNodeFromSchema(schemaNode.items)]);
|
return createArrayNode([createSampleNodeFromSchema(schemaNode.items)]);
|
||||||
}
|
}
|
||||||
@ -2870,6 +2883,7 @@ module.exports = {
|
|||||||
* title?: string,
|
* title?: string,
|
||||||
* description?: string,
|
* description?: string,
|
||||||
* defaultValue?: string,
|
* defaultValue?: string,
|
||||||
|
* enumSampleValue?: unknown,
|
||||||
* enumDisplayValues?: string[],
|
* enumDisplayValues?: string[],
|
||||||
* enumComparableValues?: string[],
|
* enumComparableValues?: string[],
|
||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
@ -2882,6 +2896,7 @@ module.exports = {
|
|||||||
* title?: string,
|
* title?: string,
|
||||||
* description?: string,
|
* description?: string,
|
||||||
* defaultValue?: string,
|
* defaultValue?: string,
|
||||||
|
* enumSampleValue?: unknown,
|
||||||
* enumDisplayValues?: string[],
|
* enumDisplayValues?: string[],
|
||||||
* enumComparableValues?: string[],
|
* enumComparableValues?: string[],
|
||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
@ -2902,6 +2917,7 @@ module.exports = {
|
|||||||
* title?: string,
|
* title?: string,
|
||||||
* description?: string,
|
* description?: string,
|
||||||
* defaultValue?: string,
|
* defaultValue?: string,
|
||||||
|
* enumSampleValue?: unknown,
|
||||||
* constValue?: string,
|
* constValue?: string,
|
||||||
* constDisplayValue?: string,
|
* constDisplayValue?: string,
|
||||||
* constComparableValue?: string,
|
* constComparableValue?: string,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const test = require("node:test");
|
const test = require("node:test");
|
||||||
const assert = require("node:assert/strict");
|
const assert = require("node:assert/strict");
|
||||||
const {
|
const {
|
||||||
|
createSampleConfigYaml,
|
||||||
parseSchemaContent,
|
parseSchemaContent,
|
||||||
parseTopLevelYaml,
|
parseTopLevelYaml,
|
||||||
validateParsedConfig
|
validateParsedConfig
|
||||||
@ -161,3 +162,66 @@ dropLevels:
|
|||||||
assert.match(diagnostics[0].message, /dropLevels\[1\]/u);
|
assert.match(diagnostics[0].message, /dropLevels\[1\]/u);
|
||||||
assert.doesNotMatch(diagnostics[0].message, /must be one of|必须是以下值之一/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);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user