From 6549eff278a1425d4cce3e795ee0c4b82e74c95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Thu, 8 Jun 2023 14:06:22 +0200 Subject: [PATCH] feat(samples): add support for contains, minContains, maxContains keywords (#8896) This change is specific to JSON Schema 2020-12 and OpenAPI 3.1.0. Refs #8577 --- .../samples-extensions/fn.js | 177 +++++++++++++----- .../samples-extensions/fn.js | 93 +++++++++ 2 files changed, 224 insertions(+), 46 deletions(-) diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn.js index 4c39610dded..aeacefcc0da 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn.js @@ -74,8 +74,25 @@ const isURI = (uri) => { const applyArrayConstraints = (array, constraints = {}) => { const { minItems, maxItems, uniqueItems } = constraints + const { contains, minContains, maxContains } = constraints let constrainedArray = [...array] + if (contains != null && typeof contains === "object") { + if (Number.isInteger(minContains) && minContains > 1) { + const containsItem = constrainedArray.at(0) + for (let i = 1; i < minContains; i += 1) { + constrainedArray.unshift(containsItem) + } + } + if (Number.isInteger(maxContains) && maxContains > 0) { + /** + * This is noop. `minContains` already generate minimum required + * number of items that satisfies `contains`. `maxContains` would + * have no effect. + */ + } + } + if (Number.isInteger(maxItems) && maxItems > 0) { constrainedArray = array.slice(0, maxItems) } @@ -84,13 +101,14 @@ const applyArrayConstraints = (array, constraints = {}) => { constrainedArray.push(constrainedArray[i % constrainedArray.length]) } } - /** - * If uniqueItems is true, it implies that every item in the array must be unique. - * This overrides any minItems constraint that cannot be satisfied with unique items. - * So if minItems is greater than the number of unique items, - * it should be reduced to the number of unique items. - */ + if (uniqueItems === true) { + /** + * If uniqueItems is true, it implies that every item in the array must be unique. + * This overrides any minItems constraint that cannot be satisfied with unique items. + * So if minItems is greater than the number of unique items, + * it should be reduced to the number of unique items. + */ constrainedArray = Array.from(new Set(constrainedArray)) } @@ -105,7 +123,13 @@ const sanitizeRef = (value) => deeplyStripKey(value, "$$ref", (val) => typeof val === "string" && isURI(val)) const objectContracts = ["maxProperties", "minProperties"] -const arrayConstraints = ["minItems", "maxItems", "uniqueItems"] +const arrayConstraints = [ + "minItems", + "maxItems", + "uniqueItems", + "minContains", + "maxContains", +] const numberConstraints = [ "minimum", "maximum", @@ -266,8 +290,15 @@ export const sampleFromSchemaGeneric = ( } } const _attr = {} - let { xml, type, example, properties, additionalProperties, items } = - schema || {} + let { + xml, + type, + example, + properties, + additionalProperties, + items, + contains, + } = schema || {} let { includeReadOnly, includeWriteOnly } = config xml = xml || {} let { name, prefix, namespace } = xml @@ -296,7 +327,7 @@ export const sampleFromSchemaGeneric = ( if (schema && typeof type !== "string" && !Array.isArray(type)) { if (properties || additionalProperties || schemaHasAny(objectContracts)) { type = "object" - } else if (items || schemaHasAny(arrayConstraints)) { + } else if (items || contains || schemaHasAny(arrayConstraints)) { type = "array" } else if (schemaHasAny(numberConstraints)) { type = "number" @@ -509,14 +540,26 @@ export const sampleFromSchemaGeneric = ( } sample = [sample] } - const itemSchema = schema ? schema.items : undefined - if (itemSchema) { - itemSchema.xml = itemSchema.xml || xml || {} - itemSchema.xml.name = itemSchema.xml.name || xml.name + + let itemSamples = [] + + if (items != null && typeof items === "object") { + items.xml = items.xml || xml || {} + items.xml.name = items.xml.name || xml.name + itemSamples = sample.map((s) => + sampleFromSchemaGeneric(items, config, s, respectXML) + ) + } + + if (contains != null && typeof contains === "object") { + contains.xml = contains.xml || xml || {} + contains.xml.name = contains.xml.name || xml.name + itemSamples = [ + sampleFromSchemaGeneric(contains, config, undefined, respectXML), + ...itemSamples, + ] } - let itemSamples = sample.map((s) => - sampleFromSchemaGeneric(itemSchema, config, s, respectXML) - ) + itemSamples = applyArrayConstraints(itemSamples, schema) if (xml.wrapped) { res[displayName] = itemSamples @@ -579,41 +622,82 @@ export const sampleFromSchemaGeneric = ( // use schema to generate sample if (type?.includes("array")) { - if (!items) { - return [] - } + let sampleArray = [] - let sampleArray - if (respectXML) { - items.xml = items.xml || schema?.xml || {} - items.xml.name = items.xml.name || xml.name + if (contains != null && typeof contains === "object") { + if (respectXML) { + contains.xml = contains.xml || schema?.xml || {} + contains.xml.name = contains.xml.name || xml.name + } + + if (Array.isArray(contains.anyOf)) { + sampleArray.push( + ...contains.anyOf.map((i) => + sampleFromSchemaGeneric( + liftSampleHelper(contains, i, config), + config, + undefined, + respectXML + ) + ) + ) + } else if (Array.isArray(contains.oneOf)) { + sampleArray.push( + ...contains.oneOf.map((i) => + sampleFromSchemaGeneric( + liftSampleHelper(contains, i, config), + config, + undefined, + respectXML + ) + ) + ) + } else if (!respectXML || (respectXML && xml.wrapped)) { + sampleArray.push( + sampleFromSchemaGeneric(contains, config, undefined, respectXML) + ) + } else { + return sampleFromSchemaGeneric(contains, config, undefined, respectXML) + } } - if (Array.isArray(items.anyOf)) { - sampleArray = items.anyOf.map((i) => - sampleFromSchemaGeneric( - liftSampleHelper(items, i, config), - config, - undefined, - respectXML + if (items != null && typeof items === "object") { + if (respectXML) { + items.xml = items.xml || schema?.xml || {} + items.xml.name = items.xml.name || xml.name + } + + if (Array.isArray(items.anyOf)) { + sampleArray.push( + ...items.anyOf.map((i) => + sampleFromSchemaGeneric( + liftSampleHelper(items, i, config), + config, + undefined, + respectXML + ) + ) ) - ) - } else if (Array.isArray(items.oneOf)) { - sampleArray = items.oneOf.map((i) => - sampleFromSchemaGeneric( - liftSampleHelper(items, i, config), - config, - undefined, - respectXML + } else if (Array.isArray(items.oneOf)) { + sampleArray.push( + ...items.oneOf.map((i) => + sampleFromSchemaGeneric( + liftSampleHelper(items, i, config), + config, + undefined, + respectXML + ) + ) ) - ) - } else if (!respectXML || (respectXML && xml.wrapped)) { - sampleArray = [ - sampleFromSchemaGeneric(items, config, undefined, respectXML), - ] - } else { - return sampleFromSchemaGeneric(items, config, undefined, respectXML) + } else if (!respectXML || (respectXML && xml.wrapped)) { + sampleArray.push( + sampleFromSchemaGeneric(items, config, undefined, respectXML) + ) + } else { + return sampleFromSchemaGeneric(items, config, undefined, respectXML) + } } + sampleArray = applyArrayConstraints(sampleArray, schema) if (respectXML && xml.wrapped) { res[displayName] = sampleArray @@ -622,6 +706,7 @@ export const sampleFromSchemaGeneric = ( } return res } + return sampleArray } diff --git a/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js b/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js index 4f48759cf64..8b194058a50 100644 --- a/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js +++ b/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js @@ -1321,6 +1321,99 @@ describe("sampleFromSchema", () => { expect(sampleFromSchema(definition)).toEqual(expected) }) + it("should handle contains", () => { + const definition = { + type: "array", + contains: { + type: "number", + }, + } + + const expected = [0] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + + it("should handle contains with items", () => { + const definition = { + type: "array", + items: { + type: "string", + }, + contains: { + type: "number", + }, + } + + const expected = [0, "string"] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + + it("should handle minContains", () => { + const definition = { + type: "array", + minContains: 3, + contains: { + type: "number", + }, + } + + const expected = [0, 0, 0] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + + it("should handle minContains with minItems", () => { + const definition = { + type: "array", + minContains: 3, + minItems: 4, + contains: { + type: "number", + }, + items: { + type: "string", + }, + } + + const expected = [0, 0, 0, "string"] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + + it("should handle maxContains", () => { + const definition = { + type: "array", + maxContains: 3, + contains: { + type: "number", + }, + } + + const expected = [0] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + + it("should handle maxContains with maxItems", () => { + const definition = { + type: "array", + maxContains: 10, + maxItem: 10, + contains: { + type: "number", + }, + items: { + type: "string", + }, + } + + const expected = [0, "string"] + + expect(sampleFromSchema(definition)).toEqual(expected) + }) + it("should handle minimum", () => { const definition = { type: "number",