Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions src/core/plugins/json-schema-2020-12/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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") {
Expand All @@ -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"
}
Expand All @@ -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])) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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]

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 8 additions & 33 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 @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand Down