feat(samples): add support for contentSchema keyword (#8907)

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-09 21:35:44 +02:00
committed by GitHub
parent d72b72c5c6
commit 6c622a87e7
9 changed files with 174 additions and 35 deletions

View File

@@ -0,0 +1,6 @@
/**
* @prettier
*/
export const SCALAR_TYPES = ["integer", "number", "string", "boolean", "null"]
export const ALL_TYPES = ["array", "object", ...SCALAR_TYPES]

View File

@@ -0,0 +1,24 @@
/**
* @prettier
*/
import { ALL_TYPES } from "./constants"
const foldType = (type) => {
if (Array.isArray(type)) {
if (type.includes("array")) {
return "array"
} else if (type.includes("object")) {
return "object"
} else if (ALL_TYPES.includes(type.at(0))) {
return type.at(0)
}
}
if (typeof type === "string" && ALL_TYPES.includes(type)) {
return type
}
return null
}
export default foldType

View File

@@ -0,0 +1,24 @@
/**
* @prettier
*/
import isPlainObject from "lodash/isPlainObject"
export const isURI = (uri) => {
try {
return new URL(uri) && true
} catch {
return false
}
}
export const isBooleanJSONSchema = (schema) => {
return typeof schema === "boolean"
}
export const isJSONSchemaObject = (schema) => {
return isPlainObject(schema)
}
export const isJSONSchema = (schema) => {
return isBooleanJSONSchema(schema) || isJSONSchemaObject(schema)
}

View File

@@ -1,10 +1,23 @@
/**
* @prettier
*/
export const isURI = (uri) => {
try {
return new URL(uri) && true
} catch {
return false
import { isBooleanJSONSchema, isJSONSchemaObject } from "./predicates"
export const fromJSONBooleanSchema = (schema) => {
if (schema === false) {
return { not: {} }
}
return {}
}
export const typeCast = (schema) => {
if (isBooleanJSONSchema(schema)) {
return fromJSONBooleanSchema(schema)
}
if (!isJSONSchemaObject(schema)) {
return {}
}
return schema
}

View File

@@ -4,22 +4,12 @@
import XML from "xml"
import isEmpty from "lodash/isEmpty"
import { objectify, isFunc, normalizeArray, deeplyStripKey } from "core/utils"
import { objectify, normalizeArray, deeplyStripKey } from "core/utils"
import memoizeN from "../../../../../helpers/memoizeN"
import typeMap from "./types/index"
import { isURI } from "./core/utils"
const primitive = (schema) => {
schema = objectify(schema)
const { type: typeList } = schema
const type = Array.isArray(typeList) ? typeList.at(0) : typeList
if (Object.hasOwn(typeMap, type)) {
return typeMap[type](schema)
}
return `Unknown Type: ${type}`
}
import { isURI } from "./core/predicates"
import foldType from "./core/fold-type"
import { typeCast } from "./core/utils"
/**
* Do a couple of quick sanity tests to ensure the value
@@ -140,7 +130,9 @@ export const sampleFromSchemaGeneric = (
exampleOverride = undefined,
respectXML = false
) => {
if (schema && isFunc(schema.toJS)) schema = schema.toJS()
if (typeof schema?.toJS === "function") schema = schema.toJS()
schema = typeCast(schema)
let usePlainValue =
exampleOverride !== undefined ||
(schema && schema.example !== undefined) ||
@@ -151,7 +143,7 @@ export const sampleFromSchemaGeneric = (
const hasAnyOf =
!usePlainValue && schema && schema.anyOf && schema.anyOf.length > 0
if (!usePlainValue && (hasOneOf || hasAnyOf)) {
const schemaToAdd = objectify(hasOneOf ? schema.oneOf[0] : schema.anyOf[0])
const schemaToAdd = typeCast(hasOneOf ? schema.oneOf[0] : schema.anyOf[0])
liftSampleHelper(schemaToAdd, schema, config)
if (!schema.xml && schemaToAdd.xml) {
schema.xml = schemaToAdd.xml
@@ -211,6 +203,7 @@ export const sampleFromSchemaGeneric = (
items,
contains,
} = schema || {}
type = foldType(type)
let { includeReadOnly, includeWriteOnly } = config
xml = xml || {}
let { name, prefix, namespace } = xml
@@ -339,9 +332,9 @@ export const sampleFromSchemaGeneric = (
} else if (enumAttrVal !== undefined) {
_attr[props[propName].xml.name || propName] = enumAttrVal
} else {
_attr[props[propName].xml.name || propName] = primitive(
props[propName]
)
const propSchema = typeCast(props[propName])
const attrName = props[propName].xml.name || propName
_attr[attrName] = typeMap[propSchema.type](propSchema)
}
return
@@ -468,7 +461,7 @@ export const sampleFromSchemaGeneric = (
]
}
itemSamples = typeMap.array(schema, itemSamples)
itemSamples = typeMap.array(schema, { sample: itemSamples })
if (xml.wrapped) {
res[displayName] = itemSamples
if (!isEmpty(_attr)) {
@@ -606,7 +599,7 @@ export const sampleFromSchemaGeneric = (
}
}
sampleArray = typeMap.array(schema, sampleArray)
sampleArray = typeMap.array(schema, { sample: sampleArray })
if (respectXML && xml.wrapped) {
res[displayName] = sampleArray
if (!isEmpty(_attr)) {
@@ -650,7 +643,7 @@ export const sampleFromSchemaGeneric = (
}
propertyAddedCounter++
} else if (additionalProperties) {
const additionalProps = objectify(additionalProperties)
const additionalProps = typeCast(additionalProperties)
const additionalPropSample = sampleFromSchemaGeneric(
additionalProps,
config,
@@ -699,7 +692,15 @@ export const sampleFromSchemaGeneric = (
value = normalizeArray(schema.enum)[0]
} else if (schema) {
// display schema default
value = primitive(schema)
const contentSample = Object.hasOwn(schema, "contentSchema")
? sampleFromSchemaGeneric(
typeCast(schema.contentSchema),
config,
undefined,
respectXML
)
: undefined
value = typeMap[type](schema, { sample: contentSample })
} else {
return
}

View File

@@ -45,8 +45,8 @@ export const applyArrayConstraints = (array, constraints = {}) => {
return constrainedArray
}
const arrayType = (schema, sampleArray) => {
return applyArrayConstraints(sampleArray, schema)
const arrayType = (schema, { sample }) => {
return applyArrayConstraints(sample, schema)
}
export default arrayType

View File

@@ -19,4 +19,12 @@ const typeMap = {
null: nullType,
}
export default typeMap
export default new Proxy(typeMap, {
get(target, prop) {
if (Object.hasOwn(target, prop)) {
return target[prop]
}
return () => `Unknown Type: ${prop}`
},
})

View File

@@ -4,6 +4,7 @@
import identity from "lodash/identity"
import { string as randomString, randexp } from "../core/random"
import { isJSONSchema } from "../core/predicates"
import emailGenerator from "../generators/email"
import idnEmailGenerator from "../generators/idn-email"
import hostnameGenerator from "../generators/hostname"
@@ -118,8 +119,9 @@ const applyStringConstraints = (string, constraints = {}) => {
return constrainedString
}
const stringType = (schema) => {
const { pattern, format, contentEncoding, contentMediaType } = schema
const stringType = (schema, { sample } = {}) => {
const { contentEncoding, contentMediaType, contentSchema } = schema
const { pattern, format } = schema
const encode = encoderAPI(contentEncoding) || identity
let generatedString
@@ -127,7 +129,17 @@ const stringType = (schema) => {
generatedString = randexp(pattern)
} else if (typeof format === "string") {
generatedString = generateFormat(schema)
} else if (typeof contentMediaType === "string") {
} else if (
isJSONSchema(contentSchema) &&
typeof contentMediaType !== "undefined" &&
typeof sample !== "undefined"
) {
if (Array.isArray(sample) || typeof sample === "object") {
generatedString = JSON.stringify(sample)
} else {
generatedString = String(sample)
}
} else if (typeof contentMediaType !== "undefined") {
const mediaTypeGenerator = mediaTypeAPI(contentMediaType)
if (typeof mediaTypeGenerator === "function") {
generatedString = mediaTypeGenerator(schema)

View File

@@ -2,7 +2,6 @@
* @prettier
*
*/
import { Buffer } from "node:buffer"
import { fromJS } from "immutable"
import {
createXMLExample,
@@ -291,6 +290,58 @@ describe("sampleFromSchema", () => {
expect(sampleFromSchema(definition)).toMatch(base64Regex)
})
it("should handle contentSchema defined as type=object", function () {
const definition = fromJS({
type: "string",
contentMediaType: "application/json",
contentSchema: {
type: "object",
properties: {
a: { const: "b" },
},
},
})
expect(sampleFromSchema(definition)).toStrictEqual('{"a":"b"}')
})
it("should handle contentSchema defined as type=string", function () {
const definition = fromJS({
type: "string",
contentMediaType: "text/plain",
contentSchema: {
type: "string",
},
})
expect(sampleFromSchema(definition)).toStrictEqual("string")
})
it("should handle contentSchema defined as type=number", function () {
const definition = fromJS({
type: "string",
contentMediaType: "text/plain",
contentSchema: {
type: "number",
},
})
expect(sampleFromSchema(definition)).toStrictEqual("0")
})
it("should handle contentSchema defined as type=number + contentEncoding", function () {
const definition = fromJS({
type: "string",
contentEncoding: "base16",
contentMediaType: "text/plain",
contentSchema: {
type: "number",
},
})
expect(sampleFromSchema(definition)).toStrictEqual("30")
})
it("should handle type keyword defined as list of types", function () {
const definition = fromJS({ type: ["object", "string"] })
const expected = {}