,
+ * minProperties?: number,
+ * maxProperties?: number,
* title?: string,
* description?: string,
* defaultValue?: string
diff --git a/tools/gframework-config-tool/src/extension.js b/tools/gframework-config-tool/src/extension.js
index c78c8aa4..02519140 100644
--- a/tools/gframework-config-tool/src/extension.js
+++ b/tools/gframework-config-tool/src/extension.js
@@ -1095,6 +1095,7 @@ function renderFormField(field) {
${escapeHtml(field.displayPath || field.path)}
${renderYamlCommentBlock(field)}
${field.description ? `${escapeHtml(field.description)}` : ""}
+ ${field.schema ? renderFieldHint(field.schema, false, false) : ""}
${renderCommentEditor(field)}
`;
@@ -1302,6 +1303,7 @@ function collectFormFields(schemaNode, yamlNode, currentPath, depth, fields, uns
path: propertyPath,
label,
description: propertySchema.description,
+ schema: propertySchema,
comment: commentLookup[propertyPath] || "",
required: requiredSet.has(key),
depth
@@ -1468,6 +1470,7 @@ function collectObjectArrayItemFields(schemaNode, yamlNode, localPath, displayPa
displayPath: itemDisplayPath,
label,
description: propertySchema.description,
+ schema: propertySchema,
comment: commentLookup[itemDisplayPath] || "",
required: requiredSet.has(key),
depth
@@ -1574,14 +1577,15 @@ function getScalarArrayValue(yamlNode) {
/**
* Render human-facing metadata hints for one schema field.
*
- * @param {{description?: string, defaultValue?: string, minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, multipleOf?: number, minLength?: number, maxLength?: number, pattern?: string, minItems?: number, maxItems?: number, uniqueItems?: boolean, enumValues?: string[], items?: {enumValues?: string[], minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, multipleOf?: number, minLength?: number, maxLength?: number, pattern?: string}, refTable?: string}} propertySchema Property schema metadata.
+ * @param {{type?: string, description?: string, defaultValue?: string, minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, multipleOf?: number, minLength?: number, maxLength?: number, pattern?: string, minItems?: number, maxItems?: number, minProperties?: number, maxProperties?: number, uniqueItems?: boolean, enumValues?: string[], items?: {enumValues?: string[], minimum?: number, exclusiveMinimum?: number, maximum?: number, exclusiveMaximum?: number, multipleOf?: number, minLength?: number, maxLength?: number, pattern?: string}, refTable?: string}} propertySchema Property schema metadata.
* @param {boolean} isArrayField Whether the field is an array.
+ * @param {boolean} includeDescription Whether description text should be included in the hint output.
* @returns {string} HTML fragment.
*/
-function renderFieldHint(propertySchema, isArrayField) {
+function renderFieldHint(propertySchema, isArrayField, includeDescription = true) {
const hints = [];
- if (propertySchema.description) {
+ if (includeDescription && propertySchema.description) {
hints.push(escapeHtml(propertySchema.description));
}
@@ -1630,6 +1634,14 @@ function renderFieldHint(propertySchema, isArrayField) {
hints.push(escapeHtml(localizer.t("webview.hint.pattern", {value: propertySchema.pattern})));
}
+ if (propertySchema.type === "object" && typeof propertySchema.minProperties === "number") {
+ hints.push(escapeHtml(localizer.t("webview.hint.minProperties", {value: propertySchema.minProperties})));
+ }
+
+ if (propertySchema.type === "object" && typeof propertySchema.maxProperties === "number") {
+ hints.push(escapeHtml(localizer.t("webview.hint.maxProperties", {value: propertySchema.maxProperties})));
+ }
+
if (isArrayField && typeof propertySchema.minItems === "number") {
hints.push(escapeHtml(localizer.t("webview.hint.minItems", {value: propertySchema.minItems})));
}
diff --git a/tools/gframework-config-tool/src/localization.js b/tools/gframework-config-tool/src/localization.js
index f58bddf2..23aaf326 100644
--- a/tools/gframework-config-tool/src/localization.js
+++ b/tools/gframework-config-tool/src/localization.js
@@ -124,6 +124,8 @@ const enMessages = {
"webview.hint.itemMinLength": "Item min length: {value}",
"webview.hint.itemMaxLength": "Item max length: {value}",
"webview.hint.itemPattern": "Item pattern: {value}",
+ "webview.hint.minProperties": "Min properties: {value}",
+ "webview.hint.maxProperties": "Max properties: {value}",
"webview.hint.refTable": "Ref table: {refTable}",
"webview.unsupported.array": "Unsupported array shapes are currently raw-YAML-only in the form preview.",
"webview.unsupported.type": "{type} fields are currently raw-YAML-only.",
@@ -134,10 +136,12 @@ const enMessages = {
[ValidationMessageKeys.maximumViolation]: "Property '{displayPath}' must be less than or equal to {value}.",
[ValidationMessageKeys.maxItemsViolation]: "Property '{displayPath}' must contain at most {value} items.",
[ValidationMessageKeys.maxLengthViolation]: "Property '{displayPath}' must be at most {value} characters long.",
+ [ValidationMessageKeys.maxPropertiesViolation]: "Property '{displayPath}' must contain at most {value} properties.",
[ValidationMessageKeys.minimumViolation]: "Property '{displayPath}' must be greater than or equal to {value}.",
[ValidationMessageKeys.multipleOfViolation]: "Property '{displayPath}' must be a multiple of {value}.",
[ValidationMessageKeys.minItemsViolation]: "Property '{displayPath}' must contain at least {value} items.",
[ValidationMessageKeys.minLengthViolation]: "Property '{displayPath}' must be at least {value} characters long.",
+ [ValidationMessageKeys.minPropertiesViolation]: "Property '{displayPath}' must contain at least {value} properties.",
[ValidationMessageKeys.patternViolation]: "Property '{displayPath}' must match pattern '{value}'.",
[ValidationMessageKeys.uniqueItemsViolation]: "Property '{displayPath}' duplicates earlier array item '{duplicatePath}', but uniqueItems is required.",
[ValidationMessageKeys.enumMismatch]: "Property '{displayPath}' must be one of: {values}.",
@@ -228,6 +232,8 @@ const zhCnMessages = {
"webview.hint.itemMinLength": "元素最小长度:{value}",
"webview.hint.itemMaxLength": "元素最大长度:{value}",
"webview.hint.itemPattern": "元素正则模式:{value}",
+ "webview.hint.minProperties": "最少属性数:{value}",
+ "webview.hint.maxProperties": "最多属性数:{value}",
"webview.hint.refTable": "引用表:{refTable}",
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
@@ -238,10 +244,12 @@ const zhCnMessages = {
[ValidationMessageKeys.maximumViolation]: "属性“{displayPath}”必须小于或等于 {value}。",
[ValidationMessageKeys.maxItemsViolation]: "属性“{displayPath}”最多只能包含 {value} 个元素。",
[ValidationMessageKeys.maxLengthViolation]: "属性“{displayPath}”长度必须不超过 {value} 个字符。",
+ [ValidationMessageKeys.maxPropertiesViolation]: "对象属性“{displayPath}”最多只能包含 {value} 个子属性。",
[ValidationMessageKeys.minimumViolation]: "属性“{displayPath}”必须大于或等于 {value}。",
[ValidationMessageKeys.multipleOfViolation]: "属性“{displayPath}”必须是 {value} 的整数倍。",
[ValidationMessageKeys.minItemsViolation]: "属性“{displayPath}”至少需要包含 {value} 个元素。",
[ValidationMessageKeys.minLengthViolation]: "属性“{displayPath}”长度必须至少为 {value} 个字符。",
+ [ValidationMessageKeys.minPropertiesViolation]: "对象属性“{displayPath}”至少需要包含 {value} 个子属性。",
[ValidationMessageKeys.patternViolation]: "属性“{displayPath}”必须匹配正则模式“{value}”。",
[ValidationMessageKeys.uniqueItemsViolation]: "属性“{displayPath}”与更早的数组元素“{duplicatePath}”重复;该数组要求元素唯一。",
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
diff --git a/tools/gframework-config-tool/src/localizationKeys.js b/tools/gframework-config-tool/src/localizationKeys.js
index 3e315ff9..622743ce 100644
--- a/tools/gframework-config-tool/src/localizationKeys.js
+++ b/tools/gframework-config-tool/src/localizationKeys.js
@@ -9,10 +9,12 @@ const ValidationMessageKeys = Object.freeze({
maximumViolation: "validation.maximumViolation",
maxItemsViolation: "validation.maxItemsViolation",
maxLengthViolation: "validation.maxLengthViolation",
+ maxPropertiesViolation: "validation.maxPropertiesViolation",
minimumViolation: "validation.minimumViolation",
multipleOfViolation: "validation.multipleOfViolation",
minItemsViolation: "validation.minItemsViolation",
minLengthViolation: "validation.minLengthViolation",
+ minPropertiesViolation: "validation.minPropertiesViolation",
missingRequired: "validation.missingRequired",
patternViolation: "validation.patternViolation",
uniqueItemsViolation: "validation.uniqueItemsViolation",
diff --git a/tools/gframework-config-tool/test/configValidation.test.js b/tools/gframework-config-tool/test/configValidation.test.js
index 3b793650..e8b1518e 100644
--- a/tools/gframework-config-tool/test/configValidation.test.js
+++ b/tools/gframework-config-tool/test/configValidation.test.js
@@ -308,6 +308,41 @@ tags:
assert.match(diagnostics[1].message, /at most 3 items|最多只能包含 3 个元素/u);
});
+test("validateParsedConfig should report object property-count mismatches", () => {
+ const schema = parseSchemaContent(`
+ {
+ "type": "object",
+ "minProperties": 2,
+ "maxProperties": 3,
+ "properties": {
+ "reward": {
+ "type": "object",
+ "minProperties": 2,
+ "maxProperties": 2,
+ "properties": {
+ "gold": { "type": "integer" },
+ "currency": { "type": "string" },
+ "tier": { "type": "string" }
+ }
+ }
+ }
+ }
+ `);
+ const yaml = parseTopLevelYaml(`
+reward:
+ gold: 10
+ currency: coin
+ tier: epic
+`);
+
+ const diagnostics = validateParsedConfig(schema, yaml);
+ const messages = diagnostics.map((diagnostic) => diagnostic.message);
+
+ assert.equal(diagnostics.length, 2);
+ assert.ok(messages.some((message) => /at least 2 properties|至少需要包含 2 个属性/u.test(message)));
+ assert.ok(messages.some((message) => /reward.*at most 2 properties|reward.*最多只能包含 2 个子属性/u.test(message)));
+});
+
test("validateParsedConfig should report multipleOf and uniqueItems violations", () => {
const schema = parseSchemaContent(`
{
@@ -615,6 +650,31 @@ test("parseSchemaContent should capture multipleOf and uniqueItems metadata", ()
assert.equal(schema.properties.dropRates.items.multipleOf, 0.5);
});
+test("parseSchemaContent should capture object property-count metadata", () => {
+ const schema = parseSchemaContent(`
+ {
+ "type": "object",
+ "minProperties": 2,
+ "maxProperties": 4,
+ "properties": {
+ "reward": {
+ "type": "object",
+ "minProperties": 1,
+ "maxProperties": 2,
+ "properties": {
+ "gold": { "type": "integer" }
+ }
+ }
+ }
+ }
+ `);
+
+ assert.equal(schema.minProperties, 2);
+ assert.equal(schema.maxProperties, 4);
+ assert.equal(schema.properties.reward.minProperties, 1);
+ assert.equal(schema.properties.reward.maxProperties, 2);
+});
+
test("parseSchemaContent should reject invalid pattern declarations instead of dropping them", () => {
assert.throws(
() => parseSchemaContent(`