feat(samples): add support for inferring schema type (#8909)

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-11 13:14:44 +02:00
committed by GitHub
parent 4b0b28518e
commit ce417d59d2
6 changed files with 179 additions and 75 deletions

View File

@@ -43,7 +43,7 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
const { type, prefixItems, items } = schema const { type, prefixItems, items } = schema
const getArrayType = () => { const getArrayType = () => {
if (prefixItems) { if (Array.isArray(prefixItems)) {
const prefixItemsTypes = prefixItems.map((itemSchema) => const prefixItemsTypes = prefixItems.map((itemSchema) =>
getType(itemSchema, processedSchemas) getType(itemSchema, processedSchemas)
) )
@@ -58,27 +58,31 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
} }
const inferType = () => { const inferType = () => {
if (prefixItems || items || schema.contains) { if (
Object.hasOwn(schema, "prefixItems") ||
Object.hasOwn(schema, "items") ||
Object.hasOwn(schema, "contains")
) {
return getArrayType() return getArrayType()
} else if ( } else if (
schema.properties || Object.hasOwn(schema, "properties") ||
schema.additionalProperties || Object.hasOwn(schema, "additionalProperties") ||
schema.patternProperties Object.hasOwn(schema, "patternProperties")
) { ) {
return "object" return "object"
} else if ( } else if (
schema.pattern || Object.hasOwn(schema, "pattern") ||
schema.format || Object.hasOwn(schema, "format") ||
schema.minLength || Object.hasOwn(schema, "minLength") ||
schema.maxLength Object.hasOwn(schema, "maxLength")
) { ) {
return "string" return "string"
} else if ( } else if (
schema.minimum || Object.hasOwn(schema, "minimum") ||
schema.maximum || Object.hasOwn(schema, "maximum") ||
schema.exclusiveMinimum || Object.hasOwn(schema, "exclusiveMinimum") ||
schema.exclusiveMaximum || Object.hasOwn(schema, "exclusiveMaximum") ||
schema.multipleOf Object.hasOwn(schema, "multipleOf")
) { ) {
return "number | integer" return "number | integer"
} else if (typeof schema.const !== "undefined") { } else if (typeof schema.const !== "undefined") {
@@ -90,6 +94,8 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
return Number.isInteger(schema.const) ? "integer" : "number" return Number.isInteger(schema.const) ? "integer" : "number"
} else if (typeof schema.const === "string") { } else if (typeof schema.const === "string") {
return "string" return "string"
} else if (Array.isArray(schema.const)) {
return "array<any>"
} else if (typeof schema.const === "object") { } else if (typeof schema.const === "object") {
return "object" return "object"
} }
@@ -103,9 +109,11 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
const typeString = Array.isArray(type) const typeString = Array.isArray(type)
? type.map((t) => (t === "array" ? getArrayType() : t)).join(" | ") ? type.map((t) => (t === "array" ? getArrayType() : t)).join(" | ")
: type && type.includes("array") : type === "array"
? getArrayType() ? getArrayType()
: type || inferType() : ["null", "boolean", "object", "array", "number", "string"].includes(type)
? type
: inferType()
const handleCombiningKeywords = (keyword, separator) => { const handleCombiningKeywords = (keyword, separator) => {
if (Array.isArray(schema[keyword])) { if (Array.isArray(schema[keyword])) {

View File

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

View File

@@ -1,24 +0,0 @@
/**
* @prettier
*/
import { ALL_TYPES } from "./constants"
const foldType = (type) => {
if (Array.isArray(type) && type.length >= 1) {
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,145 @@
/**
* @prettier
*/
import { ALL_TYPES } from "./constants"
import { isJSONSchemaObject } from "./predicates"
import { pick as randomPick } from "./random"
const inferringKeywords = {
array: [
"items",
"prefixItems",
"contains",
"maxContains",
"minContains",
"maxItems",
"minItems",
"uniqueItems",
"unevaluatedItems",
],
object: [
"properties",
"additionalProperties",
"patternProperties",
"propertyNames",
"minProperties",
"maxProperties",
"required",
"dependentSchemas",
"dependentRequired",
"unevaluatedProperties",
],
string: [
"pattern",
"format",
"minLength",
"maxLength",
"contentEncoding",
"contentMediaType",
"contentSchema",
],
integer: [
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
],
}
inferringKeywords.number = inferringKeywords.integer
const fallbackType = "string"
export const foldType = (type) => {
if (Array.isArray(type) && type.length >= 1) {
if (type.includes("array")) {
return "array"
} else if (type.includes("object")) {
return "object"
} else {
const pickedType = randomPick(type)
if (ALL_TYPES.includes(pickedType)) {
return pickedType
}
}
}
if (ALL_TYPES.includes(type)) {
return type
}
return null
}
export const inferType = (schema, processedSchemas = new WeakSet()) => {
if (!isJSONSchemaObject(schema)) return fallbackType
if (processedSchemas.has(schema)) return fallbackType
processedSchemas.add(schema)
let { type, const: constant } = schema
type = foldType(type)
// inferring type from inferring keywords
if (typeof type !== "string") {
const inferringTypes = Object.keys(inferringKeywords)
interrupt: for (let i = 0; i < inferringTypes.length; i += 1) {
const inferringType = inferringTypes[i]
const inferringTypeKeywords = inferringKeywords[inferringType]
for (let j = 0; j < inferringTypeKeywords.length; j += 1) {
const inferringKeyword = inferringTypeKeywords[j]
if (Object.hasOwn(schema, inferringKeyword)) {
type = inferringType
break interrupt
}
}
}
}
// inferring type from const keyword
if (typeof type !== "string" && typeof constant !== "undefined") {
if (constant === null) {
type = "null"
} else if (typeof constant === "boolean") {
type = "boolean"
} else if (typeof constant === "number") {
type = Number.isInteger(constant) ? "integer" : "number"
} else if (typeof constant === "string") {
type = "string"
} else if (typeof constant === "object") {
type = "object"
}
}
// inferring type from combining schemas
if (typeof type !== "string") {
const combineTypes = (keyword) => {
if (Array.isArray(schema[keyword])) {
const combinedTypes = schema[keyword].map((subSchema) =>
inferType(subSchema, processedSchemas)
)
return foldType(combinedTypes)
}
return null
}
const allOf = combineTypes("allOf")
const anyOf = combineTypes("anyOf")
const oneOf = combineTypes("oneOf")
const not = schema.not ? inferType(schema.not, processedSchemas) : null
if (allOf || anyOf || oneOf || not) {
type = foldType([allOf, anyOf, oneOf, not].filter(Boolean))
}
}
processedSchemas.delete(schema)
return type || fallbackType
}
export const getType = (schema) => {
return inferType(schema)
}

View File

@@ -7,7 +7,7 @@ import isEmpty from "lodash/isEmpty"
import { objectify, normalizeArray } from "core/utils" import { objectify, normalizeArray } from "core/utils"
import memoizeN from "../../../../../helpers/memoizeN" import memoizeN from "../../../../../helpers/memoizeN"
import typeMap from "./types/index" import typeMap from "./types/index"
import foldType from "./core/fold-type" 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"
@@ -189,13 +189,17 @@ export const sampleFromSchemaGeneric = (
} }
const _attr = {} const _attr = {}
let { xml, properties, additionalProperties, items, contains } = schema || {} let { xml, properties, additionalProperties, items, contains } = schema || {}
let type = foldType(schema.type) let type = getType(schema)
let { includeReadOnly, includeWriteOnly } = config let { includeReadOnly, includeWriteOnly } = config
xml = xml || {} xml = xml || {}
let { name, prefix, namespace } = xml let { name, prefix, namespace } = xml
let displayName let displayName
let res = {} let res = {}
if (!Object.hasOwn(schema, "type")) {
schema.type = type
}
// set xml naming and attributes // set xml naming and attributes
if (respectXML) { if (respectXML) {
name = name || "notagname" name = name || "notagname"
@@ -213,36 +217,6 @@ export const sampleFromSchemaGeneric = (
res[displayName] = [] res[displayName] = []
} }
const schemaHasAny = (keys) => keys.some((key) => Object.hasOwn(schema, key))
// try recover missing type
if (schema && typeof type !== "string" && !Array.isArray(type)) {
if (properties || additionalProperties || schemaHasAny(objectConstraints)) {
type = "object"
} else if (items || contains || schemaHasAny(arrayConstraints)) {
type = "array"
} else if (schemaHasAny(numberConstraints)) {
type = "number"
schema.type = "number"
} else if (!usePlainValue && !schema.enum) {
// implicit cover schemaHasAny(stringContracts) or A schema without a type matches any data type is:
// components:
// schemas:
// AnyValue:
// anyOf:
// - type: string
// - type: number
// - type: integer
// - type: boolean
// - type: array
// items: {}
// - type: object
//
// which would resolve to type: string
type = "string"
schema.type = "string"
}
}
// add to result helper init for xml or json // add to result helper init for xml or json
const props = objectify(properties) const props = objectify(properties)
let addPropertyToResult let addPropertyToResult
@@ -316,8 +290,9 @@ export const sampleFromSchemaGeneric = (
_attr[props[propName].xml.name || propName] = enumAttrVal _attr[props[propName].xml.name || propName] = enumAttrVal
} else { } else {
const propSchema = typeCast(props[propName]) const propSchema = typeCast(props[propName])
const propSchemaType = getType(propSchema)
const attrName = props[propName].xml.name || propName const attrName = props[propName].xml.name || propName
_attr[attrName] = typeMap[propSchema.type](propSchema) _attr[attrName] = typeMap[propSchemaType](propSchema)
} }
return return

View File

@@ -21,7 +21,7 @@ const typeMap = {
export default new Proxy(typeMap, { export default new Proxy(typeMap, {
get(target, prop) { get(target, prop) {
if (Object.hasOwn(target, prop)) { if (typeof prop === "string" && Object.hasOwn(target, prop)) {
return target[prop] return target[prop]
} }