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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user