mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 12:14:30 +08:00
feat(game): 添加游戏内容配置系统
- 实现基于 YAML 的配置文件加载功能 - 集成 JSON Schema 结构验证和类型检查 - 提供一对象一文件的目录组织方式 - 支持运行时只读查询和类型安全访问 - 实现 Source Generator 生成配置类型和表包装 - 添加 VS Code 插件提供配置浏览和编辑功能 - 支持跨表引用校验和依赖关系管理 - 实现开发期热重载功能,支持配置变更自动刷新 - 提供完整的配置加载、验证、注册和访问接口
This commit is contained in:
parent
a92e514ffe
commit
12ce31f82a
97
GFramework.Game.Abstractions/Config/ConfigLoadDiagnostic.cs
Normal file
97
GFramework.Game.Abstractions/Config/ConfigLoadDiagnostic.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一次配置加载失败的结构化诊断信息。
|
||||||
|
/// 该模型旨在为日志、测试断言、编辑器联动和热重载失败回调提供稳定字段,
|
||||||
|
/// 避免调用方只能依赖异常消息文本做脆弱解析。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConfigLoadDiagnostic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化一个配置加载诊断对象。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failureKind">失败类别。</param>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
|
/// <param name="configDirectoryPath">配置目录绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="yamlPath">配置文件绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="schemaPath">schema 文件绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="displayPath">逻辑字段路径;无法定位到字段时为空。</param>
|
||||||
|
/// <param name="referencedTableName">跨表引用目标表名称;非引用失败时为空。</param>
|
||||||
|
/// <param name="rawValue">原始值或引用值;不适用时为空。</param>
|
||||||
|
/// <param name="detail">附加细节,用于补充无法结构化成独立字段的上下文。</param>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="tableName" /> 为空时抛出。</exception>
|
||||||
|
public ConfigLoadDiagnostic(
|
||||||
|
ConfigLoadFailureKind failureKind,
|
||||||
|
string tableName,
|
||||||
|
string? configDirectoryPath = null,
|
||||||
|
string? yamlPath = null,
|
||||||
|
string? schemaPath = null,
|
||||||
|
string? displayPath = null,
|
||||||
|
string? referencedTableName = null,
|
||||||
|
string? rawValue = null,
|
||||||
|
string? detail = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tableName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
FailureKind = failureKind;
|
||||||
|
TableName = tableName;
|
||||||
|
ConfigDirectoryPath = configDirectoryPath;
|
||||||
|
YamlPath = yamlPath;
|
||||||
|
SchemaPath = schemaPath;
|
||||||
|
DisplayPath = displayPath;
|
||||||
|
ReferencedTableName = referencedTableName;
|
||||||
|
RawValue = rawValue;
|
||||||
|
Detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取失败类别。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigLoadFailureKind FailureKind { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所属配置表名称。
|
||||||
|
/// </summary>
|
||||||
|
public string TableName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取配置目录绝对路径。
|
||||||
|
/// </summary>
|
||||||
|
public string? ConfigDirectoryPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取触发失败的 YAML 文件绝对路径。
|
||||||
|
/// </summary>
|
||||||
|
public string? YamlPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取触发失败的 schema 文件绝对路径。
|
||||||
|
/// </summary>
|
||||||
|
public string? SchemaPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取便于展示的字段路径。
|
||||||
|
/// 对于根级失败或文件级失败,该值可能为空。
|
||||||
|
/// </summary>
|
||||||
|
public string? DisplayPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取跨表引用目标表名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? ReferencedTableName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取与失败相关的原始值。
|
||||||
|
/// 该字段通常用于 enum 违规、跨表引用缺失或类型转换失败等场景。
|
||||||
|
/// </summary>
|
||||||
|
public string? RawValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取补充细节。
|
||||||
|
/// 当失败上下文无法拆成更多稳定字段时,该值用于保留关键说明。
|
||||||
|
/// </summary>
|
||||||
|
public string? Detail { get; }
|
||||||
|
}
|
||||||
41
GFramework.Game.Abstractions/Config/ConfigLoadException.cs
Normal file
41
GFramework.Game.Abstractions/Config/ConfigLoadException.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示配置加载流程中的结构化失败。
|
||||||
|
/// 该异常保留原有异常链,同时通过 <see cref="Diagnostic" /> 暴露稳定字段,
|
||||||
|
/// 便于上层在不解析消息文本的情况下识别失败表、文件和字段位置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConfigLoadException : InvalidOperationException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化一个配置加载异常。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="diagnostic">结构化诊断信息。</param>
|
||||||
|
/// <param name="message">面向人类阅读的错误消息。</param>
|
||||||
|
/// <param name="innerException">底层异常;不存在时为空。</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="diagnostic" /> 为空时抛出。</exception>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="message" /> 为空时抛出。</exception>
|
||||||
|
public ConfigLoadException(
|
||||||
|
ConfigLoadDiagnostic diagnostic,
|
||||||
|
string message,
|
||||||
|
Exception? innerException = null)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
if (diagnostic == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(diagnostic));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Exception message cannot be null or whitespace.", nameof(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
Diagnostic = diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取结构化诊断信息。
|
||||||
|
/// </summary>
|
||||||
|
public ConfigLoadDiagnostic Diagnostic { get; }
|
||||||
|
}
|
||||||
109
GFramework.Game.Abstractions/Config/ConfigLoadFailureKind.cs
Normal file
109
GFramework.Game.Abstractions/Config/ConfigLoadFailureKind.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
namespace GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示配置加载过程中可稳定断言的失败类别。
|
||||||
|
/// 该枚举用于把文件系统、schema 校验、反序列化和跨表引用错误从自由文本消息中抽离出来,
|
||||||
|
/// 便于日志、测试和上层工具以结构化方式处理失败原因。
|
||||||
|
/// </summary>
|
||||||
|
public enum ConfigLoadFailureKind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置目录不存在。
|
||||||
|
/// </summary>
|
||||||
|
ConfigDirectoryNotFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 绑定的 schema 文件不存在。
|
||||||
|
/// </summary>
|
||||||
|
SchemaFileNotFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取 schema 文件失败。
|
||||||
|
/// </summary>
|
||||||
|
SchemaReadFailed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 文件不是合法 JSON。
|
||||||
|
/// </summary>
|
||||||
|
SchemaInvalidJson,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// schema 内容超出了当前运行时支持的子集或不满足最小约束。
|
||||||
|
/// </summary>
|
||||||
|
SchemaUnsupported,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取配置文件失败。
|
||||||
|
/// </summary>
|
||||||
|
ConfigFileReadFailed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 文本在进入 schema 校验阶段前无法被解析。
|
||||||
|
/// </summary>
|
||||||
|
YamlParseFailed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 文档数量或结构不符合运行时约束。
|
||||||
|
/// </summary>
|
||||||
|
InvalidYamlDocument,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对象中出现了重复字段。
|
||||||
|
/// </summary>
|
||||||
|
DuplicateProperty,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 中出现了 schema 未声明的字段。
|
||||||
|
/// </summary>
|
||||||
|
UnknownProperty,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 缺失 schema 要求的字段。
|
||||||
|
/// </summary>
|
||||||
|
MissingRequiredProperty,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 值类型与 schema 声明不匹配。
|
||||||
|
/// </summary>
|
||||||
|
PropertyTypeMismatch,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 标量值为 null,但 schema 不允许。
|
||||||
|
/// </summary>
|
||||||
|
NullScalarValue,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 标量值不在 schema 声明的 enum 集合中。
|
||||||
|
/// </summary>
|
||||||
|
EnumValueNotAllowed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// YAML 可被读取,但无法成功反序列化到目标 CLR 类型。
|
||||||
|
/// </summary>
|
||||||
|
DeserializationFailed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已解析的配置项无法构造成运行时配置表。
|
||||||
|
/// </summary>
|
||||||
|
TableBuildFailed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跨表引用声明的目标表不可用。
|
||||||
|
/// </summary>
|
||||||
|
ReferencedTableNotFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跨表引用值无法转换到目标表主键类型。
|
||||||
|
/// </summary>
|
||||||
|
ReferenceKeyTypeMismatch,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 跨表引用值在目标表中不存在。
|
||||||
|
/// </summary>
|
||||||
|
ReferencedKeyNotFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兜底的未分类失败。
|
||||||
|
/// </summary>
|
||||||
|
UnexpectedFailure
|
||||||
|
}
|
||||||
@ -15,5 +15,9 @@ public interface IConfigLoader : IUtility
|
|||||||
/// <param name="registry">用于接收配置表的注册表。</param>
|
/// <param name="registry">用于接收配置表的注册表。</param>
|
||||||
/// <param name="cancellationToken">取消令牌。</param>
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
/// <returns>表示异步加载流程的任务。</returns>
|
/// <returns>表示异步加载流程的任务。</returns>
|
||||||
|
/// <exception cref="ConfigLoadException">
|
||||||
|
/// 当配置文件、schema、反序列化或跨表引用校验失败时抛出。
|
||||||
|
/// 调用方可以通过 <see cref="ConfigLoadException.Diagnostic" /> 读取稳定的结构化字段。
|
||||||
|
/// </exception>
|
||||||
Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default);
|
Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@ -9,6 +9,8 @@ namespace GFramework.Game.Tests.Config;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class YamlConfigLoaderTests
|
public class YamlConfigLoaderTests
|
||||||
{
|
{
|
||||||
|
private string _rootPath = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
|
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -31,8 +33,6 @@ public class YamlConfigLoaderTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _rootPath = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证加载器能够扫描 YAML 文件并将结果写入注册表。
|
/// 验证加载器能够扫描 YAML 文件并将结果写入注册表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,12 +80,16 @@ public class YamlConfigLoaderTests
|
|||||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<DirectoryNotFoundException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(exception, Is.Not.Null);
|
Assert.That(exception, Is.Not.Null);
|
||||||
Assert.That(exception!.Message, Does.Contain("monster"));
|
Assert.That(exception!.Message, Does.Contain("monster"));
|
||||||
|
Assert.That(exception.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConfigDirectoryNotFound));
|
||||||
|
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(exception.Diagnostic.ConfigDirectoryPath,
|
||||||
|
Is.EqualTo(Path.Combine(_rootPath, "monster")));
|
||||||
Assert.That(registry.Count, Is.EqualTo(0));
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -118,10 +122,14 @@ public class YamlConfigLoaderTests
|
|||||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id)
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id)
|
||||||
.RegisterTable<int, MonsterConfigStub>("broken", "broken", static config => config.Id);
|
.RegisterTable<int, MonsterConfigStub>("broken", "broken", static config => config.Id);
|
||||||
|
|
||||||
Assert.ThrowsAsync<DirectoryNotFoundException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Diagnostic.FailureKind,
|
||||||
|
Is.EqualTo(ConfigLoadFailureKind.ConfigDirectoryNotFound));
|
||||||
|
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("broken"));
|
||||||
Assert.That(registry.Count, Is.EqualTo(1));
|
Assert.That(registry.Count, Is.EqualTo(1));
|
||||||
Assert.That(registry.HasTable("monster"), Is.False);
|
Assert.That(registry.HasTable("monster"), Is.False);
|
||||||
Assert.That(registry.GetTable<int, ExistingConfigStub>("existing").Get(100).Name, Is.EqualTo("Original"));
|
Assert.That(registry.GetTable<int, ExistingConfigStub>("existing").Get(100).Name, Is.EqualTo("Original"));
|
||||||
@ -145,7 +153,7 @@ public class YamlConfigLoaderTests
|
|||||||
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -186,12 +194,19 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(exception, Is.Not.Null);
|
Assert.That(exception, Is.Not.Null);
|
||||||
Assert.That(exception!.Message, Does.Contain("name"));
|
Assert.That(exception!.Message, Does.Contain("name"));
|
||||||
|
Assert.That(exception.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.MissingRequiredProperty));
|
||||||
|
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(exception.Diagnostic.YamlPath,
|
||||||
|
Does.EndWith("monster/slime.yaml").Or.EndWith("monster\\slime.yaml"));
|
||||||
|
Assert.That(exception.Diagnostic.SchemaPath,
|
||||||
|
Does.EndWith("schemas/monster.schema.json").Or.EndWith("schemas\\monster.schema.json"));
|
||||||
|
Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("name"));
|
||||||
Assert.That(registry.Count, Is.EqualTo(0));
|
Assert.That(registry.Count, Is.EqualTo(0));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -228,7 +243,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -274,7 +289,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -317,7 +332,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -367,7 +382,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -417,7 +432,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -468,7 +483,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -524,7 +539,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -605,7 +620,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -736,7 +751,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -811,7 +826,7 @@ public class YamlConfigLoaderTests
|
|||||||
static config => config.Id);
|
static config => config.Id);
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loader.LoadAsync(registry));
|
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -945,11 +960,17 @@ public class YamlConfigLoaderTests
|
|||||||
""");
|
""");
|
||||||
|
|
||||||
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
||||||
|
var diagnosticException = failure.Exception as ConfigLoadException;
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(failure.TableName, Is.EqualTo("monster"));
|
Assert.That(failure.TableName, Is.EqualTo("monster"));
|
||||||
Assert.That(failure.Exception.Message, Does.Contain("rarity"));
|
Assert.That(failure.Exception.Message, Does.Contain("rarity"));
|
||||||
|
Assert.That(diagnosticException, Is.Not.Null);
|
||||||
|
Assert.That(diagnosticException!.Diagnostic.FailureKind,
|
||||||
|
Is.EqualTo(ConfigLoadFailureKind.MissingRequiredProperty));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.DisplayPath, Is.EqualTo("rarity"));
|
||||||
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(10));
|
Assert.That(registry.GetTable<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(10));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1034,11 +1055,19 @@ public class YamlConfigLoaderTests
|
|||||||
""");
|
""");
|
||||||
|
|
||||||
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5));
|
||||||
|
var diagnosticException = failure.Exception as ConfigLoadException;
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(failure.TableName, Is.EqualTo("item"));
|
Assert.That(failure.TableName, Is.EqualTo("item"));
|
||||||
Assert.That(failure.Exception.Message, Does.Contain("dropItemId"));
|
Assert.That(failure.Exception.Message, Does.Contain("dropItemId"));
|
||||||
|
Assert.That(diagnosticException, Is.Not.Null);
|
||||||
|
Assert.That(diagnosticException!.Diagnostic.FailureKind,
|
||||||
|
Is.EqualTo(ConfigLoadFailureKind.ReferencedKeyNotFound));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.ReferencedTableName, Is.EqualTo("item"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.DisplayPath, Is.EqualTo("dropItemId"));
|
||||||
|
Assert.That(diagnosticException.Diagnostic.RawValue, Is.EqualTo("potion"));
|
||||||
Assert.That(registry.GetTable<string, ItemConfigStub>("item").ContainsKey("potion"), Is.True);
|
Assert.That(registry.GetTable<string, ItemConfigStub>("item").ContainsKey("potion"), Is.True);
|
||||||
Assert.That(registry.GetTable<int, MonsterDropConfigStub>("monster").Get(1).DropItemId,
|
Assert.That(registry.GetTable<int, MonsterDropConfigStub>("monster").Get(1).DropItemId,
|
||||||
Is.EqualTo("potion"));
|
Is.EqualTo("potion"));
|
||||||
|
|||||||
51
GFramework.Game/Config/ConfigLoadExceptionFactory.cs
Normal file
51
GFramework.Game/Config/ConfigLoadExceptionFactory.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责在运行时配置系统内部构造结构化加载异常。
|
||||||
|
/// 该工厂集中封装诊断字段填充,避免不同失败路径对同一语义产生不一致的消息和字段约定。
|
||||||
|
/// </summary>
|
||||||
|
internal static class ConfigLoadExceptionFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个包含结构化诊断信息的配置加载异常。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failureKind">失败类别。</param>
|
||||||
|
/// <param name="tableName">配置表名称。</param>
|
||||||
|
/// <param name="message">错误消息。</param>
|
||||||
|
/// <param name="configDirectoryPath">配置目录绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="yamlPath">YAML 文件绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="schemaPath">schema 文件绝对路径;不适用时为空。</param>
|
||||||
|
/// <param name="displayPath">逻辑字段路径;不适用时为空。</param>
|
||||||
|
/// <param name="referencedTableName">跨表引用目标表名称;不适用时为空。</param>
|
||||||
|
/// <param name="rawValue">原始值或引用值;不适用时为空。</param>
|
||||||
|
/// <param name="detail">附加细节;不适用时为空。</param>
|
||||||
|
/// <param name="innerException">底层异常;不适用时为空。</param>
|
||||||
|
/// <returns>构造完成的配置加载异常。</returns>
|
||||||
|
internal static ConfigLoadException Create(
|
||||||
|
ConfigLoadFailureKind failureKind,
|
||||||
|
string tableName,
|
||||||
|
string message,
|
||||||
|
string? configDirectoryPath = null,
|
||||||
|
string? yamlPath = null,
|
||||||
|
string? schemaPath = null,
|
||||||
|
string? displayPath = null,
|
||||||
|
string? referencedTableName = null,
|
||||||
|
string? rawValue = null,
|
||||||
|
string? detail = null,
|
||||||
|
Exception? innerException = null)
|
||||||
|
{
|
||||||
|
var diagnostic = new ConfigLoadDiagnostic(
|
||||||
|
failureKind,
|
||||||
|
tableName,
|
||||||
|
configDirectoryPath,
|
||||||
|
yamlPath,
|
||||||
|
schemaPath,
|
||||||
|
displayPath,
|
||||||
|
referencedTableName,
|
||||||
|
rawValue,
|
||||||
|
detail);
|
||||||
|
return new ConfigLoadException(diagnostic, message, innerException);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Events;
|
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
|
||||||
|
|
||||||
namespace GFramework.Game.Config;
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
@ -87,7 +84,10 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="registry">要被热重载更新的配置注册表。</param>
|
/// <param name="registry">要被热重载更新的配置注册表。</param>
|
||||||
/// <param name="onTableReloaded">单个配置表重载成功后的可选回调。</param>
|
/// <param name="onTableReloaded">单个配置表重载成功后的可选回调。</param>
|
||||||
/// <param name="onTableReloadFailed">单个配置表重载失败后的可选回调。</param>
|
/// <param name="onTableReloadFailed">
|
||||||
|
/// 单个配置表重载失败后的可选回调。
|
||||||
|
/// 当失败来自加载器本身时,传入异常通常为 <see cref="ConfigLoadException" />,可从其诊断对象读取稳定字段。
|
||||||
|
/// </param>
|
||||||
/// <param name="debounceDelay">防抖延迟;为空时默认使用 200 毫秒。</param>
|
/// <param name="debounceDelay">防抖延迟;为空时默认使用 200 毫秒。</param>
|
||||||
/// <returns>用于停止热重载监听的注销句柄。</returns>
|
/// <returns>用于停止热重载监听的注销句柄。</returns>
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
|
||||||
@ -327,8 +327,11 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
var directoryPath = Path.Combine(rootPath, RelativePath);
|
var directoryPath = Path.Combine(rootPath, RelativePath);
|
||||||
if (!Directory.Exists(directoryPath))
|
if (!Directory.Exists(directoryPath))
|
||||||
{
|
{
|
||||||
throw new DirectoryNotFoundException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config directory '{directoryPath}' was not found for table '{Name}'.");
|
ConfigLoadFailureKind.ConfigDirectoryNotFound,
|
||||||
|
Name,
|
||||||
|
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
|
||||||
|
configDirectoryPath: directoryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
YamlConfigSchema? schema = null;
|
YamlConfigSchema? schema = null;
|
||||||
@ -336,7 +339,7 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
if (!string.IsNullOrEmpty(SchemaRelativePath))
|
if (!string.IsNullOrEmpty(SchemaRelativePath))
|
||||||
{
|
{
|
||||||
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
|
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
|
||||||
schema = await YamlConfigSchemaValidator.LoadAsync(schemaPath, cancellationToken);
|
schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken);
|
||||||
referencedTableNames = schema.ReferencedTableNames;
|
referencedTableNames = schema.ReferencedTableNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,18 +362,27 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
{
|
{
|
||||||
yaml = await File.ReadAllTextAsync(file, cancellationToken);
|
yaml = await File.ReadAllTextAsync(file, cancellationToken);
|
||||||
}
|
}
|
||||||
|
catch (ConfigLoadException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConfigFileReadFailed,
|
||||||
|
Name,
|
||||||
$"Failed to read config file '{file}' for table '{Name}'.",
|
$"Failed to read config file '{file}' for table '{Name}'.",
|
||||||
exception);
|
configDirectoryPath: directoryPath,
|
||||||
|
yamlPath: file,
|
||||||
|
schemaPath: schema?.SchemaPath,
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema != null)
|
if (schema != null)
|
||||||
{
|
{
|
||||||
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
|
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
|
||||||
referenceUsages.AddRange(
|
referenceUsages.AddRange(
|
||||||
YamlConfigSchemaValidator.ValidateAndCollectReferences(schema, file, yaml));
|
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -384,11 +396,21 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
|
|
||||||
values.Add(value);
|
values.Add(value);
|
||||||
}
|
}
|
||||||
|
catch (ConfigLoadException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.DeserializationFailed,
|
||||||
|
Name,
|
||||||
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
|
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
|
||||||
exception);
|
configDirectoryPath: directoryPath,
|
||||||
|
yamlPath: file,
|
||||||
|
schemaPath: schema?.SchemaPath,
|
||||||
|
detail: $"Target CLR type: {typeof(TValue).FullName}.",
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,11 +419,19 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
|
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
|
||||||
return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages);
|
return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages);
|
||||||
}
|
}
|
||||||
|
catch (ConfigLoadException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.TableBuildFailed,
|
||||||
|
Name,
|
||||||
$"Failed to build config table '{Name}' from directory '{directoryPath}'.",
|
$"Failed to build config table '{Name}' from directory '{directoryPath}'.",
|
||||||
exception);
|
configDirectoryPath: directoryPath,
|
||||||
|
schemaPath: schema?.SchemaPath,
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -482,21 +512,43 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
if (!TryResolveTargetTable(registry, loadedTableLookup, referenceUsage.ReferencedTableName,
|
if (!TryResolveTargetTable(registry, loadedTableLookup, referenceUsage.ReferencedTableName,
|
||||||
out var targetTable))
|
out var targetTable))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references table '{referenceUsage.ReferencedTableName}', but that table is not available in the current loader batch or registry.");
|
ConfigLoadFailureKind.ReferencedTableNotFound,
|
||||||
|
loadedTable.Name,
|
||||||
|
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references table '{referenceUsage.ReferencedTableName}', but that table is not available in the current loader batch or registry.",
|
||||||
|
yamlPath: referenceUsage.YamlPath,
|
||||||
|
schemaPath: referenceUsage.SchemaPath,
|
||||||
|
displayPath: referenceUsage.DisplayPath,
|
||||||
|
referencedTableName: referenceUsage.ReferencedTableName,
|
||||||
|
rawValue: referenceUsage.RawValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryConvertReferenceKey(referenceUsage, targetTable.KeyType, out var convertedKey,
|
if (!TryConvertReferenceKey(referenceUsage, targetTable.KeyType, out var convertedKey,
|
||||||
out var conversionError))
|
out var conversionError))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' cannot target table '{referenceUsage.ReferencedTableName}' with key type '{targetTable.KeyType.Name}'. {conversionError}");
|
ConfigLoadFailureKind.ReferenceKeyTypeMismatch,
|
||||||
|
loadedTable.Name,
|
||||||
|
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' cannot target table '{referenceUsage.ReferencedTableName}' with key type '{targetTable.KeyType.Name}'. {conversionError}",
|
||||||
|
yamlPath: referenceUsage.YamlPath,
|
||||||
|
schemaPath: referenceUsage.SchemaPath,
|
||||||
|
displayPath: referenceUsage.DisplayPath,
|
||||||
|
referencedTableName: referenceUsage.ReferencedTableName,
|
||||||
|
rawValue: referenceUsage.RawValue,
|
||||||
|
detail: conversionError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ContainsKey(targetTable, convertedKey!))
|
if (!ContainsKey(targetTable, convertedKey!))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references missing key '{referenceUsage.RawValue}' in table '{referenceUsage.ReferencedTableName}'.");
|
ConfigLoadFailureKind.ReferencedKeyNotFound,
|
||||||
|
loadedTable.Name,
|
||||||
|
$"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references missing key '{referenceUsage.RawValue}' in table '{referenceUsage.ReferencedTableName}'.",
|
||||||
|
yamlPath: referenceUsage.YamlPath,
|
||||||
|
schemaPath: referenceUsage.SchemaPath,
|
||||||
|
displayPath: referenceUsage.DisplayPath,
|
||||||
|
referencedTableName: referenceUsage.ReferencedTableName,
|
||||||
|
rawValue: referenceUsage.RawValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
namespace GFramework.Game.Config;
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -10,16 +12,23 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从磁盘加载并解析一个 JSON Schema 文件。
|
/// 从磁盘加载并解析一个 JSON Schema 文件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="cancellationToken">取消令牌。</param>
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
/// <returns>解析后的 schema 模型。</returns>
|
/// <returns>解析后的 schema 模型。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="tableName" /> 为空时抛出。</exception>
|
||||||
/// <exception cref="ArgumentException">当 <paramref name="schemaPath" /> 为空时抛出。</exception>
|
/// <exception cref="ArgumentException">当 <paramref name="schemaPath" /> 为空时抛出。</exception>
|
||||||
/// <exception cref="FileNotFoundException">当 schema 文件不存在时抛出。</exception>
|
/// <exception cref="ConfigLoadException">当 schema 文件不存在或内容非法时抛出。</exception>
|
||||||
/// <exception cref="InvalidOperationException">当 schema 内容不符合当前运行时支持的子集时抛出。</exception>
|
|
||||||
internal static async Task<YamlConfigSchema> LoadAsync(
|
internal static async Task<YamlConfigSchema> LoadAsync(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tableName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(schemaPath))
|
if (string.IsNullOrWhiteSpace(schemaPath))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
|
throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
|
||||||
@ -27,7 +36,11 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (!File.Exists(schemaPath))
|
if (!File.Exists(schemaPath))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException($"Schema file '{schemaPath}' was not found.", schemaPath);
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.SchemaFileNotFound,
|
||||||
|
tableName,
|
||||||
|
$"Schema file '{schemaPath}' was not found.",
|
||||||
|
schemaPath: schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
string schemaText;
|
string schemaText;
|
||||||
@ -37,18 +50,26 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Failed to read schema file '{schemaPath}'.", exception);
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.SchemaReadFailed,
|
||||||
|
tableName,
|
||||||
|
$"Failed to read schema file '{schemaPath}'.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(schemaText);
|
using var document = JsonDocument.Parse(schemaText);
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
var rootNode = ParseNode(schemaPath, "<root>", root, isRoot: true);
|
var rootNode = ParseNode(tableName, schemaPath, "<root>", root, isRoot: true);
|
||||||
if (rootNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
if (rootNode.NodeType != YamlConfigSchemaPropertyType.Object)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Schema file '{schemaPath}' must declare a root object schema.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Schema file '{schemaPath}' must declare a root object schema.",
|
||||||
|
schemaPath: schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var referencedTableNames = new HashSet<string>(StringComparer.Ordinal);
|
var referencedTableNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
@ -56,43 +77,61 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
return new YamlConfigSchema(schemaPath, rootNode, referencedTableNames.ToArray());
|
return new YamlConfigSchema(schemaPath, rootNode, referencedTableNames.ToArray());
|
||||||
}
|
}
|
||||||
|
catch (ConfigLoadException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (JsonException exception)
|
catch (JsonException exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Schema file '{schemaPath}' contains invalid JSON.", exception);
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.SchemaInvalidJson,
|
||||||
|
tableName,
|
||||||
|
$"Schema file '{schemaPath}' contains invalid JSON.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用已解析的 schema 校验 YAML 文本。
|
/// 使用已解析的 schema 校验 YAML 文本。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schema">已解析的 schema 模型。</param>
|
/// <param name="schema">已解析的 schema 模型。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径,仅用于诊断信息。</param>
|
/// <param name="yamlPath">YAML 文件路径,仅用于诊断信息。</param>
|
||||||
/// <param name="yamlText">YAML 文本内容。</param>
|
/// <param name="yamlText">YAML 文本内容。</param>
|
||||||
/// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
|
||||||
/// <exception cref="InvalidOperationException">当 YAML 内容与 schema 不匹配时抛出。</exception>
|
/// <exception cref="ConfigLoadException">当 YAML 内容与 schema 不匹配时抛出。</exception>
|
||||||
internal static void Validate(
|
internal static void Validate(
|
||||||
|
string tableName,
|
||||||
YamlConfigSchema schema,
|
YamlConfigSchema schema,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string yamlText)
|
string yamlText)
|
||||||
{
|
{
|
||||||
ValidateAndCollectReferences(schema, yamlPath, yamlText);
|
ValidateAndCollectReferences(tableName, schema, yamlPath, yamlText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用已解析的 schema 校验 YAML 文本,并提取声明过的跨表引用。
|
/// 使用已解析的 schema 校验 YAML 文本,并提取声明过的跨表引用。
|
||||||
/// 该方法让结构校验与引用采集共享同一份 YAML 解析结果,避免加载器重复解析同一文件。
|
/// 该方法让结构校验与引用采集共享同一份 YAML 解析结果,避免加载器重复解析同一文件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schema">已解析的 schema 模型。</param>
|
/// <param name="schema">已解析的 schema 模型。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径,仅用于诊断信息。</param>
|
/// <param name="yamlPath">YAML 文件路径,仅用于诊断信息。</param>
|
||||||
/// <param name="yamlText">YAML 文本内容。</param>
|
/// <param name="yamlText">YAML 文本内容。</param>
|
||||||
/// <returns>当前 YAML 文件中声明的跨表引用集合。</returns>
|
/// <returns>当前 YAML 文件中声明的跨表引用集合。</returns>
|
||||||
/// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
|
/// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
|
||||||
/// <exception cref="InvalidOperationException">当 YAML 内容与 schema 不匹配时抛出。</exception>
|
/// <exception cref="ConfigLoadException">当 YAML 内容与 schema 不匹配时抛出。</exception>
|
||||||
internal static IReadOnlyList<YamlConfigReferenceUsage> ValidateAndCollectReferences(
|
internal static IReadOnlyList<YamlConfigReferenceUsage> ValidateAndCollectReferences(
|
||||||
|
string tableName,
|
||||||
YamlConfigSchema schema,
|
YamlConfigSchema schema,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string yamlText)
|
string yamlText)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tableName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
|
||||||
|
}
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(schema);
|
ArgumentNullException.ThrowIfNull(schema);
|
||||||
ArgumentNullException.ThrowIfNull(yamlPath);
|
ArgumentNullException.ThrowIfNull(yamlPath);
|
||||||
ArgumentNullException.ThrowIfNull(yamlText);
|
ArgumentNullException.ThrowIfNull(yamlText);
|
||||||
@ -105,31 +144,41 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.YamlParseFailed,
|
||||||
|
tableName,
|
||||||
$"Config file '{yamlPath}' could not be parsed as YAML before schema validation.",
|
$"Config file '{yamlPath}' could not be parsed as YAML before schema validation.",
|
||||||
exception);
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schema.SchemaPath,
|
||||||
|
innerException: exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yamlStream.Documents.Count != 1)
|
if (yamlStream.Documents.Count != 1)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{yamlPath}' must contain exactly one YAML document.");
|
ConfigLoadFailureKind.InvalidYamlDocument,
|
||||||
|
tableName,
|
||||||
|
$"Config file '{yamlPath}' must contain exactly one YAML document.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schema.SchemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var references = new List<YamlConfigReferenceUsage>();
|
var references = new List<YamlConfigReferenceUsage>();
|
||||||
ValidateNode(yamlPath, string.Empty, yamlStream.Documents[0].RootNode, schema.RootNode, references);
|
ValidateNode(tableName, yamlPath, string.Empty, yamlStream.Documents[0].RootNode, schema.RootNode, references);
|
||||||
return references;
|
return references;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 递归解析 schema 节点,使运行时只保留校验真正需要的最小结构信息。
|
/// 递归解析 schema 节点,使运行时只保留校验真正需要的最小结构信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
|
/// <param name="propertyPath">当前节点的逻辑属性路径。</param>
|
||||||
/// <param name="element">Schema JSON 节点。</param>
|
/// <param name="element">Schema JSON 节点。</param>
|
||||||
/// <param name="isRoot">是否为根节点。</param>
|
/// <param name="isRoot">是否为根节点。</param>
|
||||||
/// <returns>可用于运行时校验的节点模型。</returns>
|
/// <returns>可用于运行时校验的节点模型。</returns>
|
||||||
private static YamlConfigSchemaNode ParseNode(
|
private static YamlConfigSchemaNode ParseNode(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
@ -138,54 +187,66 @@ internal static class YamlConfigSchemaValidator
|
|||||||
if (!element.TryGetProperty("type", out var typeElement) ||
|
if (!element.TryGetProperty("type", out var typeElement) ||
|
||||||
typeElement.ValueKind != JsonValueKind.String)
|
typeElement.ValueKind != JsonValueKind.String)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'type'.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'type'.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var typeName = typeElement.GetString() ?? string.Empty;
|
var typeName = typeElement.GetString() ?? string.Empty;
|
||||||
var referenceTableName = TryGetReferenceTableName(schemaPath, propertyPath, element);
|
var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
|
||||||
|
|
||||||
switch (typeName)
|
switch (typeName)
|
||||||
{
|
{
|
||||||
case "object":
|
case "object":
|
||||||
EnsureReferenceKeywordIsSupported(schemaPath, propertyPath, YamlConfigSchemaPropertyType.Object,
|
EnsureReferenceKeywordIsSupported(tableName, schemaPath, propertyPath,
|
||||||
|
YamlConfigSchemaPropertyType.Object,
|
||||||
referenceTableName);
|
referenceTableName);
|
||||||
return ParseObjectNode(schemaPath, propertyPath, element, isRoot);
|
return ParseObjectNode(tableName, schemaPath, propertyPath, element, isRoot);
|
||||||
|
|
||||||
case "array":
|
case "array":
|
||||||
return ParseArrayNode(schemaPath, propertyPath, element, referenceTableName);
|
return ParseArrayNode(tableName, schemaPath, propertyPath, element, referenceTableName);
|
||||||
|
|
||||||
case "integer":
|
case "integer":
|
||||||
return CreateScalarNode(schemaPath, propertyPath, YamlConfigSchemaPropertyType.Integer, element,
|
return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Integer,
|
||||||
referenceTableName);
|
element, referenceTableName);
|
||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
return CreateScalarNode(schemaPath, propertyPath, YamlConfigSchemaPropertyType.Number, element,
|
return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Number,
|
||||||
referenceTableName);
|
element, referenceTableName);
|
||||||
|
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return CreateScalarNode(schemaPath, propertyPath, YamlConfigSchemaPropertyType.Boolean, element,
|
return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.Boolean,
|
||||||
referenceTableName);
|
element, referenceTableName);
|
||||||
|
|
||||||
case "string":
|
case "string":
|
||||||
return CreateScalarNode(schemaPath, propertyPath, YamlConfigSchemaPropertyType.String, element,
|
return CreateScalarNode(tableName, schemaPath, propertyPath, YamlConfigSchemaPropertyType.String,
|
||||||
referenceTableName);
|
element, referenceTableName);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath),
|
||||||
|
rawValue: typeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析对象节点,保留属性字典与必填集合,以便后续递归校验时逐层定位错误。
|
/// 解析对象节点,保留属性字典与必填集合,以便后续递归校验时逐层定位错误。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">对象属性路径。</param>
|
/// <param name="propertyPath">对象属性路径。</param>
|
||||||
/// <param name="element">对象 schema 节点。</param>
|
/// <param name="element">对象 schema 节点。</param>
|
||||||
/// <param name="isRoot">是否为根节点。</param>
|
/// <param name="isRoot">是否为根节点。</param>
|
||||||
/// <returns>对象节点模型。</returns>
|
/// <returns>对象节点模型。</returns>
|
||||||
private static YamlConfigSchemaNode ParseObjectNode(
|
private static YamlConfigSchemaNode ParseObjectNode(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
@ -195,8 +256,12 @@ internal static class YamlConfigSchemaValidator
|
|||||||
propertiesElement.ValueKind != JsonValueKind.Object)
|
propertiesElement.ValueKind != JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
var subject = isRoot ? "root schema" : $"object property '{propertyPath}'";
|
var subject = isRoot ? "root schema" : $"object property '{propertyPath}'";
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"The {subject} in schema file '{schemaPath}' must declare an object-valued 'properties' section.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"The {subject} in schema file '{schemaPath}' must declare an object-valued 'properties' section.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var requiredProperties = new HashSet<string>(StringComparer.Ordinal);
|
var requiredProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||||
@ -222,6 +287,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
foreach (var property in propertiesElement.EnumerateObject())
|
foreach (var property in propertiesElement.EnumerateObject())
|
||||||
{
|
{
|
||||||
properties[property.Name] = ParseNode(
|
properties[property.Name] = ParseNode(
|
||||||
|
tableName,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
CombineSchemaPath(propertyPath, property.Name),
|
CombineSchemaPath(propertyPath, property.Name),
|
||||||
property.Value);
|
property.Value);
|
||||||
@ -242,12 +308,14 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// 当前子集支持标量数组和对象数组,不支持数组嵌套数组。
|
/// 当前子集支持标量数组和对象数组,不支持数组嵌套数组。
|
||||||
/// 当数组声明跨表引用时,会把引用语义挂到元素节点上,便于后续逐项校验。
|
/// 当数组声明跨表引用时,会把引用语义挂到元素节点上,便于后续逐项校验。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">数组属性路径。</param>
|
/// <param name="propertyPath">数组属性路径。</param>
|
||||||
/// <param name="element">数组 schema 节点。</param>
|
/// <param name="element">数组 schema 节点。</param>
|
||||||
/// <param name="referenceTableName">声明在数组节点上的目标引用表。</param>
|
/// <param name="referenceTableName">声明在数组节点上的目标引用表。</param>
|
||||||
/// <returns>数组节点模型。</returns>
|
/// <returns>数组节点模型。</returns>
|
||||||
private static YamlConfigSchemaNode ParseArrayNode(
|
private static YamlConfigSchemaNode ParseArrayNode(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
@ -256,18 +324,27 @@ internal static class YamlConfigSchemaValidator
|
|||||||
if (!element.TryGetProperty("items", out var itemsElement) ||
|
if (!element.TryGetProperty("items", out var itemsElement) ||
|
||||||
itemsElement.ValueKind != JsonValueKind.Object)
|
itemsElement.ValueKind != JsonValueKind.Object)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Array property '{propertyPath}' in schema file '{schemaPath}' must declare an object-valued 'items' schema.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Array property '{propertyPath}' in schema file '{schemaPath}' must declare an object-valued 'items' schema.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemNode = ParseNode(schemaPath, $"{propertyPath}[]", itemsElement);
|
var itemNode = ParseNode(tableName, schemaPath, $"{propertyPath}[]", itemsElement);
|
||||||
if (!string.IsNullOrWhiteSpace(referenceTableName))
|
if (!string.IsNullOrWhiteSpace(referenceTableName))
|
||||||
{
|
{
|
||||||
if (itemNode.NodeType != YamlConfigSchemaPropertyType.String &&
|
if (itemNode.NodeType != YamlConfigSchemaPropertyType.String &&
|
||||||
itemNode.NodeType != YamlConfigSchemaPropertyType.Integer)
|
itemNode.NodeType != YamlConfigSchemaPropertyType.Integer)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath),
|
||||||
|
referencedTableName: referenceTableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
itemNode = itemNode.WithReferenceTable(referenceTableName);
|
itemNode = itemNode.WithReferenceTable(referenceTableName);
|
||||||
@ -275,8 +352,12 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (itemNode.NodeType == YamlConfigSchemaPropertyType.Array)
|
if (itemNode.NodeType == YamlConfigSchemaPropertyType.Array)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Array property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array items.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Array property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array items.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new YamlConfigSchemaNode(
|
return new YamlConfigSchemaNode(
|
||||||
@ -292,6 +373,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建标量节点,并在解析阶段就完成 enum 与引用约束的兼容性检查。
|
/// 创建标量节点,并在解析阶段就完成 enum 与引用约束的兼容性检查。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">标量属性路径。</param>
|
/// <param name="propertyPath">标量属性路径。</param>
|
||||||
/// <param name="nodeType">标量类型。</param>
|
/// <param name="nodeType">标量类型。</param>
|
||||||
@ -299,20 +381,21 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="referenceTableName">目标引用表名称。</param>
|
/// <param name="referenceTableName">目标引用表名称。</param>
|
||||||
/// <returns>标量节点模型。</returns>
|
/// <returns>标量节点模型。</returns>
|
||||||
private static YamlConfigSchemaNode CreateScalarNode(
|
private static YamlConfigSchemaNode CreateScalarNode(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
YamlConfigSchemaPropertyType nodeType,
|
YamlConfigSchemaPropertyType nodeType,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
string? referenceTableName)
|
string? referenceTableName)
|
||||||
{
|
{
|
||||||
EnsureReferenceKeywordIsSupported(schemaPath, propertyPath, nodeType, referenceTableName);
|
EnsureReferenceKeywordIsSupported(tableName, schemaPath, propertyPath, nodeType, referenceTableName);
|
||||||
return new YamlConfigSchemaNode(
|
return new YamlConfigSchemaNode(
|
||||||
nodeType,
|
nodeType,
|
||||||
properties: null,
|
properties: null,
|
||||||
requiredProperties: null,
|
requiredProperties: null,
|
||||||
itemNode: null,
|
itemNode: null,
|
||||||
referenceTableName,
|
referenceTableName,
|
||||||
ParseEnumValues(schemaPath, propertyPath, element, nodeType, "enum"),
|
ParseEnumValues(tableName, schemaPath, propertyPath, element, nodeType, "enum"),
|
||||||
schemaPath);
|
schemaPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,12 +403,14 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// 递归校验 YAML 节点。
|
/// 递归校验 YAML 节点。
|
||||||
/// 每层都带上逻辑字段路径,这样深层对象与数组元素的错误也能直接定位。
|
/// 每层都带上逻辑字段路径,这样深层对象与数组元素的错误也能直接定位。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
/// <param name="displayPath">当前字段路径;根节点时为空。</param>
|
/// <param name="displayPath">当前字段路径;根节点时为空。</param>
|
||||||
/// <param name="node">实际 YAML 节点。</param>
|
/// <param name="node">实际 YAML 节点。</param>
|
||||||
/// <param name="schemaNode">对应的 schema 节点。</param>
|
/// <param name="schemaNode">对应的 schema 节点。</param>
|
||||||
/// <param name="references">已收集的跨表引用。</param>
|
/// <param name="references">已收集的跨表引用。</param>
|
||||||
private static void ValidateNode(
|
private static void ValidateNode(
|
||||||
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlNode node,
|
YamlNode node,
|
||||||
@ -335,35 +420,43 @@ internal static class YamlConfigSchemaValidator
|
|||||||
switch (schemaNode.NodeType)
|
switch (schemaNode.NodeType)
|
||||||
{
|
{
|
||||||
case YamlConfigSchemaPropertyType.Object:
|
case YamlConfigSchemaPropertyType.Object:
|
||||||
ValidateObjectNode(yamlPath, displayPath, node, schemaNode, references);
|
ValidateObjectNode(tableName, yamlPath, displayPath, node, schemaNode, references);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case YamlConfigSchemaPropertyType.Array:
|
case YamlConfigSchemaPropertyType.Array:
|
||||||
ValidateArrayNode(yamlPath, displayPath, node, schemaNode, references);
|
ValidateArrayNode(tableName, yamlPath, displayPath, node, schemaNode, references);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case YamlConfigSchemaPropertyType.Integer:
|
case YamlConfigSchemaPropertyType.Integer:
|
||||||
case YamlConfigSchemaPropertyType.Number:
|
case YamlConfigSchemaPropertyType.Number:
|
||||||
case YamlConfigSchemaPropertyType.Boolean:
|
case YamlConfigSchemaPropertyType.Boolean:
|
||||||
case YamlConfigSchemaPropertyType.String:
|
case YamlConfigSchemaPropertyType.String:
|
||||||
ValidateScalarNode(yamlPath, displayPath, node, schemaNode, references);
|
ValidateScalarNode(tableName, yamlPath, displayPath, node, schemaNode, references);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Schema node '{displayPath}' uses unsupported runtime node type '{schemaNode.NodeType}'.");
|
ConfigLoadFailureKind.UnexpectedFailure,
|
||||||
|
tableName,
|
||||||
|
$"Schema node '{displayPath}' uses unsupported runtime node type '{schemaNode.NodeType}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: schemaNode.NodeType.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验对象节点,同时处理重复字段、未知字段和深层必填字段。
|
/// 校验对象节点,同时处理重复字段、未知字段和深层必填字段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
/// <param name="displayPath">当前对象的逻辑字段路径。</param>
|
/// <param name="displayPath">当前对象的逻辑字段路径。</param>
|
||||||
/// <param name="node">实际 YAML 节点。</param>
|
/// <param name="node">实际 YAML 节点。</param>
|
||||||
/// <param name="schemaNode">对象 schema 节点。</param>
|
/// <param name="schemaNode">对象 schema 节点。</param>
|
||||||
/// <param name="references">已收集的跨表引用。</param>
|
/// <param name="references">已收集的跨表引用。</param>
|
||||||
private static void ValidateObjectNode(
|
private static void ValidateObjectNode(
|
||||||
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlNode node,
|
YamlNode node,
|
||||||
@ -373,8 +466,13 @@ internal static class YamlConfigSchemaValidator
|
|||||||
if (node is not YamlMappingNode mappingNode)
|
if (node is not YamlMappingNode mappingNode)
|
||||||
{
|
{
|
||||||
var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
|
var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"{subject} in config file '{yamlPath}' must be an object.");
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"{subject} in config file '{yamlPath}' must be an object.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
var seenProperties = new HashSet<string>(StringComparer.Ordinal);
|
||||||
@ -384,26 +482,41 @@ internal static class YamlConfigSchemaValidator
|
|||||||
string.IsNullOrWhiteSpace(keyNode.Value))
|
string.IsNullOrWhiteSpace(keyNode.Value))
|
||||||
{
|
{
|
||||||
var subject = displayPath.Length == 0 ? "root object" : $"object property '{displayPath}'";
|
var subject = displayPath.Length == 0 ? "root object" : $"object property '{displayPath}'";
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{yamlPath}' contains a non-scalar or empty property name inside {subject}.");
|
ConfigLoadFailureKind.InvalidYamlDocument,
|
||||||
|
tableName,
|
||||||
|
$"Config file '{yamlPath}' contains a non-scalar or empty property name inside {subject}.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var propertyName = keyNode.Value;
|
var propertyName = keyNode.Value;
|
||||||
var propertyPath = CombineDisplayPath(displayPath, propertyName);
|
var propertyPath = CombineDisplayPath(displayPath, propertyName);
|
||||||
if (!seenProperties.Add(propertyName))
|
if (!seenProperties.Add(propertyName))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{yamlPath}' contains duplicate property '{propertyPath}'.");
|
ConfigLoadFailureKind.DuplicateProperty,
|
||||||
|
tableName,
|
||||||
|
$"Config file '{yamlPath}' contains duplicate property '{propertyPath}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: propertyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.Properties is null ||
|
if (schemaNode.Properties is null ||
|
||||||
!schemaNode.Properties.TryGetValue(propertyName, out var propertySchema))
|
!schemaNode.Properties.TryGetValue(propertyName, out var propertySchema))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Config file '{yamlPath}' contains unknown property '{propertyPath}' that is not declared in schema '{schemaNode.SchemaPathHint}'.");
|
ConfigLoadFailureKind.UnknownProperty,
|
||||||
|
tableName,
|
||||||
|
$"Config file '{yamlPath}' contains unknown property '{propertyPath}' that is not declared in schema '{schemaNode.SchemaPathHint}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: propertyPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateNode(yamlPath, propertyPath, entry.Value, propertySchema, references);
|
ValidateNode(tableName, yamlPath, propertyPath, entry.Value, propertySchema, references);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.RequiredProperties is null)
|
if (schemaNode.RequiredProperties is null)
|
||||||
@ -418,20 +531,28 @@ internal static class YamlConfigSchemaValidator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
var requiredPath = CombineDisplayPath(displayPath, requiredProperty);
|
||||||
$"Config file '{yamlPath}' is missing required property '{CombineDisplayPath(displayPath, requiredProperty)}' defined by schema '{schemaNode.SchemaPathHint}'.");
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.MissingRequiredProperty,
|
||||||
|
tableName,
|
||||||
|
$"Config file '{yamlPath}' is missing required property '{requiredPath}' defined by schema '{schemaNode.SchemaPathHint}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: requiredPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验数组节点,并递归验证每个元素。
|
/// 校验数组节点,并递归验证每个元素。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
/// <param name="displayPath">数组字段路径。</param>
|
/// <param name="displayPath">数组字段路径。</param>
|
||||||
/// <param name="node">实际 YAML 节点。</param>
|
/// <param name="node">实际 YAML 节点。</param>
|
||||||
/// <param name="schemaNode">数组 schema 节点。</param>
|
/// <param name="schemaNode">数组 schema 节点。</param>
|
||||||
/// <param name="references">已收集的跨表引用。</param>
|
/// <param name="references">已收集的跨表引用。</param>
|
||||||
private static void ValidateArrayNode(
|
private static void ValidateArrayNode(
|
||||||
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlNode node,
|
YamlNode node,
|
||||||
@ -440,19 +561,30 @@ internal static class YamlConfigSchemaValidator
|
|||||||
{
|
{
|
||||||
if (node is not YamlSequenceNode sequenceNode)
|
if (node is not YamlSequenceNode sequenceNode)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be an array.");
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be an array.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.ItemNode is null)
|
if (schemaNode.ItemNode is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Schema node '{displayPath}' is missing array item information.");
|
ConfigLoadFailureKind.UnexpectedFailure,
|
||||||
|
tableName,
|
||||||
|
$"Schema node '{displayPath}' is missing array item information.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
|
||||||
{
|
{
|
||||||
ValidateNode(
|
ValidateNode(
|
||||||
|
tableName,
|
||||||
yamlPath,
|
yamlPath,
|
||||||
$"{displayPath}[{itemIndex}]",
|
$"{displayPath}[{itemIndex}]",
|
||||||
sequenceNode.Children[itemIndex],
|
sequenceNode.Children[itemIndex],
|
||||||
@ -464,12 +596,14 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验标量节点,并在值有效时收集跨表引用。
|
/// 校验标量节点,并在值有效时收集跨表引用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="yamlPath">YAML 文件路径。</param>
|
/// <param name="yamlPath">YAML 文件路径。</param>
|
||||||
/// <param name="displayPath">标量字段路径。</param>
|
/// <param name="displayPath">标量字段路径。</param>
|
||||||
/// <param name="node">实际 YAML 节点。</param>
|
/// <param name="node">实际 YAML 节点。</param>
|
||||||
/// <param name="schemaNode">标量 schema 节点。</param>
|
/// <param name="schemaNode">标量 schema 节点。</param>
|
||||||
/// <param name="references">已收集的跨表引用。</param>
|
/// <param name="references">已收集的跨表引用。</param>
|
||||||
private static void ValidateScalarNode(
|
private static void ValidateScalarNode(
|
||||||
|
string tableName,
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
string displayPath,
|
string displayPath,
|
||||||
YamlNode node,
|
YamlNode node,
|
||||||
@ -478,15 +612,25 @@ internal static class YamlConfigSchemaValidator
|
|||||||
{
|
{
|
||||||
if (node is not YamlScalarNode scalarNode)
|
if (node is not YamlScalarNode scalarNode)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.");
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = scalarNode.Value;
|
var value = scalarNode.Value;
|
||||||
if (value is null)
|
if (value is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.");
|
ConfigLoadFailureKind.NullScalarValue,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag = scalarNode.Tag.ToString();
|
var tag = scalarNode.Tag.ToString();
|
||||||
@ -509,16 +653,29 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.");
|
ConfigLoadFailureKind.PropertyTypeMismatch,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
|
||||||
if (schemaNode.AllowedValues is { Count: > 0 } &&
|
if (schemaNode.AllowedValues is { Count: > 0 } &&
|
||||||
!schemaNode.AllowedValues.Contains(normalizedValue, StringComparer.Ordinal))
|
!schemaNode.AllowedValues.Contains(normalizedValue, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{displayPath}' in config file '{yamlPath}' must be one of [{string.Join(", ", schemaNode.AllowedValues)}], but the current YAML scalar value is '{value}'.");
|
ConfigLoadFailureKind.EnumValueNotAllowed,
|
||||||
|
tableName,
|
||||||
|
$"Property '{displayPath}' in config file '{yamlPath}' must be one of [{string.Join(", ", schemaNode.AllowedValues)}], but the current YAML scalar value is '{value}'.",
|
||||||
|
yamlPath: yamlPath,
|
||||||
|
schemaPath: schemaNode.SchemaPathHint,
|
||||||
|
displayPath: GetDiagnosticPath(displayPath),
|
||||||
|
rawValue: value,
|
||||||
|
detail: $"Allowed values: {string.Join(", ", schemaNode.AllowedValues)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaNode.ReferenceTableName != null)
|
if (schemaNode.ReferenceTableName != null)
|
||||||
@ -526,6 +683,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
references.Add(
|
references.Add(
|
||||||
new YamlConfigReferenceUsage(
|
new YamlConfigReferenceUsage(
|
||||||
yamlPath,
|
yamlPath,
|
||||||
|
schemaNode.SchemaPathHint,
|
||||||
displayPath,
|
displayPath,
|
||||||
normalizedValue,
|
normalizedValue,
|
||||||
schemaNode.ReferenceTableName,
|
schemaNode.ReferenceTableName,
|
||||||
@ -536,6 +694,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析 enum,并在读取阶段验证枚举值与字段类型的兼容性。
|
/// 解析 enum,并在读取阶段验证枚举值与字段类型的兼容性。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">字段路径。</param>
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
/// <param name="element">Schema 节点。</param>
|
/// <param name="element">Schema 节点。</param>
|
||||||
@ -543,6 +702,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="keywordName">当前读取的关键字名称。</param>
|
/// <param name="keywordName">当前读取的关键字名称。</param>
|
||||||
/// <returns>归一化后的枚举值集合;未声明时返回空。</returns>
|
/// <returns>归一化后的枚举值集合;未声明时返回空。</returns>
|
||||||
private static IReadOnlyCollection<string>? ParseEnumValues(
|
private static IReadOnlyCollection<string>? ParseEnumValues(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element,
|
JsonElement element,
|
||||||
@ -556,14 +716,19 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (enumElement.ValueKind != JsonValueKind.Array)
|
if (enumElement.ValueKind != JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as an array.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as an array.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedValues = new List<string>();
|
var allowedValues = new List<string>();
|
||||||
foreach (var item in enumElement.EnumerateArray())
|
foreach (var item in enumElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
allowedValues.Add(NormalizeEnumValue(schemaPath, propertyPath, keywordName, expectedType, item));
|
allowedValues.Add(
|
||||||
|
NormalizeEnumValue(tableName, schemaPath, propertyPath, keywordName, expectedType, item));
|
||||||
}
|
}
|
||||||
|
|
||||||
return allowedValues;
|
return allowedValues;
|
||||||
@ -572,11 +737,13 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析跨表引用目标表名称。
|
/// 解析跨表引用目标表名称。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">字段路径。</param>
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
/// <param name="element">Schema 节点。</param>
|
/// <param name="element">Schema 节点。</param>
|
||||||
/// <returns>目标表名称;未声明时返回空。</returns>
|
/// <returns>目标表名称;未声明时返回空。</returns>
|
||||||
private static string? TryGetReferenceTableName(
|
private static string? TryGetReferenceTableName(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
JsonElement element)
|
JsonElement element)
|
||||||
@ -588,15 +755,23 @@ internal static class YamlConfigSchemaValidator
|
|||||||
|
|
||||||
if (referenceTableElement.ValueKind != JsonValueKind.String)
|
if (referenceTableElement.ValueKind != JsonValueKind.String)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'x-gframework-ref-table' value.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'x-gframework-ref-table' value.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var referenceTableName = referenceTableElement.GetString();
|
var referenceTableName = referenceTableElement.GetString();
|
||||||
if (string.IsNullOrWhiteSpace(referenceTableName))
|
if (string.IsNullOrWhiteSpace(referenceTableName))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a non-empty 'x-gframework-ref-table' value.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' must declare a non-empty 'x-gframework-ref-table' value.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
return referenceTableName;
|
return referenceTableName;
|
||||||
@ -605,11 +780,13 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证哪些 schema 类型允许声明跨表引用。
|
/// 验证哪些 schema 类型允许声明跨表引用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">字段路径。</param>
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
/// <param name="propertyType">字段类型。</param>
|
/// <param name="propertyType">字段类型。</param>
|
||||||
/// <param name="referenceTableName">目标表名称。</param>
|
/// <param name="referenceTableName">目标表名称。</param>
|
||||||
private static void EnsureReferenceKeywordIsSupported(
|
private static void EnsureReferenceKeywordIsSupported(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
YamlConfigSchemaPropertyType propertyType,
|
YamlConfigSchemaPropertyType propertyType,
|
||||||
@ -626,8 +803,13 @@ internal static class YamlConfigSchemaValidator
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath),
|
||||||
|
referencedTableName: referenceTableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -661,6 +843,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 schema 中的 enum 单值归一化到运行时比较字符串。
|
/// 将 schema 中的 enum 单值归一化到运行时比较字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="tableName">所属配置表名称。</param>
|
||||||
/// <param name="schemaPath">Schema 文件路径。</param>
|
/// <param name="schemaPath">Schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">字段路径。</param>
|
/// <param name="propertyPath">字段路径。</param>
|
||||||
/// <param name="keywordName">关键字名称。</param>
|
/// <param name="keywordName">关键字名称。</param>
|
||||||
@ -668,6 +851,7 @@ internal static class YamlConfigSchemaValidator
|
|||||||
/// <param name="item">当前枚举值节点。</param>
|
/// <param name="item">当前枚举值节点。</param>
|
||||||
/// <returns>归一化后的字符串值。</returns>
|
/// <returns>归一化后的字符串值。</returns>
|
||||||
private static string NormalizeEnumValue(
|
private static string NormalizeEnumValue(
|
||||||
|
string tableName,
|
||||||
string schemaPath,
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
string keywordName,
|
string keywordName,
|
||||||
@ -693,11 +877,27 @@ internal static class YamlConfigSchemaValidator
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
$"Property '{propertyPath}' in schema file '{schemaPath}' contains a '{keywordName}' value that is incompatible with schema type '{GetTypeName(expectedType)}'.");
|
ConfigLoadFailureKind.SchemaUnsupported,
|
||||||
|
tableName,
|
||||||
|
$"Property '{propertyPath}' in schema file '{schemaPath}' contains a '{keywordName}' value that is incompatible with schema type '{GetTypeName(expectedType)}'.",
|
||||||
|
schemaPath: schemaPath,
|
||||||
|
displayPath: GetDiagnosticPath(propertyPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将内部路径转换为适合放入诊断对象的可选字段路径。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">内部使用的属性路径。</param>
|
||||||
|
/// <returns>可用于诊断的路径;根节点时返回空。</returns>
|
||||||
|
private static string? GetDiagnosticPath(string path)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(path) || string.Equals(path, "<root>", StringComparison.Ordinal)
|
||||||
|
? null
|
||||||
|
: path;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 YAML 标量值规范化成运行时比较格式。
|
/// 将 YAML 标量值规范化成运行时比较格式。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -925,23 +1125,27 @@ internal sealed class YamlConfigReferenceUsage
|
|||||||
/// 初始化一个跨表引用使用记录。
|
/// 初始化一个跨表引用使用记录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="yamlPath">源 YAML 文件路径。</param>
|
/// <param name="yamlPath">源 YAML 文件路径。</param>
|
||||||
|
/// <param name="schemaPath">定义该引用的 schema 文件路径。</param>
|
||||||
/// <param name="propertyPath">声明引用的字段路径。</param>
|
/// <param name="propertyPath">声明引用的字段路径。</param>
|
||||||
/// <param name="rawValue">YAML 中的原始标量值。</param>
|
/// <param name="rawValue">YAML 中的原始标量值。</param>
|
||||||
/// <param name="referencedTableName">目标配置表名称。</param>
|
/// <param name="referencedTableName">目标配置表名称。</param>
|
||||||
/// <param name="valueType">引用值的 schema 标量类型。</param>
|
/// <param name="valueType">引用值的 schema 标量类型。</param>
|
||||||
public YamlConfigReferenceUsage(
|
public YamlConfigReferenceUsage(
|
||||||
string yamlPath,
|
string yamlPath,
|
||||||
|
string schemaPath,
|
||||||
string propertyPath,
|
string propertyPath,
|
||||||
string rawValue,
|
string rawValue,
|
||||||
string referencedTableName,
|
string referencedTableName,
|
||||||
YamlConfigSchemaPropertyType valueType)
|
YamlConfigSchemaPropertyType valueType)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(yamlPath);
|
ArgumentNullException.ThrowIfNull(yamlPath);
|
||||||
|
ArgumentNullException.ThrowIfNull(schemaPath);
|
||||||
ArgumentNullException.ThrowIfNull(propertyPath);
|
ArgumentNullException.ThrowIfNull(propertyPath);
|
||||||
ArgumentNullException.ThrowIfNull(rawValue);
|
ArgumentNullException.ThrowIfNull(rawValue);
|
||||||
ArgumentNullException.ThrowIfNull(referencedTableName);
|
ArgumentNullException.ThrowIfNull(referencedTableName);
|
||||||
|
|
||||||
YamlPath = yamlPath;
|
YamlPath = yamlPath;
|
||||||
|
SchemaPath = schemaPath;
|
||||||
PropertyPath = propertyPath;
|
PropertyPath = propertyPath;
|
||||||
RawValue = rawValue;
|
RawValue = rawValue;
|
||||||
ReferencedTableName = referencedTableName;
|
ReferencedTableName = referencedTableName;
|
||||||
@ -953,6 +1157,11 @@ internal sealed class YamlConfigReferenceUsage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string YamlPath { get; }
|
public string YamlPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取定义该引用的 schema 文件路径。
|
||||||
|
/// </summary>
|
||||||
|
public string SchemaPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取声明引用的字段路径。
|
/// 获取声明引用的字段路径。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -154,11 +154,41 @@ var slime = monsterTable.Get(1);
|
|||||||
|
|
||||||
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
这样可以避免错误配置被默认值或 `IgnoreUnmatchedProperties` 静默吞掉。
|
||||||
|
|
||||||
|
加载失败时,`YamlConfigLoader` 会抛出 `ConfigLoadException`。你可以通过 `exception.Diagnostic` 读取稳定字段,而不必解析消息文本:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await loader.LoadAsync(registry);
|
||||||
|
}
|
||||||
|
catch (ConfigLoadException exception)
|
||||||
|
{
|
||||||
|
Console.WriteLine(exception.Diagnostic.FailureKind);
|
||||||
|
Console.WriteLine(exception.Diagnostic.TableName);
|
||||||
|
Console.WriteLine(exception.Diagnostic.YamlPath);
|
||||||
|
Console.WriteLine(exception.Diagnostic.SchemaPath);
|
||||||
|
Console.WriteLine(exception.Diagnostic.DisplayPath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前诊断对象会优先暴露这些字段:
|
||||||
|
|
||||||
|
- `FailureKind`
|
||||||
|
- `TableName`
|
||||||
|
- `YamlPath`
|
||||||
|
- `SchemaPath`
|
||||||
|
- `DisplayPath`
|
||||||
|
- `ReferencedTableName`
|
||||||
|
- `RawValue`
|
||||||
|
|
||||||
## 开发期热重载
|
## 开发期热重载
|
||||||
|
|
||||||
如果你希望在开发期修改配置文件后自动刷新运行时表,可以在初次加载完成后启用热重载:
|
如果你希望在开发期修改配置文件后自动刷新运行时表,可以在初次加载完成后启用热重载:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
|
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
@ -175,7 +205,11 @@ var hotReload = loader.EnableHotReload(
|
|||||||
registry,
|
registry,
|
||||||
onTableReloaded: tableName => Console.WriteLine($"Reloaded: {tableName}"),
|
onTableReloaded: tableName => Console.WriteLine($"Reloaded: {tableName}"),
|
||||||
onTableReloadFailed: (tableName, exception) =>
|
onTableReloadFailed: (tableName, exception) =>
|
||||||
Console.WriteLine($"Reload failed: {tableName}, {exception.Message}"));
|
{
|
||||||
|
var diagnostic = (exception as ConfigLoadException)?.Diagnostic;
|
||||||
|
Console.WriteLine($"Reload failed: {tableName}, {exception.Message}");
|
||||||
|
Console.WriteLine($"Failure kind: {diagnostic?.FailureKind}");
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
当前热重载行为如下:
|
当前热重载行为如下:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user