Skip to content

Commit

Permalink
feat(examples): add support for examples keyword (#8908)
Browse files Browse the repository at this point in the history
This change is specific to JSON Schema 2020-12
and OpenAPI 3.1.0.

Refs #8577
  • Loading branch information
char0n committed Jun 10, 2023
1 parent 6c622a8 commit 4b0b285
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -15,6 +24,10 @@ export const randexp = (pattern) => {
}
}

export const pick = (list) => {
return list.at(0)
}

export const string = () => "string"

export const number = () => 0
Expand Down
69 changes: 25 additions & 44 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 @@ -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 = [
Expand Down Expand Up @@ -49,6 +43,7 @@ const liftSampleHelper = (oldSchema, target, config = {}) => {
}

;[
"examples",
"example",
"default",
"enum",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,15 @@ 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") {
generatedString = JSON.stringify(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)
Expand Down
Loading

0 comments on commit 4b0b285

Please sign in to comment.