feat(config): 添加YAML配置文件的JSON Schema校验功能

- 实现了YAML配置文件与JSON Schema的运行时校验能力
- 支持嵌套对象、对象数组、标量数组的递归校验
- 提供跨表引用的约束检查与引用采集功能
- 支持enum枚举值与数值范围约束验证
- 实现详细的错误诊断信息与字段路径定位
- 包含完整的异常处理与错误报告机制
This commit is contained in:
GeWuYou 2026-04-03 17:08:24 +08:00
parent 0e538738df
commit b4e026a70d
3 changed files with 97 additions and 19 deletions

View File

@ -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.")
};
}

View File

@ -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, {

View File

@ -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(`
{