mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(config): 添加YAML配置文件的JSON Schema校验功能
- 实现了YAML配置文件与JSON Schema的运行时校验能力 - 支持嵌套对象、对象数组、标量数组的递归校验 - 提供跨表引用的约束检查与引用采集功能 - 支持enum枚举值与数值范围约束验证 - 实现详细的错误诊断信息与字段路径定位 - 包含完整的异常处理与错误报告机制
This commit is contained in:
parent
0e538738df
commit
b4e026a70d
@ -913,7 +913,21 @@ internal static class YamlConfigSchemaValidator
|
||||
{
|
||||
case YamlConfigSchemaPropertyType.Integer:
|
||||
case YamlConfigSchemaPropertyType.Number:
|
||||
var numericValue = double.Parse(normalizedValue, CultureInfo.InvariantCulture);
|
||||
if (!double.TryParse(
|
||||
normalizedValue,
|
||||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var numericValue))
|
||||
{
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.UnexpectedFailure,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' could not be normalized into a comparable numeric value.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: rawValue);
|
||||
}
|
||||
|
||||
if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value)
|
||||
{
|
||||
@ -975,6 +989,16 @@ internal static class YamlConfigSchemaValidator
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
throw ConfigLoadExceptionFactory.Create(
|
||||
ConfigLoadFailureKind.UnexpectedFailure,
|
||||
tableName,
|
||||
$"Property '{displayPath}' in config file '{yamlPath}' resolved unsupported constraint host type '{schemaNode.NodeType}'.",
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaNode.SchemaPathHint,
|
||||
displayPath: GetDiagnosticPath(displayPath),
|
||||
rawValue: schemaNode.NodeType.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1153,16 +1177,29 @@ internal static class YamlConfigSchemaValidator
|
||||
return expectedType switch
|
||||
{
|
||||
YamlConfigSchemaPropertyType.String => value,
|
||||
YamlConfigSchemaPropertyType.Integer => long.Parse(
|
||||
value,
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture),
|
||||
YamlConfigSchemaPropertyType.Number => double.Parse(
|
||||
value,
|
||||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture),
|
||||
YamlConfigSchemaPropertyType.Boolean => bool.Parse(value).ToString().ToLowerInvariant(),
|
||||
_ => value
|
||||
YamlConfigSchemaPropertyType.Integer when long.TryParse(
|
||||
value,
|
||||
NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var integerValue) =>
|
||||
integerValue.ToString(CultureInfo.InvariantCulture),
|
||||
YamlConfigSchemaPropertyType.Number when double.TryParse(
|
||||
value,
|
||||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var numberValue) =>
|
||||
numberValue.ToString(CultureInfo.InvariantCulture),
|
||||
YamlConfigSchemaPropertyType.Boolean when bool.TryParse(value, out var booleanValue) =>
|
||||
booleanValue.ToString().ToLowerInvariant(),
|
||||
YamlConfigSchemaPropertyType.Integer =>
|
||||
throw new InvalidOperationException($"Value '{value}' cannot be normalized as integer."),
|
||||
YamlConfigSchemaPropertyType.Number =>
|
||||
throw new InvalidOperationException($"Value '{value}' cannot be normalized as number."),
|
||||
YamlConfigSchemaPropertyType.Boolean =>
|
||||
throw new InvalidOperationException($"Value '{value}' cannot be normalized as boolean."),
|
||||
_ =>
|
||||
throw new InvalidOperationException(
|
||||
$"Schema node type '{expectedType}' cannot be normalized as a scalar value.")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -505,10 +505,18 @@ function parseSchemaNode(rawNode, displayPath) {
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
defaultValue: metadata.defaultValue,
|
||||
minimum: metadata.minimum,
|
||||
maximum: metadata.maximum,
|
||||
minLength: metadata.minLength,
|
||||
maxLength: metadata.maxLength,
|
||||
minimum: type === "integer" || type === "number"
|
||||
? metadata.minimum
|
||||
: undefined,
|
||||
maximum: type === "integer" || type === "number"
|
||||
? metadata.maximum
|
||||
: undefined,
|
||||
minLength: type === "string"
|
||||
? metadata.minLength
|
||||
: undefined,
|
||||
maxLength: type === "string"
|
||||
? metadata.maxLength
|
||||
: undefined,
|
||||
enumValues: normalizeSchemaEnumValues(value.enum),
|
||||
refTable: metadata.refTable
|
||||
};
|
||||
@ -587,7 +595,12 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
}
|
||||
|
||||
const scalarValue = unquoteScalar(yamlNode.value);
|
||||
if (typeof schemaNode.minimum === "number" && Number(scalarValue) < schemaNode.minimum) {
|
||||
const supportsNumericConstraints = schemaNode.type === "integer" || schemaNode.type === "number";
|
||||
const supportsLengthConstraints = schemaNode.type === "string";
|
||||
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.minimum === "number" &&
|
||||
Number(scalarValue) < schemaNode.minimum) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.minimumViolation, localizer, {
|
||||
@ -597,7 +610,9 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof schemaNode.maximum === "number" && Number(scalarValue) > schemaNode.maximum) {
|
||||
if (supportsNumericConstraints &&
|
||||
typeof schemaNode.maximum === "number" &&
|
||||
Number(scalarValue) > schemaNode.maximum) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.maximumViolation, localizer, {
|
||||
@ -607,7 +622,9 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof schemaNode.minLength === "number" && scalarValue.length < schemaNode.minLength) {
|
||||
if (supportsLengthConstraints &&
|
||||
typeof schemaNode.minLength === "number" &&
|
||||
scalarValue.length < schemaNode.minLength) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.minLengthViolation, localizer, {
|
||||
@ -617,7 +634,9 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof schemaNode.maxLength === "number" && scalarValue.length > schemaNode.maxLength) {
|
||||
if (supportsLengthConstraints &&
|
||||
typeof schemaNode.maxLength === "number" &&
|
||||
scalarValue.length > schemaNode.maxLength) {
|
||||
diagnostics.push({
|
||||
severity: "error",
|
||||
message: localizeValidationMessage(ValidationMessageKeys.maxLengthViolation, localizer, {
|
||||
|
||||
@ -266,6 +266,28 @@ test("parseSchemaContent should capture scalar range and length metadata", () =>
|
||||
assert.equal(schema.properties.tags.items.maxLength, 6);
|
||||
});
|
||||
|
||||
test("parseSchemaContent should ignore mismatched constraint metadata on unsupported scalar types", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"minimum": 1,
|
||||
"minLength": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
const yaml = parseTopLevelYaml(`
|
||||
enabled: true
|
||||
`);
|
||||
|
||||
assert.equal(schema.properties.enabled.minimum, undefined);
|
||||
assert.equal(schema.properties.enabled.minLength, undefined);
|
||||
assert.deepEqual(validateParsedConfig(schema, yaml), []);
|
||||
});
|
||||
|
||||
test("validateParsedConfig should localize diagnostics when Chinese UI is requested", () => {
|
||||
const schema = parseSchemaContent(`
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user