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 { 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])) {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 { 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
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user