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:
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @prettier
|
||||
*/
|
||||
export const SCALAR_TYPES = ["integer", "number", "string", "boolean", "null"]
|
||||
|
||||
export const ALL_TYPES = ["array", "object", ...SCALAR_TYPES]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user