feat(examples): add support for examples keyword (#8908)
This change is specific to JSON Schema 2020-12 and OpenAPI 3.1.0. Refs #8577
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
import { ALL_TYPES } from "./constants"
|
import { ALL_TYPES } from "./constants"
|
||||||
|
|
||||||
const foldType = (type) => {
|
const foldType = (type) => {
|
||||||
if (Array.isArray(type)) {
|
if (Array.isArray(type) && type.length >= 1) {
|
||||||
if (type.includes("array")) {
|
if (type.includes("array")) {
|
||||||
return "array"
|
return "array"
|
||||||
} else if (type.includes("object")) {
|
} else if (type.includes("object")) {
|
||||||
|
|||||||
@@ -3,14 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
import isPlainObject from "lodash/isPlainObject"
|
import isPlainObject from "lodash/isPlainObject"
|
||||||
|
|
||||||
export const isURI = (uri) => {
|
|
||||||
try {
|
|
||||||
return new URL(uri) && true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isBooleanJSONSchema = (schema) => {
|
export const isBooleanJSONSchema = (schema) => {
|
||||||
return typeof schema === "boolean"
|
return typeof schema === "boolean"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
*/
|
*/
|
||||||
import randomBytes from "randombytes"
|
import randomBytes from "randombytes"
|
||||||
import RandExp from "randexp"
|
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 bytes = (length) => randomBytes(length)
|
||||||
|
|
||||||
export const randexp = (pattern) => {
|
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 string = () => "string"
|
||||||
|
|
||||||
export const number = () => 0
|
export const number = () => 0
|
||||||
|
|||||||
@@ -4,19 +4,13 @@
|
|||||||
import XML from "xml"
|
import XML from "xml"
|
||||||
import isEmpty from "lodash/isEmpty"
|
import isEmpty from "lodash/isEmpty"
|
||||||
|
|
||||||
import { objectify, normalizeArray, deeplyStripKey } 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 { isURI } from "./core/predicates"
|
|
||||||
import foldType from "./core/fold-type"
|
import foldType from "./core/fold-type"
|
||||||
import { typeCast } from "./core/utils"
|
import { typeCast } from "./core/utils"
|
||||||
|
import { hasExample, extractExample } from "./core/example"
|
||||||
/**
|
import { pick as randomPick } from "./core/random"
|
||||||
* 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))
|
|
||||||
|
|
||||||
const objectConstraints = ["maxProperties", "minProperties", "required"]
|
const objectConstraints = ["maxProperties", "minProperties", "required"]
|
||||||
const arrayConstraints = [
|
const arrayConstraints = [
|
||||||
@@ -49,6 +43,7 @@ const liftSampleHelper = (oldSchema, target, config = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
;[
|
;[
|
||||||
|
"examples",
|
||||||
"example",
|
"example",
|
||||||
"default",
|
"default",
|
||||||
"enum",
|
"enum",
|
||||||
@@ -133,22 +128,21 @@ export const sampleFromSchemaGeneric = (
|
|||||||
if (typeof schema?.toJS === "function") schema = schema.toJS()
|
if (typeof schema?.toJS === "function") schema = schema.toJS()
|
||||||
schema = typeCast(schema)
|
schema = typeCast(schema)
|
||||||
|
|
||||||
let usePlainValue =
|
let usePlainValue = exampleOverride !== undefined || hasExample(schema)
|
||||||
exampleOverride !== undefined ||
|
|
||||||
(schema && schema.example !== undefined) ||
|
|
||||||
(schema && schema.default !== undefined)
|
|
||||||
// first check if there is the need of combining this schema with others required by allOf
|
// first check if there is the need of combining this schema with others required by allOf
|
||||||
const hasOneOf =
|
const hasOneOf =
|
||||||
!usePlainValue && schema && schema.oneOf && schema.oneOf.length > 0
|
!usePlainValue && schema && schema.oneOf && schema.oneOf.length > 0
|
||||||
const hasAnyOf =
|
const hasAnyOf =
|
||||||
!usePlainValue && schema && schema.anyOf && schema.anyOf.length > 0
|
!usePlainValue && schema && schema.anyOf && schema.anyOf.length > 0
|
||||||
if (!usePlainValue && (hasOneOf || hasAnyOf)) {
|
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)
|
liftSampleHelper(schemaToAdd, schema, config)
|
||||||
if (!schema.xml && schemaToAdd.xml) {
|
if (!schema.xml && schemaToAdd.xml) {
|
||||||
schema.xml = schemaToAdd.xml
|
schema.xml = schemaToAdd.xml
|
||||||
}
|
}
|
||||||
if (schema.example !== undefined && schemaToAdd.example !== undefined) {
|
if (hasExample(schema) && hasExample(schemaToAdd)) {
|
||||||
usePlainValue = true
|
usePlainValue = true
|
||||||
} else if (schemaToAdd.properties) {
|
} else if (schemaToAdd.properties) {
|
||||||
if (!schema.properties) {
|
if (!schema.properties) {
|
||||||
@@ -194,16 +188,8 @@ export const sampleFromSchemaGeneric = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const _attr = {}
|
const _attr = {}
|
||||||
let {
|
let { xml, properties, additionalProperties, items, contains } = schema || {}
|
||||||
xml,
|
let type = foldType(schema.type)
|
||||||
type,
|
|
||||||
example,
|
|
||||||
properties,
|
|
||||||
additionalProperties,
|
|
||||||
items,
|
|
||||||
contains,
|
|
||||||
} = schema || {}
|
|
||||||
type = foldType(type)
|
|
||||||
let { includeReadOnly, includeWriteOnly } = config
|
let { includeReadOnly, includeWriteOnly } = config
|
||||||
xml = xml || {}
|
xml = xml || {}
|
||||||
let { name, prefix, namespace } = xml
|
let { name, prefix, namespace } = xml
|
||||||
@@ -320,15 +306,12 @@ export const sampleFromSchemaGeneric = (
|
|||||||
|
|
||||||
if (props[propName].xml.attribute) {
|
if (props[propName].xml.attribute) {
|
||||||
const enumAttrVal = Array.isArray(props[propName].enum)
|
const enumAttrVal = Array.isArray(props[propName].enum)
|
||||||
? props[propName].enum[0]
|
? randomPick(props[propName].enum)
|
||||||
: undefined
|
: undefined
|
||||||
const attrExample = props[propName].example
|
if (hasExample(props[propName])) {
|
||||||
const attrDefault = props[propName].default
|
_attr[props[propName].xml.name || propName] = extractExample(
|
||||||
|
props[propName]
|
||||||
if (attrExample !== undefined) {
|
)
|
||||||
_attr[props[propName].xml.name || propName] = attrExample
|
|
||||||
} else if (attrDefault !== undefined) {
|
|
||||||
_attr[props[propName].xml.name || propName] = attrDefault
|
|
||||||
} else if (enumAttrVal !== undefined) {
|
} else if (enumAttrVal !== undefined) {
|
||||||
_attr[props[propName].xml.name || propName] = enumAttrVal
|
_attr[props[propName].xml.name || propName] = enumAttrVal
|
||||||
} else {
|
} else {
|
||||||
@@ -402,21 +385,19 @@ export const sampleFromSchemaGeneric = (
|
|||||||
if (usePlainValue) {
|
if (usePlainValue) {
|
||||||
let sample
|
let sample
|
||||||
if (exampleOverride !== undefined) {
|
if (exampleOverride !== undefined) {
|
||||||
sample = sanitizeRef(exampleOverride)
|
sample = exampleOverride
|
||||||
} else if (example !== undefined) {
|
|
||||||
sample = sanitizeRef(example)
|
|
||||||
} else {
|
} else {
|
||||||
sample = sanitizeRef(schema.default)
|
sample = extractExample(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if json just return
|
// if json just return
|
||||||
if (!respectXML) {
|
if (!respectXML) {
|
||||||
// spacial case yaml parser can not know about
|
// spacial case yaml parser can not know about
|
||||||
if (typeof sample === "number" && type?.includes("string")) {
|
if (typeof sample === "number" && type === "string") {
|
||||||
return `${sample}`
|
return `${sample}`
|
||||||
}
|
}
|
||||||
// return if sample does not need any parsing
|
// return if sample does not need any parsing
|
||||||
if (typeof sample !== "string" || type?.includes("string")) {
|
if (typeof sample !== "string" || type === "string") {
|
||||||
return sample
|
return sample
|
||||||
}
|
}
|
||||||
// check if sample is parsable or just a plain string
|
// check if sample is parsable or just a plain string
|
||||||
@@ -434,7 +415,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate xml sample recursively for array case
|
// generate xml sample recursively for array case
|
||||||
if (type?.includes("array")) {
|
if (type === "array") {
|
||||||
if (!Array.isArray(sample)) {
|
if (!Array.isArray(sample)) {
|
||||||
if (typeof sample === "string") {
|
if (typeof sample === "string") {
|
||||||
return sample
|
return sample
|
||||||
@@ -474,7 +455,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generate xml sample recursively for object case
|
// generate xml sample recursively for object case
|
||||||
if (type?.includes("object")) {
|
if (type === "object") {
|
||||||
// case literal example
|
// case literal example
|
||||||
if (typeof sample === "string") {
|
if (typeof sample === "string") {
|
||||||
return sample
|
return sample
|
||||||
@@ -522,7 +503,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// use schema to generate sample
|
// use schema to generate sample
|
||||||
if (type?.includes("array")) {
|
if (type === "array") {
|
||||||
let sampleArray = []
|
let sampleArray = []
|
||||||
|
|
||||||
if (contains != null && typeof contains === "object") {
|
if (contains != null && typeof contains === "object") {
|
||||||
@@ -611,7 +592,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
return sampleArray
|
return sampleArray
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type?.includes("object")) {
|
if (type === "object") {
|
||||||
for (let propName in props) {
|
for (let propName in props) {
|
||||||
if (!Object.hasOwn(props, propName)) {
|
if (!Object.hasOwn(props, propName)) {
|
||||||
continue
|
continue
|
||||||
@@ -689,7 +670,7 @@ export const sampleFromSchemaGeneric = (
|
|||||||
value = schema.const
|
value = schema.const
|
||||||
} else if (schema && Array.isArray(schema.enum)) {
|
} else if (schema && Array.isArray(schema.enum)) {
|
||||||
//display enum first value
|
//display enum first value
|
||||||
value = normalizeArray(schema.enum)[0]
|
value = randomPick(normalizeArray(schema.enum))
|
||||||
} else if (schema) {
|
} else if (schema) {
|
||||||
// display schema default
|
// display schema default
|
||||||
const contentSample = Object.hasOwn(schema, "contentSchema")
|
const contentSample = Object.hasOwn(schema, "contentSchema")
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const stringType = (schema, { sample } = {}) => {
|
|||||||
generatedString = generateFormat(schema)
|
generatedString = generateFormat(schema)
|
||||||
} else if (
|
} else if (
|
||||||
isJSONSchema(contentSchema) &&
|
isJSONSchema(contentSchema) &&
|
||||||
typeof contentMediaType !== "undefined" &&
|
typeof contentMediaType === "string" &&
|
||||||
typeof sample !== "undefined"
|
typeof sample !== "undefined"
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(sample) || typeof sample === "object") {
|
if (Array.isArray(sample) || typeof sample === "object") {
|
||||||
@@ -139,7 +139,7 @@ const stringType = (schema, { sample } = {}) => {
|
|||||||
} else {
|
} else {
|
||||||
generatedString = String(sample)
|
generatedString = String(sample)
|
||||||
}
|
}
|
||||||
} else if (typeof contentMediaType !== "undefined") {
|
} else if (typeof contentMediaType === "string") {
|
||||||
const mediaTypeGenerator = mediaTypeAPI(contentMediaType)
|
const mediaTypeGenerator = mediaTypeAPI(contentMediaType)
|
||||||
if (typeof mediaTypeGenerator === "function") {
|
if (typeof mediaTypeGenerator === "function") {
|
||||||
generatedString = mediaTypeGenerator(schema)
|
generatedString = mediaTypeGenerator(schema)
|
||||||
|
|||||||
@@ -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", () => {
|
it("returns example value for date-time property", () => {
|
||||||
const definition = {
|
const definition = {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -1836,7 +1744,7 @@ describe("createXMLExample", function () {
|
|||||||
|
|
||||||
it("returns example value when provided", function () {
|
it("returns example value when provided", function () {
|
||||||
const expected =
|
const expected =
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n<newtagname>two</newtagname>'
|
'<?xml version="1.0" encoding="UTF-8"?>\n<newtagname>one</newtagname>'
|
||||||
const definition = {
|
const definition = {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "one",
|
default: "one",
|
||||||
@@ -1850,6 +1758,23 @@ describe("createXMLExample", function () {
|
|||||||
expect(sut(definition)).toEqual(expected)
|
expect(sut(definition)).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("returns item from examples value when provided", function () {
|
||||||
|
const expected =
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n<newtagname>three</newtagname>'
|
||||||
|
const definition = {
|
||||||
|
type: "string",
|
||||||
|
default: "one",
|
||||||
|
example: "two",
|
||||||
|
examples: ["three", "four"],
|
||||||
|
enum: ["two", "one"],
|
||||||
|
xml: {
|
||||||
|
name: "newtagname",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(sut(definition)).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
it("sets first enum if provided", function () {
|
it("sets first enum if provided", function () {
|
||||||
const expected =
|
const expected =
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n<newtagname>one</newtagname>'
|
'<?xml version="1.0" encoding="UTF-8"?>\n<newtagname>one</newtagname>'
|
||||||
|
|||||||
Reference in New Issue
Block a user