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:
@@ -43,7 +43,7 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
|
||||
const { type, prefixItems, items } = schema
|
||||
|
||||
const getArrayType = () => {
|
||||
if (prefixItems) {
|
||||
if (Array.isArray(prefixItems)) {
|
||||
const prefixItemsTypes = prefixItems.map((itemSchema) =>
|
||||
getType(itemSchema, processedSchemas)
|
||||
)
|
||||
@@ -58,27 +58,31 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
|
||||
}
|
||||
|
||||
const inferType = () => {
|
||||
if (prefixItems || items || schema.contains) {
|
||||
if (
|
||||
Object.hasOwn(schema, "prefixItems") ||
|
||||
Object.hasOwn(schema, "items") ||
|
||||
Object.hasOwn(schema, "contains")
|
||||
) {
|
||||
return getArrayType()
|
||||
} else if (
|
||||
schema.properties ||
|
||||
schema.additionalProperties ||
|
||||
schema.patternProperties
|
||||
Object.hasOwn(schema, "properties") ||
|
||||
Object.hasOwn(schema, "additionalProperties") ||
|
||||
Object.hasOwn(schema, "patternProperties")
|
||||
) {
|
||||
return "object"
|
||||
} else if (
|
||||
schema.pattern ||
|
||||
schema.format ||
|
||||
schema.minLength ||
|
||||
schema.maxLength
|
||||
Object.hasOwn(schema, "pattern") ||
|
||||
Object.hasOwn(schema, "format") ||
|
||||
Object.hasOwn(schema, "minLength") ||
|
||||
Object.hasOwn(schema, "maxLength")
|
||||
) {
|
||||
return "string"
|
||||
} else if (
|
||||
schema.minimum ||
|
||||
schema.maximum ||
|
||||
schema.exclusiveMinimum ||
|
||||
schema.exclusiveMaximum ||
|
||||
schema.multipleOf
|
||||
Object.hasOwn(schema, "minimum") ||
|
||||
Object.hasOwn(schema, "maximum") ||
|
||||
Object.hasOwn(schema, "exclusiveMinimum") ||
|
||||
Object.hasOwn(schema, "exclusiveMaximum") ||
|
||||
Object.hasOwn(schema, "multipleOf")
|
||||
) {
|
||||
return "number | integer"
|
||||
} else if (typeof schema.const !== "undefined") {
|
||||
@@ -90,6 +94,8 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
|
||||
return Number.isInteger(schema.const) ? "integer" : "number"
|
||||
} else if (typeof schema.const === "string") {
|
||||
return "string"
|
||||
} else if (Array.isArray(schema.const)) {
|
||||
return "array<any>"
|
||||
} else if (typeof schema.const === "object") {
|
||||
return "object"
|
||||
}
|
||||
@@ -103,9 +109,11 @@ export const getType = (schema, processedSchemas = new WeakSet()) => {
|
||||
|
||||
const typeString = Array.isArray(type)
|
||||
? type.map((t) => (t === "array" ? getArrayType() : t)).join(" | ")
|
||||
: type && type.includes("array")
|
||||
: type === "array"
|
||||
? getArrayType()
|
||||
: type || inferType()
|
||||
: ["null", "boolean", "object", "array", "number", "string"].includes(type)
|
||||
? type
|
||||
: inferType()
|
||||
|
||||
const handleCombiningKeywords = (keyword, separator) => {
|
||||
if (Array.isArray(schema[keyword])) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @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]
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import isEmpty from "lodash/isEmpty"
|
||||
import { objectify, normalizeArray } from "core/utils"
|
||||
import memoizeN from "../../../../../helpers/memoizeN"
|
||||
import typeMap from "./types/index"
|
||||
import foldType from "./core/fold-type"
|
||||
import { getType } from "./core/type"
|
||||
import { typeCast } from "./core/utils"
|
||||
import { hasExample, extractExample } from "./core/example"
|
||||
import { pick as randomPick } from "./core/random"
|
||||
@@ -189,13 +189,17 @@ export const sampleFromSchemaGeneric = (
|
||||
}
|
||||
const _attr = {}
|
||||
let { xml, properties, additionalProperties, items, contains } = schema || {}
|
||||
let type = foldType(schema.type)
|
||||
let type = getType(schema)
|
||||
let { includeReadOnly, includeWriteOnly } = config
|
||||
xml = xml || {}
|
||||
let { name, prefix, namespace } = xml
|
||||
let displayName
|
||||
let res = {}
|
||||
|
||||
if (!Object.hasOwn(schema, "type")) {
|
||||
schema.type = type
|
||||
}
|
||||
|
||||
// set xml naming and attributes
|
||||
if (respectXML) {
|
||||
name = name || "notagname"
|
||||
@@ -213,36 +217,6 @@ export const sampleFromSchemaGeneric = (
|
||||
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
|
||||
const props = objectify(properties)
|
||||
let addPropertyToResult
|
||||
@@ -316,8 +290,9 @@ export const sampleFromSchemaGeneric = (
|
||||
_attr[props[propName].xml.name || propName] = enumAttrVal
|
||||
} else {
|
||||
const propSchema = typeCast(props[propName])
|
||||
const propSchemaType = getType(propSchema)
|
||||
const attrName = props[propName].xml.name || propName
|
||||
_attr[attrName] = typeMap[propSchema.type](propSchema)
|
||||
_attr[attrName] = typeMap[propSchemaType](propSchema)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -21,7 +21,7 @@ const typeMap = {
|
||||
|
||||
export default new Proxy(typeMap, {
|
||||
get(target, prop) {
|
||||
if (Object.hasOwn(target, prop)) {
|
||||
if (typeof prop === "string" && Object.hasOwn(target, prop)) {
|
||||
return target[prop]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user