diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/example.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/example.js new file mode 100644 index 00000000000..f9b78e59edb --- /dev/null +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/example.js @@ -0,0 +1,57 @@ +/** + * @prettier + */ +import { isJSONSchemaObject } from "./predicates" + +/** + * Precedence of keywords that provides author defined values (top of the list = higher priority) + * + * ### examples + * Array containing example values for the item defined by the schema. + * Not guaranteed to be valid or invalid against the schema + * + * ### default + * Default value for an item defined by the schema. + * Is expected to be a valid instance of the schema. + * + * ### example + * Deprecated. Part of OpenAPI 3.1.0 Schema Object dialect. + * Represents single example. Equivalent of `examples` keywords + * with single item. + */ + +export const hasExample = (schema) => { + if (!isJSONSchemaObject(schema)) return false + + const { examples, example, default: defaultVal } = schema + + if (Array.isArray(examples) && examples.length >= 1) { + return true + } + + if (typeof defaultVal !== "undefined") { + return true + } + + return typeof example !== "undefined" +} + +export const extractExample = (schema) => { + if (!isJSONSchemaObject(schema)) return null + + const { examples, example, default: defaultVal } = schema + + if (Array.isArray(examples) && examples.length >= 1) { + return examples.at(0) + } + + if (typeof defaultVal !== "undefined") { + return defaultVal + } + + if (typeof example !== "undefined") { + return example + } + + return undefined +} 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 index c8a19f75781..2a07d8c84c0 100644 --- 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 @@ -4,7 +4,7 @@ import { ALL_TYPES } from "./constants" const foldType = (type) => { - if (Array.isArray(type)) { + if (Array.isArray(type) && type.length >= 1) { if (type.includes("array")) { return "array" } else if (type.includes("object")) { diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/predicates.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/predicates.js index ae0bf044fd0..6c780b58d77 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/predicates.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/predicates.js @@ -3,14 +3,6 @@ */ import isPlainObject from "lodash/isPlainObject" -export const isURI = (uri) => { - try { - return new URL(uri) && true - } catch { - return false - } -} - export const isBooleanJSONSchema = (schema) => { return typeof schema === "boolean" } diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/random.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/random.js index cd2ec61efad..6a707639cba 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/random.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/core/random.js @@ -3,6 +3,15 @@ */ import randomBytes from "randombytes" import RandExp from "randexp" + +/** + * Some of the functions returns constants. This is due to the nature + * of SwaggerUI expectations - provide as stable data as possible. + * + * In future, we may decide to randomize these function and provide + * true random values. + */ + export const bytes = (length) => randomBytes(length) export const randexp = (pattern) => { @@ -15,6 +24,10 @@ export const randexp = (pattern) => { } } +export const pick = (list) => { + return list.at(0) +} + export const string = () => "string" export const number = () => 0 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 555aac47b2a..0f36ca2c8ca 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 @@ -4,19 +4,13 @@ import XML from "xml" import isEmpty from "lodash/isEmpty" -import { objectify, normalizeArray, deeplyStripKey } from "core/utils" +import { objectify, normalizeArray } from "core/utils" import memoizeN from "../../../../../helpers/memoizeN" import typeMap from "./types/index" -import { isURI } from "./core/predicates" import foldType from "./core/fold-type" import { typeCast } from "./core/utils" - -/** - * Do a couple of quick sanity tests to ensure the value - * looks like a $$ref that swagger-client generates. - */ -const sanitizeRef = (value) => - deeplyStripKey(value, "$$ref", (val) => typeof val === "string" && isURI(val)) +import { hasExample, extractExample } from "./core/example" +import { pick as randomPick } from "./core/random" const objectConstraints = ["maxProperties", "minProperties", "required"] const arrayConstraints = [ @@ -49,6 +43,7 @@ const liftSampleHelper = (oldSchema, target, config = {}) => { } ;[ + "examples", "example", "default", "enum", @@ -133,22 +128,21 @@ export const sampleFromSchemaGeneric = ( if (typeof schema?.toJS === "function") schema = schema.toJS() schema = typeCast(schema) - let usePlainValue = - exampleOverride !== undefined || - (schema && schema.example !== undefined) || - (schema && schema.default !== undefined) + let usePlainValue = exampleOverride !== undefined || hasExample(schema) // first check if there is the need of combining this schema with others required by allOf const hasOneOf = !usePlainValue && schema && schema.oneOf && schema.oneOf.length > 0 const hasAnyOf = !usePlainValue && schema && schema.anyOf && schema.anyOf.length > 0 if (!usePlainValue && (hasOneOf || hasAnyOf)) { - const schemaToAdd = typeCast(hasOneOf ? schema.oneOf[0] : schema.anyOf[0]) + const schemaToAdd = typeCast( + hasOneOf ? randomPick(schema.oneOf) : randomPick(schema.anyOf) + ) liftSampleHelper(schemaToAdd, schema, config) if (!schema.xml && schemaToAdd.xml) { schema.xml = schemaToAdd.xml } - if (schema.example !== undefined && schemaToAdd.example !== undefined) { + if (hasExample(schema) && hasExample(schemaToAdd)) { usePlainValue = true } else if (schemaToAdd.properties) { if (!schema.properties) { @@ -194,16 +188,8 @@ export const sampleFromSchemaGeneric = ( } } const _attr = {} - let { - xml, - type, - example, - properties, - additionalProperties, - items, - contains, - } = schema || {} - type = foldType(type) + let { xml, properties, additionalProperties, items, contains } = schema || {} + let type = foldType(schema.type) let { includeReadOnly, includeWriteOnly } = config xml = xml || {} let { name, prefix, namespace } = xml @@ -320,15 +306,12 @@ export const sampleFromSchemaGeneric = ( if (props[propName].xml.attribute) { const enumAttrVal = Array.isArray(props[propName].enum) - ? props[propName].enum[0] + ? randomPick(props[propName].enum) : undefined - const attrExample = props[propName].example - const attrDefault = props[propName].default - - if (attrExample !== undefined) { - _attr[props[propName].xml.name || propName] = attrExample - } else if (attrDefault !== undefined) { - _attr[props[propName].xml.name || propName] = attrDefault + if (hasExample(props[propName])) { + _attr[props[propName].xml.name || propName] = extractExample( + props[propName] + ) } else if (enumAttrVal !== undefined) { _attr[props[propName].xml.name || propName] = enumAttrVal } else { @@ -402,21 +385,19 @@ export const sampleFromSchemaGeneric = ( if (usePlainValue) { let sample if (exampleOverride !== undefined) { - sample = sanitizeRef(exampleOverride) - } else if (example !== undefined) { - sample = sanitizeRef(example) + sample = exampleOverride } else { - sample = sanitizeRef(schema.default) + sample = extractExample(schema) } // if json just return if (!respectXML) { // spacial case yaml parser can not know about - if (typeof sample === "number" && type?.includes("string")) { + if (typeof sample === "number" && type === "string") { return `${sample}` } // return if sample does not need any parsing - if (typeof sample !== "string" || type?.includes("string")) { + if (typeof sample !== "string" || type === "string") { return sample } // check if sample is parsable or just a plain string @@ -434,7 +415,7 @@ export const sampleFromSchemaGeneric = ( } // generate xml sample recursively for array case - if (type?.includes("array")) { + if (type === "array") { if (!Array.isArray(sample)) { if (typeof sample === "string") { return sample @@ -474,7 +455,7 @@ export const sampleFromSchemaGeneric = ( } // generate xml sample recursively for object case - if (type?.includes("object")) { + if (type === "object") { // case literal example if (typeof sample === "string") { return sample @@ -522,7 +503,7 @@ export const sampleFromSchemaGeneric = ( } // use schema to generate sample - if (type?.includes("array")) { + if (type === "array") { let sampleArray = [] if (contains != null && typeof contains === "object") { @@ -611,7 +592,7 @@ export const sampleFromSchemaGeneric = ( return sampleArray } - if (type?.includes("object")) { + if (type === "object") { for (let propName in props) { if (!Object.hasOwn(props, propName)) { continue @@ -689,7 +670,7 @@ export const sampleFromSchemaGeneric = ( value = schema.const } else if (schema && Array.isArray(schema.enum)) { //display enum first value - value = normalizeArray(schema.enum)[0] + value = randomPick(normalizeArray(schema.enum)) } else if (schema) { // display schema default const contentSample = Object.hasOwn(schema, "contentSchema") diff --git a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/string.js b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/string.js index 1ba14250137..4c9b33986e5 100644 --- a/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/string.js +++ b/src/core/plugins/json-schema-2020-12/samples-extensions/fn/types/string.js @@ -131,7 +131,7 @@ const stringType = (schema, { sample } = {}) => { generatedString = generateFormat(schema) } else if ( isJSONSchema(contentSchema) && - typeof contentMediaType !== "undefined" && + typeof contentMediaType === "string" && typeof sample !== "undefined" ) { if (Array.isArray(sample) || typeof sample === "object") { @@ -139,7 +139,7 @@ const stringType = (schema, { sample } = {}) => { } else { generatedString = String(sample) } - } else if (typeof contentMediaType !== "undefined") { + } else if (typeof contentMediaType === "string") { const mediaTypeGenerator = mediaTypeAPI(contentMediaType) if (typeof mediaTypeGenerator === "function") { generatedString = mediaTypeGenerator(schema) diff --git a/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js b/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js index e84fcfe730f..df51e6df3df 100644 --- a/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js +++ b/test/unit/core/plugins/json-schema-2020-12/samples-extensions/fn.js @@ -627,98 +627,6 @@ describe("sampleFromSchema", () => { ) }) - it("returns object without any $$ref fields at the root schema level", function () { - const definition = { - type: "object", - properties: { - message: { - type: "string", - }, - }, - example: { - value: { - message: "Hello, World!", - }, - $$ref: "https://example.com/#/components/examples/WelcomeExample", - }, - $$ref: "https://example.com/#/components/schemas/Welcome", - } - - const expected = { - value: { - message: "Hello, World!", - }, - } - - expect(sampleFromSchema(definition, { includeWriteOnly: true })).toEqual( - expected - ) - }) - - it("returns object without any $$ref fields at nested schema levels", function () { - const definition = { - type: "object", - properties: { - message: { - type: "string", - }, - }, - example: { - a: { - value: { - message: "Hello, World!", - }, - $$ref: "https://example.com/#/components/examples/WelcomeExample", - }, - }, - $$ref: "https://example.com/#/components/schemas/Welcome", - } - - const expected = { - a: { - value: { - message: "Hello, World!", - }, - }, - } - - expect(sampleFromSchema(definition, { includeWriteOnly: true })).toEqual( - expected - ) - }) - - it("returns object with any $$ref fields that appear to be user-created", function () { - const definition = { - type: "object", - properties: { - message: { - type: "string", - }, - }, - example: { - $$ref: { - value: { - message: "Hello, World!", - }, - $$ref: "https://example.com/#/components/examples/WelcomeExample", - }, - }, - $$ref: "https://example.com/#/components/schemas/Welcome", - } - - const expected = { - $$ref: { - value: { - message: "Hello, World!", - }, - }, - } - - expect(sampleFromSchema(definition, { includeWriteOnly: true })).toEqual( - expected - ) - }) - it("returns example value for date-time property", () => { const definition = { type: "string", @@ -1836,11 +1744,28 @@ describe("createXMLExample", function () { it("returns example value when provided", function () { const expected = - '\ntwo' + '\none' + const definition = { + type: "string", + default: "one", + example: "two", + enum: ["two", "one"], + xml: { + name: "newtagname", + }, + } + + expect(sut(definition)).toEqual(expected) + }) + + it("returns item from examples value when provided", function () { + const expected = + '\nthree' const definition = { type: "string", default: "one", example: "two", + examples: ["three", "four"], enum: ["two", "one"], xml: { name: "newtagname",