From 8b046217d3098cf1ab2066a14e9f9bff5cdcfa33 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Sun, 11 Jun 2023 13:08:15 +0200 Subject: [PATCH] feat(samples): add support for inferring schema type This change is specific to JSON Schema 2020-12 and OpenAPI 3.1.0. Refs #8577 --- src/core/plugins/json-schema-2020-12/fn.js | 40 +++-- .../samples-extensions/fn/core/constants.js | 2 +- .../samples-extensions/fn/core/fold-type.js | 24 --- .../samples-extensions/fn/core/type.js | 145 ++++++++++++++++++ .../samples-extensions/fn/main.js | 41 +---- .../samples-extensions/fn/types/index.js | 2 +- 6 files changed, 179 insertions(+), 75 deletions(-) delete mode 100644 src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/fold-type.js create mode 100644 src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/type.js diff --git a/src/core/plugins/json-schema-2020-12/fn.js b/src/core/plugins/json-schema-2020-12/fn.js index 10aa64720f7..f4f27c9c94b 100644 --- a/src/core/plugins/json-schema-2020-12/fn.js +++ b/src/core/plugins/json-schema-2020-12/fn.js @@ -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" } 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])) { diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/constants.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/constants.js index 8751dd4c374..ded1fb36c19 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/constants.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/constants.js @@ -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] diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/fold-type.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/fold-type.js deleted file mode 100644 index 2a07d8c84c0..00000000000 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/fold-type.js +++ /dev/null @@ -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 diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/type.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/type.js new file mode 100644 index 00000000000..70f2cc16880 --- /dev/null +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/type.js @@ -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) +} diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/main.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/main.js index 0f36ca2c8ca..daeed6ea7c8 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/main.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/main.js @@ -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 diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/index.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/index.js index 6d7aeda8cbb..ba2393bf435 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/index.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/index.js @@ -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] }