Skip to content

Commit

Permalink
feat(samples): add support for contentSchema keyword (#8907)
Browse files Browse the repository at this point in the history
This change is specific to JSON Schema 2020-12
and OpenAPI 3.1.0.

Refs #8577
  • Loading branch information
char0n authored Jun 9, 2023
1 parent d72b72c commit 6c622a8
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @prettier
*/
export const SCALAR_TYPES = ["integer", "number", "string", "boolean", "null"]

export const ALL_TYPES = ["array", "object", ...SCALAR_TYPES]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 24 additions & 23 deletions src/core/plugins/json-schema-2020-12/samples-extensions/fn/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) ||
Expand All @@ -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
Expand Down Expand Up @@ -211,6 +203,7 @@ export const sampleFromSchemaGeneric = (
items,
contains,
} = schema || {}
type = foldType(type)
let { includeReadOnly, includeWriteOnly } = config
xml = xml || {}
let { name, prefix, namespace } = xml
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -650,7 +643,7 @@ export const sampleFromSchemaGeneric = (
}
propertyAddedCounter++
} else if (additionalProperties) {
const additionalProps = objectify(additionalProperties)
const additionalProps = typeCast(additionalProperties)
const additionalPropSample = sampleFromSchemaGeneric(
additionalProps,
config,
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -118,16 +119,27 @@ 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

if (typeof pattern === "string") {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @prettier
*
*/
import { Buffer } from "node:buffer"
import { fromJS } from "immutable"
import {
createXMLExample,
Expand Down Expand Up @@ -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 = {}
Expand Down

0 comments on commit 6c622a8

Please sign in to comment.