feat(samples): add support for proper schema merging (#8910)
This change is specific to JSON Schema 2020-12 and OpenAPI 3.1.0. Refs #8577
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import { isBooleanJSONSchema, isJSONSchema } from "./predicates"
|
||||||
|
|
||||||
|
const merge = (target, source, config = {}) => {
|
||||||
|
if (isBooleanJSONSchema(target) && target === true) return true
|
||||||
|
if (isBooleanJSONSchema(target) && target === false) return false
|
||||||
|
if (isBooleanJSONSchema(source) && source === true) return true
|
||||||
|
if (isBooleanJSONSchema(source) && source === false) return false
|
||||||
|
|
||||||
|
if (!isJSONSchema(target)) return source
|
||||||
|
if (!isJSONSchema(source)) return target
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merging properties from the source object into the target object
|
||||||
|
* only if they do not already exist in the target object.
|
||||||
|
*/
|
||||||
|
const merged = { ...source, ...target }
|
||||||
|
|
||||||
|
// merging required keyword
|
||||||
|
if (Array.isArray(source.required) && Array.isArray(target.required)) {
|
||||||
|
merged.required = [...new Set([...target.required, ...source.required])]
|
||||||
|
}
|
||||||
|
|
||||||
|
// merging properties keyword
|
||||||
|
if (source.properties && target.properties) {
|
||||||
|
const allPropertyNames = new Set([
|
||||||
|
...Object.keys(source.properties),
|
||||||
|
...Object.keys(target.properties),
|
||||||
|
])
|
||||||
|
|
||||||
|
merged.properties = {}
|
||||||
|
for (const name of allPropertyNames) {
|
||||||
|
const sourceProperty = source.properties[name] || {}
|
||||||
|
const targetProperty = target.properties[name] || {}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(sourceProperty.readOnly && !config.includeReadOnly) ||
|
||||||
|
(sourceProperty.writeOnly && !config.includeWriteOnly)
|
||||||
|
) {
|
||||||
|
merged.required = (merged.required || []).filter((p) => p !== name)
|
||||||
|
} else {
|
||||||
|
merged.properties[name] = merge(targetProperty, sourceProperty, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merging items keyword
|
||||||
|
if (isJSONSchema(source.items) && isJSONSchema(target.items)) {
|
||||||
|
merged.items = merge(target.items, source.items, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merging contains keyword
|
||||||
|
if (isJSONSchema(source.contains) && isJSONSchema(target.contains)) {
|
||||||
|
merged.contains = merge(target.contains, source.contains, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merging contentSchema keyword
|
||||||
|
if (
|
||||||
|
isJSONSchema(source.contentSchema) &&
|
||||||
|
isJSONSchema(target.contentSchema)
|
||||||
|
) {
|
||||||
|
merged.contentSchema = merge(
|
||||||
|
target.contentSchema,
|
||||||
|
source.contentSchema,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
export default merge
|
||||||
@@ -11,113 +11,8 @@ import { getType } from "./core/type"
|
|||||||
import { typeCast } from "./core/utils"
|
import { typeCast } from "./core/utils"
|
||||||
import { hasExample, extractExample } from "./core/example"
|
import { hasExample, extractExample } from "./core/example"
|
||||||
import { pick as randomPick } from "./core/random"
|
import { pick as randomPick } from "./core/random"
|
||||||
|
import merge from "./core/merge"
|
||||||
const objectConstraints = ["maxProperties", "minProperties", "required"]
|
import { isBooleanJSONSchema, isJSONSchemaObject } from "./core/predicates"
|
||||||
const arrayConstraints = [
|
|
||||||
"minItems",
|
|
||||||
"maxItems",
|
|
||||||
"uniqueItems",
|
|
||||||
"minContains",
|
|
||||||
"maxContains",
|
|
||||||
]
|
|
||||||
const numberConstraints = [
|
|
||||||
"minimum",
|
|
||||||
"maximum",
|
|
||||||
"exclusiveMinimum",
|
|
||||||
"exclusiveMaximum",
|
|
||||||
"multipleOf",
|
|
||||||
]
|
|
||||||
const stringConstraints = [
|
|
||||||
"minLength",
|
|
||||||
"maxLength",
|
|
||||||
"pattern",
|
|
||||||
"contentEncoding",
|
|
||||||
"contentMediaType",
|
|
||||||
]
|
|
||||||
|
|
||||||
const liftSampleHelper = (oldSchema, target, config = {}) => {
|
|
||||||
const setIfNotDefinedInTarget = (key) => {
|
|
||||||
if (target[key] === undefined && oldSchema[key] !== undefined) {
|
|
||||||
target[key] = oldSchema[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
;[
|
|
||||||
"examples",
|
|
||||||
"example",
|
|
||||||
"default",
|
|
||||||
"enum",
|
|
||||||
"xml",
|
|
||||||
"type",
|
|
||||||
"const",
|
|
||||||
...objectConstraints,
|
|
||||||
...arrayConstraints,
|
|
||||||
...numberConstraints,
|
|
||||||
...stringConstraints,
|
|
||||||
].forEach((key) => setIfNotDefinedInTarget(key))
|
|
||||||
|
|
||||||
if (oldSchema.required !== undefined && Array.isArray(oldSchema.required)) {
|
|
||||||
if (target.required === undefined || !target.required.length) {
|
|
||||||
target.required = []
|
|
||||||
}
|
|
||||||
oldSchema.required.forEach((key) => {
|
|
||||||
if (target.required.includes(key)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
target.required.push(key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (oldSchema.properties) {
|
|
||||||
if (!target.properties) {
|
|
||||||
target.properties = {}
|
|
||||||
}
|
|
||||||
let props = objectify(oldSchema.properties)
|
|
||||||
for (let propName in props) {
|
|
||||||
if (!Object.hasOwn(props, propName)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (props[propName] && props[propName].deprecated) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
props[propName] &&
|
|
||||||
props[propName].readOnly &&
|
|
||||||
!config.includeReadOnly
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
props[propName] &&
|
|
||||||
props[propName].writeOnly &&
|
|
||||||
!config.includeWriteOnly
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!target.properties[propName]) {
|
|
||||||
target.properties[propName] = props[propName]
|
|
||||||
if (
|
|
||||||
!oldSchema.required &&
|
|
||||||
Array.isArray(oldSchema.required) &&
|
|
||||||
oldSchema.required.indexOf(propName) !== -1
|
|
||||||
) {
|
|
||||||
if (!target.required) {
|
|
||||||
target.required = [propName]
|
|
||||||
} else {
|
|
||||||
target.required.push(propName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldSchema.items) {
|
|
||||||
if (!target.items) {
|
|
||||||
target.items = {}
|
|
||||||
}
|
|
||||||
target.items = liftSampleHelper(oldSchema.items, target.items, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sampleFromSchemaGeneric = (
|
export const sampleFromSchemaGeneric = (
|
||||||
schema,
|
schema,
|
||||||
@@ -138,7 +33,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
const schemaToAdd = typeCast(
|
const schemaToAdd = typeCast(
|
||||||
hasOneOf ? randomPick(schema.oneOf) : randomPick(schema.anyOf)
|
hasOneOf ? randomPick(schema.oneOf) : randomPick(schema.anyOf)
|
||||||
)
|
)
|
||||||
liftSampleHelper(schemaToAdd, schema, config)
|
schema = merge(schema, schemaToAdd, config)
|
||||||
if (!schema.xml && schemaToAdd.xml) {
|
if (!schema.xml && schemaToAdd.xml) {
|
||||||
schema.xml = schemaToAdd.xml
|
schema.xml = schemaToAdd.xml
|
||||||
}
|
}
|
||||||
@@ -489,9 +384,9 @@ export const sampleFromSchemaGeneric = (
|
|||||||
|
|
||||||
if (Array.isArray(contains.anyOf)) {
|
if (Array.isArray(contains.anyOf)) {
|
||||||
sampleArray.push(
|
sampleArray.push(
|
||||||
...contains.anyOf.map((i) =>
|
...contains.anyOf.map((anyOfSchema) =>
|
||||||
sampleFromSchemaGeneric(
|
sampleFromSchemaGeneric(
|
||||||
liftSampleHelper(contains, i, config),
|
merge(anyOfSchema, contains, config),
|
||||||
config,
|
config,
|
||||||
undefined,
|
undefined,
|
||||||
respectXML
|
respectXML
|
||||||
@@ -500,9 +395,9 @@ export const sampleFromSchemaGeneric = (
|
|||||||
)
|
)
|
||||||
} else if (Array.isArray(contains.oneOf)) {
|
} else if (Array.isArray(contains.oneOf)) {
|
||||||
sampleArray.push(
|
sampleArray.push(
|
||||||
...contains.oneOf.map((i) =>
|
...contains.oneOf.map((oneOfSchema) =>
|
||||||
sampleFromSchemaGeneric(
|
sampleFromSchemaGeneric(
|
||||||
liftSampleHelper(contains, i, config),
|
merge(oneOfSchema, contains, config),
|
||||||
config,
|
config,
|
||||||
undefined,
|
undefined,
|
||||||
respectXML
|
respectXML
|
||||||
@@ -528,7 +423,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
sampleArray.push(
|
sampleArray.push(
|
||||||
...items.anyOf.map((i) =>
|
...items.anyOf.map((i) =>
|
||||||
sampleFromSchemaGeneric(
|
sampleFromSchemaGeneric(
|
||||||
liftSampleHelper(items, i, config),
|
merge(i, items, config),
|
||||||
config,
|
config,
|
||||||
undefined,
|
undefined,
|
||||||
respectXML
|
respectXML
|
||||||
@@ -539,7 +434,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
sampleArray.push(
|
sampleArray.push(
|
||||||
...items.oneOf.map((i) =>
|
...items.oneOf.map((i) =>
|
||||||
sampleFromSchemaGeneric(
|
sampleFromSchemaGeneric(
|
||||||
liftSampleHelper(items, i, config),
|
merge(i, items, config),
|
||||||
config,
|
config,
|
||||||
undefined,
|
undefined,
|
||||||
respectXML
|
respectXML
|
||||||
@@ -591,14 +486,14 @@ export const sampleFromSchemaGeneric = (
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalProperties === true) {
|
if (isBooleanJSONSchema(additionalProperties)) {
|
||||||
if (respectXML) {
|
if (respectXML) {
|
||||||
res[displayName].push({ additionalProp: "Anything can be here" })
|
res[displayName].push({ additionalProp: "Anything can be here" })
|
||||||
} else {
|
} else {
|
||||||
res.additionalProp1 = {}
|
res.additionalProp1 = {}
|
||||||
}
|
}
|
||||||
propertyAddedCounter++
|
propertyAddedCounter++
|
||||||
} else if (additionalProperties) {
|
} else if (isJSONSchemaObject(additionalProperties)) {
|
||||||
const additionalProps = typeCast(additionalProperties)
|
const additionalProps = typeCast(additionalProperties)
|
||||||
const additionalPropSample = sampleFromSchemaGeneric(
|
const additionalPropSample = sampleFromSchemaGeneric(
|
||||||
additionalProps,
|
additionalProps,
|
||||||
|
|||||||
@@ -1154,7 +1154,7 @@ describe("sampleFromSchema", () => {
|
|||||||
expect(sampleFromSchema(definition)).toEqual(expected)
|
expect(sampleFromSchema(definition)).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should lift items with anyOf", () => {
|
it("should merge items with anyOf", () => {
|
||||||
const definition = {
|
const definition = {
|
||||||
type: "array",
|
type: "array",
|
||||||
anyOf: [
|
anyOf: [
|
||||||
@@ -1172,7 +1172,7 @@ describe("sampleFromSchema", () => {
|
|||||||
expect(sampleFromSchema(definition)).toEqual(expected)
|
expect(sampleFromSchema(definition)).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should lift items with oneOf", () => {
|
it("should merge items with oneOf", () => {
|
||||||
const definition = {
|
const definition = {
|
||||||
type: "array",
|
type: "array",
|
||||||
oneOf: [
|
oneOf: [
|
||||||
|
|||||||
Reference in New Issue
Block a user