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
This commit is contained in:
Vladimír Gorej
2023-06-08 14:06:22 +02:00
committed by GitHub
parent 1114965782
commit 6549eff278
2 changed files with 226 additions and 48 deletions

View File

@@ -74,8 +74,25 @@ const isURI = (uri) => {
const applyArrayConstraints = (array, constraints = {}) => { const applyArrayConstraints = (array, constraints = {}) => {
const { minItems, maxItems, uniqueItems } = constraints const { minItems, maxItems, uniqueItems } = constraints
const { contains, minContains, maxContains } = constraints
let constrainedArray = [...array] 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) { if (Number.isInteger(maxItems) && maxItems > 0) {
constrainedArray = array.slice(0, maxItems) constrainedArray = array.slice(0, maxItems)
} }
@@ -84,13 +101,14 @@ const applyArrayConstraints = (array, constraints = {}) => {
constrainedArray.push(constrainedArray[i % constrainedArray.length]) constrainedArray.push(constrainedArray[i % constrainedArray.length])
} }
} }
if (uniqueItems === true) {
/** /**
* If uniqueItems is true, it implies that every item in the array must be unique. * 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. * This overrides any minItems constraint that cannot be satisfied with unique items.
* So if minItems is greater than the number of unique items, * So if minItems is greater than the number of unique items,
* it should be reduced to the number of unique items. * it should be reduced to the number of unique items.
*/ */
if (uniqueItems === true) {
constrainedArray = Array.from(new Set(constrainedArray)) constrainedArray = Array.from(new Set(constrainedArray))
} }
@@ -105,7 +123,13 @@ const sanitizeRef = (value) =>
deeplyStripKey(value, "$$ref", (val) => typeof val === "string" && isURI(val)) deeplyStripKey(value, "$$ref", (val) => typeof val === "string" && isURI(val))
const objectContracts = ["maxProperties", "minProperties"] const objectContracts = ["maxProperties", "minProperties"]
const arrayConstraints = ["minItems", "maxItems", "uniqueItems"] const arrayConstraints = [
"minItems",
"maxItems",
"uniqueItems",
"minContains",
"maxContains",
]
const numberConstraints = [ const numberConstraints = [
"minimum", "minimum",
"maximum", "maximum",
@@ -266,8 +290,15 @@ export const sampleFromSchemaGeneric = (
} }
} }
const _attr = {} const _attr = {}
let { xml, type, example, properties, additionalProperties, items } = let {
schema || {} xml,
type,
example,
properties,
additionalProperties,
items,
contains,
} = schema || {}
let { includeReadOnly, includeWriteOnly } = config let { includeReadOnly, includeWriteOnly } = config
xml = xml || {} xml = xml || {}
let { name, prefix, namespace } = xml let { name, prefix, namespace } = xml
@@ -296,7 +327,7 @@ export const sampleFromSchemaGeneric = (
if (schema && typeof type !== "string" && !Array.isArray(type)) { if (schema && typeof type !== "string" && !Array.isArray(type)) {
if (properties || additionalProperties || schemaHasAny(objectContracts)) { if (properties || additionalProperties || schemaHasAny(objectContracts)) {
type = "object" type = "object"
} else if (items || schemaHasAny(arrayConstraints)) { } else if (items || contains || schemaHasAny(arrayConstraints)) {
type = "array" type = "array"
} else if (schemaHasAny(numberConstraints)) { } else if (schemaHasAny(numberConstraints)) {
type = "number" type = "number"
@@ -509,14 +540,26 @@ export const sampleFromSchemaGeneric = (
} }
sample = [sample] sample = [sample]
} }
const itemSchema = schema ? schema.items : undefined
if (itemSchema) { let itemSamples = []
itemSchema.xml = itemSchema.xml || xml || {}
itemSchema.xml.name = itemSchema.xml.name || xml.name if (items != null && typeof items === "object") {
} items.xml = items.xml || xml || {}
let itemSamples = sample.map((s) => items.xml.name = items.xml.name || xml.name
sampleFromSchemaGeneric(itemSchema, config, s, respectXML) 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,
]
}
itemSamples = applyArrayConstraints(itemSamples, schema) itemSamples = applyArrayConstraints(itemSamples, schema)
if (xml.wrapped) { if (xml.wrapped) {
res[displayName] = itemSamples res[displayName] = itemSamples
@@ -579,18 +622,54 @@ export const sampleFromSchemaGeneric = (
// use schema to generate sample // use schema to generate sample
if (type?.includes("array")) { if (type?.includes("array")) {
if (!items) { let sampleArray = []
return []
if (contains != null && typeof contains === "object") {
if (respectXML) {
contains.xml = contains.xml || schema?.xml || {}
contains.xml.name = contains.xml.name || xml.name
} }
let sampleArray 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 (items != null && typeof items === "object") {
if (respectXML) { if (respectXML) {
items.xml = items.xml || schema?.xml || {} items.xml = items.xml || schema?.xml || {}
items.xml.name = items.xml.name || xml.name items.xml.name = items.xml.name || xml.name
} }
if (Array.isArray(items.anyOf)) { if (Array.isArray(items.anyOf)) {
sampleArray = items.anyOf.map((i) => sampleArray.push(
...items.anyOf.map((i) =>
sampleFromSchemaGeneric( sampleFromSchemaGeneric(
liftSampleHelper(items, i, config), liftSampleHelper(items, i, config),
config, config,
@@ -598,8 +677,10 @@ export const sampleFromSchemaGeneric = (
respectXML respectXML
) )
) )
)
} else if (Array.isArray(items.oneOf)) { } else if (Array.isArray(items.oneOf)) {
sampleArray = items.oneOf.map((i) => sampleArray.push(
...items.oneOf.map((i) =>
sampleFromSchemaGeneric( sampleFromSchemaGeneric(
liftSampleHelper(items, i, config), liftSampleHelper(items, i, config),
config, config,
@@ -607,13 +688,16 @@ export const sampleFromSchemaGeneric = (
respectXML respectXML
) )
) )
)
} else if (!respectXML || (respectXML && xml.wrapped)) { } else if (!respectXML || (respectXML && xml.wrapped)) {
sampleArray = [ sampleArray.push(
sampleFromSchemaGeneric(items, config, undefined, respectXML), sampleFromSchemaGeneric(items, config, undefined, respectXML)
] )
} else { } else {
return sampleFromSchemaGeneric(items, config, undefined, respectXML) return sampleFromSchemaGeneric(items, config, undefined, respectXML)
} }
}
sampleArray = applyArrayConstraints(sampleArray, schema) sampleArray = applyArrayConstraints(sampleArray, schema)
if (respectXML && xml.wrapped) { if (respectXML && xml.wrapped) {
res[displayName] = sampleArray res[displayName] = sampleArray
@@ -622,6 +706,7 @@ export const sampleFromSchemaGeneric = (
} }
return res return res
} }
return sampleArray return sampleArray
} }

View File

@@ -1321,6 +1321,99 @@ describe("sampleFromSchema", () => {
expect(sampleFromSchema(definition)).toEqual(expected) 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", () => { it("should handle minimum", () => {
const definition = { const definition = {
type: "number", type: "number",