feat(json-schema): expose API that generates examples from JSON Schema (#9190)

This allows to use the samples API in a static way
without fully instantiating SwaggerUI.

Refs #9188
This commit is contained in:
Vladimír Gorej
2023-09-05 14:13:53 +02:00
committed by GitHub
parent edd1153723
commit 113996f627
83 changed files with 292 additions and 88 deletions

View File

@@ -44,16 +44,6 @@ import Accordion from "./components/Accordion/Accordion"
import ExpandDeepButton from "./components/ExpandDeepButton/ExpandDeepButton"
import ChevronRightIcon from "./components/icons/ChevronRight"
import { upperFirst, hasKeyword, isExpandable } from "./fn"
import {
sampleFromSchema,
sampleFromSchemaGeneric,
createXMLExample,
memoizedSampleFromSchema,
memoizedCreateXMLExample,
encoderAPI,
mediaTypeAPI,
formatAPI,
} from "./samples-extensions/fn/index"
import { JSONSchemaDeepExpansionContext } from "./context"
import { useFn, useConfig, useComponent, useIsExpandedDeeply } from "./hooks"
import { withJSONSchemaContext } from "./hoc"
@@ -114,14 +104,6 @@ const JSONSchema202012Plugin = () => ({
useConfig,
useComponent,
useIsExpandedDeeply,
sampleFromSchema,
sampleFromSchemaGeneric,
sampleEncoderAPI: encoderAPI,
sampleFormatAPI: formatAPI,
sampleMediaTypeAPI: mediaTypeAPI,
createXMLExample,
memoizedSampleFromSchema,
memoizedCreateXMLExample,
},
},
})

View File

@@ -1,20 +0,0 @@
/**
* @prettier
*/
import EncoderRegistry from "core/plugins/json-schema-2020-12/samples-extensions/fn/class/EncoderRegistry"
const registry = new EncoderRegistry()
const encoderAPI = (encodingName, encoder) => {
if (typeof encoder === "function") {
return registry.register(encodingName, encoder)
} else if (encoder === null) {
return registry.unregister(encodingName)
}
return registry.get(encodingName)
}
encoderAPI.getDefaults = () => registry.defaults
export default encoderAPI

View File

@@ -1,19 +0,0 @@
/**
* @prettier
*/
import Registry from "../class/Registry"
const registry = new Registry()
const formatAPI = (format, generator) => {
if (typeof generator === "function") {
return registry.register(format, generator)
} else if (generator === null) {
return registry.unregister(format)
}
return registry.get(format)
}
export default formatAPI

View File

@@ -1,27 +0,0 @@
/**
* @prettier
*/
import MediaTypeRegistry from "../class/MediaTypeRegistry"
const registry = new MediaTypeRegistry()
const mediaTypeAPI = (mediaType, generator) => {
if (typeof generator === "function") {
return registry.register(mediaType, generator)
} else if (generator === null) {
return registry.unregister(mediaType)
}
const mediaTypeNoParams = mediaType.split(";").at(0)
const topLevelMediaType = `${mediaTypeNoParams.split("/").at(0)}/*`
return (
registry.get(mediaType) ||
registry.get(mediaTypeNoParams) ||
registry.get(topLevelMediaType)
)
}
mediaTypeAPI.getDefaults = () => registry.defaults
export default mediaTypeAPI

View File

@@ -1,31 +0,0 @@
/**
* @prettier
*/
import Registry from "./Registry"
import encode7bit from "../encoders/7bit"
import encode8bit from "../encoders/8bit"
import encodeBinary from "../encoders/binary"
import encodeQuotedPrintable from "../encoders/quoted-printable"
import encodeBase16 from "../encoders/base16"
import encodeBase32 from "../encoders/base32"
import encodeBase64 from "../encoders/base64"
class EncoderRegistry extends Registry {
#defaults = {
"7bit": encode7bit,
"8bit": encode8bit,
binary: encodeBinary,
"quoted-printable": encodeQuotedPrintable,
base16: encodeBase16,
base32: encodeBase32,
base64: encodeBase64,
}
data = { ...this.#defaults }
get defaults() {
return { ...this.#defaults }
}
}
export default EncoderRegistry

View File

@@ -1,27 +0,0 @@
/**
* @prettier
*/
import Registry from "./Registry"
import textMediaTypesGenerators from "../generators/media-types/text"
import imageMediaTypesGenerators from "../generators/media-types/image"
import audioMediaTypesGenerators from "../generators/media-types/audio"
import videoMediaTypesGenerators from "../generators/media-types/video"
import applicationMediaTypesGenerators from "../generators/media-types/application"
class MediaTypeRegistry extends Registry {
#defaults = {
...textMediaTypesGenerators,
...imageMediaTypesGenerators,
...audioMediaTypesGenerators,
...videoMediaTypesGenerators,
...applicationMediaTypesGenerators,
}
data = { ...this.#defaults }
get defaults() {
return { ...this.#defaults }
}
}
export default MediaTypeRegistry

View File

@@ -1,24 +0,0 @@
/**
* @prettier
*/
class Registry {
data = {}
register(name, value) {
this.data[name] = value
}
unregister(name) {
if (typeof name === "undefined") {
this.data = {}
} else {
delete this.data[name]
}
}
get(name) {
return this.data[name]
}
}
export default Registry

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
export const SCALAR_TYPES = ["number", "integer", "string", "boolean", "null"]
export const ALL_TYPES = ["array", "object", ...SCALAR_TYPES]

View File

@@ -1,57 +0,0 @@
/**
* @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
}

View File

@@ -1,83 +0,0 @@
/**
* @prettier
*/
import { normalizeArray as ensureArray } from "core/utils"
import { isBooleanJSONSchema, isJSONSchema } from "./predicates"
const merge = (target, source, config = {}) => {
if (isBooleanJSONSchema(target) && target === true) return true
if (isBooleanJSONSchema(target) && target === false) return false
if (isBooleanJSONSchema(source) && source === true) return true
if (isBooleanJSONSchema(source) && source === false) return false
if (!isJSONSchema(target)) return source
if (!isJSONSchema(source)) return target
/**
* Merging properties from the source object into the target object
* only if they do not already exist in the target object.
*/
const merged = { ...source, ...target }
// merging the type keyword
if (source.type && target.type) {
if (Array.isArray(source.type) && typeof source.type === "string") {
const mergedType = ensureArray(source.type).concat(target.type)
merged.type = Array.from(new Set(mergedType))
}
}
// merging required keyword
if (Array.isArray(source.required) && Array.isArray(target.required)) {
merged.required = [...new Set([...target.required, ...source.required])]
}
// merging properties keyword
if (source.properties && target.properties) {
const allPropertyNames = new Set([
...Object.keys(source.properties),
...Object.keys(target.properties),
])
merged.properties = {}
for (const name of allPropertyNames) {
const sourceProperty = source.properties[name] || {}
const targetProperty = target.properties[name] || {}
if (
(sourceProperty.readOnly && !config.includeReadOnly) ||
(sourceProperty.writeOnly && !config.includeWriteOnly)
) {
merged.required = (merged.required || []).filter((p) => p !== name)
} else {
merged.properties[name] = merge(targetProperty, sourceProperty, config)
}
}
}
// merging items keyword
if (isJSONSchema(source.items) && isJSONSchema(target.items)) {
merged.items = merge(target.items, source.items, config)
}
// merging contains keyword
if (isJSONSchema(source.contains) && isJSONSchema(target.contains)) {
merged.contains = merge(target.contains, source.contains, config)
}
// merging contentSchema keyword
if (
isJSONSchema(source.contentSchema) &&
isJSONSchema(target.contentSchema)
) {
merged.contentSchema = merge(
target.contentSchema,
source.contentSchema,
config
)
}
return merged
}
export default merge

View File

@@ -1,16 +0,0 @@
/**
* @prettier
*/
import isPlainObject from "lodash/isPlainObject"
export const isBooleanJSONSchema = (schema) => {
return typeof schema === "boolean"
}
export const isJSONSchemaObject = (schema) => {
return isPlainObject(schema)
}
export const isJSONSchema = (schema) => {
return isBooleanJSONSchema(schema) || isJSONSchemaObject(schema)
}

View File

@@ -1,35 +0,0 @@
/**
* @prettier
*/
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) => {
try {
const randexpInstance = new RandExp(pattern)
return randexpInstance.gen()
} catch {
// invalid regex should not cause a crash (regex syntax varies across languages)
return "string"
}
}
export const pick = (list) => {
return list.at(0)
}
export const string = () => "string"
export const number = () => 0
export const integer = () => 0

View File

@@ -1,153 +0,0 @@
/**
* @prettier
*/
import { ALL_TYPES } from "./constants"
import { isJSONSchemaObject } from "./predicates"
import { pick as randomPick } from "./random"
import { hasExample, extractExample } from "./example"
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"
const inferTypeFromValue = (value) => {
if (typeof value === "undefined") return null
if (value === null) return "null"
if (Array.isArray(value)) return "array"
if (Number.isInteger(value)) return "integer"
return typeof value
}
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") {
const constType = inferTypeFromValue(constant)
type = typeof constType === "string" ? constType : type
}
// 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))
}
}
// inferring type from example
if (typeof type !== "string" && hasExample(schema)) {
const example = extractExample(schema)
const exampleType = inferTypeFromValue(example)
type = typeof exampleType === "string" ? exampleType : type
}
processedSchemas.delete(schema)
return type || fallbackType
}
export const getType = (schema) => {
return inferType(schema)
}

View File

@@ -1,23 +0,0 @@
/**
* @prettier
*/
import { isBooleanJSONSchema, isJSONSchemaObject } from "./predicates"
export const fromJSONBooleanSchema = (schema) => {
if (schema === false) {
return { not: {} }
}
return {}
}
export const typeCast = (schema) => {
if (isBooleanJSONSchema(schema)) {
return fromJSONBooleanSchema(schema)
}
if (!isJSONSchemaObject(schema)) {
return {}
}
return schema
}

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const encode7bit = (content) => Buffer.from(content).toString("ascii")
export default encode7bit

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const encode8bit = (content) => Buffer.from(content).toString("utf8")
export default encode8bit

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const encodeBase16 = (content) => Buffer.from(content).toString("hex")
export default encodeBase16

View File

@@ -1,34 +0,0 @@
/**
* @prettier
*/
const encodeBase32 = (content) => {
const utf8Value = Buffer.from(content).toString("utf8")
const base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
let paddingCount = 0
let base32Str = ""
let buffer = 0
let bufferLength = 0
for (let i = 0; i < utf8Value.length; i++) {
buffer = (buffer << 8) | utf8Value.charCodeAt(i)
bufferLength += 8
while (bufferLength >= 5) {
base32Str += base32Alphabet.charAt((buffer >>> (bufferLength - 5)) & 31)
bufferLength -= 5
}
}
if (bufferLength > 0) {
base32Str += base32Alphabet.charAt((buffer << (5 - bufferLength)) & 31)
paddingCount = (8 - ((utf8Value.length * 8) % 5)) % 5
}
for (let i = 0; i < paddingCount; i++) {
base32Str += "="
}
return base32Str
}
export default encodeBase32

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const encodeBase64 = (content) => Buffer.from(content).toString("base64")
export default encodeBase64

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const encodeBinary = (content) => Buffer.from(content).toString("binary")
export default encodeBinary

View File

@@ -1,38 +0,0 @@
/**
* @prettier
*/
const encodeQuotedPrintable = (content) => {
let quotedPrintable = ""
for (let i = 0; i < content.length; i++) {
const charCode = content.charCodeAt(i)
if (charCode === 61) {
// ASCII content of "="
quotedPrintable += "=3D"
} else if (
(charCode >= 33 && charCode <= 60) ||
(charCode >= 62 && charCode <= 126) ||
charCode === 9 ||
charCode === 32
) {
quotedPrintable += content.charAt(i)
} else if (charCode === 13 || charCode === 10) {
quotedPrintable += "\r\n"
} else if (charCode > 126) {
// convert non-ASCII characters to UTF-8 and encode each byte
const utf8 = unescape(encodeURIComponent(content.charAt(i)))
for (let j = 0; j < utf8.length; j++) {
quotedPrintable +=
"=" + ("0" + utf8.charCodeAt(j).toString(16)).slice(-2).toUpperCase()
}
} else {
quotedPrintable +=
"=" + ("0" + charCode.toString(16)).slice(-2).toUpperCase()
}
}
return quotedPrintable
}
export default encodeQuotedPrintable

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const dateTimeGenerator = () => new Date().toISOString()
export default dateTimeGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const dateGenerator = () => new Date().toISOString().substring(0, 10)
export default dateGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const doubleGenerator = () => 0.1
export default doubleGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const durationGenerator = () => "P3D" // expresses a duration of 3 days
export default durationGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const emailGenerator = () => "user@example.com"
export default emailGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const floatGenerator = () => 0.1
export default floatGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const hostnameGenerator = () => "example.com"
export default hostnameGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const idnEmailGenerator = () => "실례@example.com"
export default idnEmailGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const idnHostnameGenerator = () => "실례.com"
export default idnHostnameGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const int32Generator = () => (2 ** 30) >>> 0
export default int32Generator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const int64Generator = () => 2 ** 53 - 1
export default int64Generator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const ipv4Generator = () => "198.51.100.42"
export default ipv4Generator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const ipv6Generator = () => "2001:0db8:5b96:0000:0000:426f:8e17:642a"
export default ipv6Generator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const iriReferenceGenerator = () => "path/실례.html"
export default iriReferenceGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const iriGenerator = () => "https://실례.com/"
export default iriGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const jsonPointerGenerator = () => "/a/b/c"
export default jsonPointerGenerator

View File

@@ -1,17 +0,0 @@
/**
* @prettier
*/
import { bytes } from "../../core/random"
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const applicationMediaTypesGenerators = {
"application/json": () => '{"key":"value"}',
"application/ld+json": () => '{"name": "John Doe"}',
"application/x-httpd-php": () => "<?php echo '<p>Hello World!</p>'; ?>",
"application/rtf": () => String.raw`{\rtf1\adeflang1025\ansi\ansicpg1252\uc1`,
"application/x-sh": () => 'echo "Hello World!"',
"application/xhtml+xml": () => "<p>content</p>",
"application/*": () => bytes(25).toString("binary"),
}
export default applicationMediaTypesGenerators

View File

@@ -1,10 +0,0 @@
/**
* @prettier
*/
import { bytes } from "../../core/random"
const audioMediaTypesGenerators = {
"audio/*": () => bytes(25).toString("binary"),
}
export default audioMediaTypesGenerators

View File

@@ -1,10 +0,0 @@
/**
* @prettier
*/
import { bytes } from "../../core/random"
const imageMediaTypesGenerators = {
"image/*": () => bytes(25).toString("binary"),
}
export default imageMediaTypesGenerators

View File

@@ -1,17 +0,0 @@
/**
* @prettier
*/
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const textMediaTypesGenerators = {
"text/plain": () => "string",
"text/css": () => ".selector { border: 1px solid red }",
"text/csv": () => "value1,value2,value3",
"text/html": () => "<p>content</p>",
"text/calendar": () => "BEGIN:VCALENDAR",
"text/javascript": () => "console.dir('Hello world!');",
"text/xml": () => '<person age="30">John Doe</person>',
"text/*": () => "string",
}
export default textMediaTypesGenerators

View File

@@ -1,10 +0,0 @@
/**
* @prettier
*/
import { bytes } from "../../core/random"
const videoMediaTypesGenerators = {
"video/*": () => bytes(25).toString("binary"),
}
export default videoMediaTypesGenerators

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const passwordGenerator = () => "********"
export default passwordGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const regexGenerator = () => "^[a-z]+$"
export default regexGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const relativeJsonPointerGenerator = () => "1/0"
export default relativeJsonPointerGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const timeGenerator = () => new Date().toISOString().substring(11)
export default timeGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const uriReferenceGenerator = () => "path/index.html"
export default uriReferenceGenerator

View File

@@ -1,7 +0,0 @@
/**
* @prettier
*/
const uriTemplateGenerator = () =>
"https://example.com/dictionary/{term:1}/{term}"
export default uriTemplateGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const uriGenerator = () => "https://example.com/"
export default uriGenerator

View File

@@ -1,6 +0,0 @@
/**
* @prettier
*/
const uuidGenerator = () => "3fa85f64-5717-4562-b3fc-2c963f66afa6"
export default uuidGenerator

View File

@@ -1,13 +0,0 @@
/**
* @prettier
*/
export {
sampleFromSchema,
sampleFromSchemaGeneric,
createXMLExample,
memoizedSampleFromSchema,
memoizedCreateXMLExample,
} from "./main"
export { default as encoderAPI } from "./api/encoderAPI"
export { default as formatAPI } from "./api/formatAPI"
export { default as mediaTypeAPI } from "./api/mediaTypeAPI"

View File

@@ -1,521 +0,0 @@
/**
* @prettier
*/
import XML from "xml"
import isEmpty from "lodash/isEmpty"
import isPlainObject from "lodash/isPlainObject"
import { objectify, normalizeArray } from "core/utils"
import memoizeN from "core/utils/memoizeN"
import typeMap from "./types/index"
import { getType } from "./core/type"
import { typeCast } from "./core/utils"
import { hasExample, extractExample } from "./core/example"
import { pick as randomPick } from "./core/random"
import merge from "./core/merge"
import { isBooleanJSONSchema, isJSONSchemaObject } from "./core/predicates"
export const sampleFromSchemaGeneric = (
schema,
config = {},
exampleOverride = undefined,
respectXML = false
) => {
if (typeof schema?.toJS === "function") schema = schema.toJS()
schema = typeCast(schema)
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 && Array.isArray(schema.oneOf) && schema.oneOf.length > 0
const hasAnyOf =
!usePlainValue && Array.isArray(schema.anyOf) && schema.anyOf.length > 0
if (!usePlainValue && (hasOneOf || hasAnyOf)) {
const schemaToAdd = typeCast(
hasOneOf ? randomPick(schema.oneOf) : randomPick(schema.anyOf)
)
schema = merge(schema, schemaToAdd, config)
if (!schema.xml && schemaToAdd.xml) {
schema.xml = schemaToAdd.xml
}
if (hasExample(schema) && hasExample(schemaToAdd)) {
usePlainValue = true
}
}
const _attr = {}
let { xml, properties, additionalProperties, items, contains } = schema || {}
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"
// add prefix to name if exists
displayName = (prefix ? `${prefix}:` : "") + name
if (namespace) {
//add prefix to namespace if exists
let namespacePrefix = prefix ? `xmlns:${prefix}` : "xmlns"
_attr[namespacePrefix] = namespace
}
}
// init xml default response sample obj
if (respectXML) {
res[displayName] = []
}
// add to result helper init for xml or json
const props = objectify(properties)
let addPropertyToResult
let propertyAddedCounter = 0
const hasExceededMaxProperties = () =>
Number.isInteger(schema.maxProperties) &&
schema.maxProperties > 0 &&
propertyAddedCounter >= schema.maxProperties
const requiredPropertiesToAdd = () => {
if (!Array.isArray(schema.required) || schema.required.length === 0) {
return 0
}
let addedCount = 0
if (respectXML) {
schema.required.forEach(
(key) => (addedCount += res[key] === undefined ? 0 : 1)
)
} else {
schema.required.forEach((key) => {
addedCount +=
res[displayName]?.find((x) => x[key] !== undefined) === undefined
? 0
: 1
})
}
return schema.required.length - addedCount
}
const isOptionalProperty = (propName) => {
if (!Array.isArray(schema.required)) return true
if (schema.required.length === 0) return true
return !schema.required.includes(propName)
}
const canAddProperty = (propName) => {
if (!(Number.isInteger(schema.maxProperties) && schema.maxProperties > 0)) {
return true
}
if (hasExceededMaxProperties()) {
return false
}
if (!isOptionalProperty(propName)) {
return true
}
return (
schema.maxProperties - propertyAddedCounter - requiredPropertiesToAdd() >
0
)
}
if (respectXML) {
addPropertyToResult = (propName, overrideE = undefined) => {
if (schema && props[propName]) {
// case it is a xml attribute
props[propName].xml = props[propName].xml || {}
if (props[propName].xml.attribute) {
const enumAttrVal = Array.isArray(props[propName].enum)
? randomPick(props[propName].enum)
: undefined
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 {
const propSchema = typeCast(props[propName])
const propSchemaType = getType(propSchema)
const attrName = props[propName].xml.name || propName
_attr[attrName] = typeMap[propSchemaType](propSchema)
}
return
}
props[propName].xml.name = props[propName].xml.name || propName
} else if (!props[propName] && additionalProperties !== false) {
// case only additionalProperty that is not defined in schema
props[propName] = {
xml: {
name: propName,
},
}
}
let t = sampleFromSchemaGeneric(
props[propName],
config,
overrideE,
respectXML
)
if (!canAddProperty(propName)) {
return
}
propertyAddedCounter++
if (Array.isArray(t)) {
res[displayName] = res[displayName].concat(t)
} else {
res[displayName].push(t)
}
}
} else {
addPropertyToResult = (propName, overrideE) => {
if (!canAddProperty(propName)) {
return
}
if (
isPlainObject(schema.discriminator?.mapping) &&
schema.discriminator.propertyName === propName &&
typeof schema.$$ref === "string"
) {
for (const pair in schema.discriminator.mapping) {
if (schema.$$ref.search(schema.discriminator.mapping[pair]) !== -1) {
res[propName] = pair
break
}
}
} else {
res[propName] = sampleFromSchemaGeneric(
props[propName],
config,
overrideE,
respectXML
)
}
propertyAddedCounter++
}
}
// check for plain value and if found use it to generate sample from it
if (usePlainValue) {
let sample
if (exampleOverride !== undefined) {
sample = exampleOverride
} else {
sample = extractExample(schema)
}
// if json just return
if (!respectXML) {
// spacial case yaml parser can not know about
if (typeof sample === "number" && type === "string") {
return `${sample}`
}
// return if sample does not need any parsing
if (typeof sample !== "string" || type === "string") {
return sample
}
// check if sample is parsable or just a plain string
try {
return JSON.parse(sample)
} catch {
// sample is just plain string return it
return sample
}
}
// generate xml sample recursively for array case
if (type === "array") {
if (!Array.isArray(sample)) {
if (typeof sample === "string") {
return sample
}
sample = [sample]
}
let itemSamples = []
if (isJSONSchemaObject(items)) {
items.xml = items.xml || xml || {}
items.xml.name = items.xml.name || xml.name
itemSamples = sample.map((s) =>
sampleFromSchemaGeneric(items, config, s, respectXML)
)
}
if (isJSONSchemaObject(contains)) {
contains.xml = contains.xml || xml || {}
contains.xml.name = contains.xml.name || xml.name
itemSamples = [
sampleFromSchemaGeneric(contains, config, undefined, respectXML),
...itemSamples,
]
}
itemSamples = typeMap.array(schema, { sample: itemSamples })
if (xml.wrapped) {
res[displayName] = itemSamples
if (!isEmpty(_attr)) {
res[displayName].push({ _attr: _attr })
}
} else {
res = itemSamples
}
return res
}
// generate xml sample recursively for object case
if (type === "object") {
// case literal example
if (typeof sample === "string") {
return sample
}
for (const propName in sample) {
if (!Object.hasOwn(sample, propName)) {
continue
}
if (props[propName]?.readOnly && !includeReadOnly) {
continue
}
if (props[propName]?.writeOnly && !includeWriteOnly) {
continue
}
if (props[propName]?.xml?.attribute) {
_attr[props[propName].xml.name || propName] = sample[propName]
continue
}
addPropertyToResult(propName, sample[propName])
}
if (!isEmpty(_attr)) {
res[displayName].push({ _attr: _attr })
}
return res
}
res[displayName] = !isEmpty(_attr) ? [{ _attr: _attr }, sample] : sample
return res
}
// use schema to generate sample
if (type === "array") {
let sampleArray = []
if (isJSONSchemaObject(contains)) {
if (respectXML) {
contains.xml = contains.xml || schema.xml || {}
contains.xml.name = contains.xml.name || xml.name
}
if (Array.isArray(contains.anyOf)) {
sampleArray.push(
...contains.anyOf.map((anyOfSchema) =>
sampleFromSchemaGeneric(
merge(anyOfSchema, contains, config),
config,
undefined,
respectXML
)
)
)
} else if (Array.isArray(contains.oneOf)) {
sampleArray.push(
...contains.oneOf.map((oneOfSchema) =>
sampleFromSchemaGeneric(
merge(oneOfSchema, contains, config),
config,
undefined,
respectXML
)
)
)
} else if (!respectXML || (respectXML && xml.wrapped)) {
sampleArray.push(
sampleFromSchemaGeneric(contains, config, undefined, respectXML)
)
} else {
return sampleFromSchemaGeneric(contains, config, undefined, respectXML)
}
}
if (isJSONSchemaObject(items)) {
if (respectXML) {
items.xml = items.xml || schema.xml || {}
items.xml.name = items.xml.name || xml.name
}
if (Array.isArray(items.anyOf)) {
sampleArray.push(
...items.anyOf.map((i) =>
sampleFromSchemaGeneric(
merge(i, items, config),
config,
undefined,
respectXML
)
)
)
} else if (Array.isArray(items.oneOf)) {
sampleArray.push(
...items.oneOf.map((i) =>
sampleFromSchemaGeneric(
merge(i, items, config),
config,
undefined,
respectXML
)
)
)
} else if (!respectXML || (respectXML && xml.wrapped)) {
sampleArray.push(
sampleFromSchemaGeneric(items, config, undefined, respectXML)
)
} else {
return sampleFromSchemaGeneric(items, config, undefined, respectXML)
}
}
sampleArray = typeMap.array(schema, { sample: sampleArray })
if (respectXML && xml.wrapped) {
res[displayName] = sampleArray
if (!isEmpty(_attr)) {
res[displayName].push({ _attr: _attr })
}
return res
}
return sampleArray
}
if (type === "object") {
for (let propName in props) {
if (!Object.hasOwn(props, propName)) {
continue
}
if (props[propName]?.deprecated) {
continue
}
if (props[propName]?.readOnly && !includeReadOnly) {
continue
}
if (props[propName]?.writeOnly && !includeWriteOnly) {
continue
}
addPropertyToResult(propName)
}
if (respectXML && _attr) {
res[displayName].push({ _attr: _attr })
}
if (hasExceededMaxProperties()) {
return res
}
if (isBooleanJSONSchema(additionalProperties) && additionalProperties) {
if (respectXML) {
res[displayName].push({ additionalProp: "Anything can be here" })
} else {
res.additionalProp1 = {}
}
propertyAddedCounter++
} else if (isJSONSchemaObject(additionalProperties)) {
const additionalProps = additionalProperties
const additionalPropSample = sampleFromSchemaGeneric(
additionalProps,
config,
undefined,
respectXML
)
if (
respectXML &&
typeof additionalProps?.xml?.name === "string" &&
additionalProps?.xml?.name !== "notagname"
) {
res[displayName].push(additionalPropSample)
} else {
const toGenerateCount =
Number.isInteger(schema.minProperties) &&
schema.minProperties > 0 &&
propertyAddedCounter < schema.minProperties
? schema.minProperties - propertyAddedCounter
: 3
for (let i = 1; i <= toGenerateCount; i++) {
if (hasExceededMaxProperties()) {
return res
}
if (respectXML) {
const temp = {}
temp["additionalProp" + i] = additionalPropSample["notagname"]
res[displayName].push(temp)
} else {
res["additionalProp" + i] = additionalPropSample
}
propertyAddedCounter++
}
}
}
return res
}
let value
if (typeof schema.const !== "undefined") {
// display const value
value = schema.const
} else if (schema && Array.isArray(schema.enum)) {
//display enum first value
value = randomPick(normalizeArray(schema.enum))
} else {
// display schema default
const contentSample = isJSONSchemaObject(schema.contentSchema)
? sampleFromSchemaGeneric(
schema.contentSchema,
config,
undefined,
respectXML
)
: undefined
value = typeMap[type](schema, { sample: contentSample })
}
if (respectXML) {
res[displayName] = !isEmpty(_attr) ? [{ _attr: _attr }, value] : value
return res
}
return value
}
export const createXMLExample = (schema, config, o) => {
const json = sampleFromSchemaGeneric(schema, config, o, true)
if (!json) {
return
}
if (typeof json === "string") {
return json
}
return XML(json, { declaration: true, indent: "\t" })
}
export const sampleFromSchema = (schema, config, o) => {
return sampleFromSchemaGeneric(schema, config, o, false)
}
const resolver = (arg1, arg2, arg3) => [
arg1,
JSON.stringify(arg2),
JSON.stringify(arg3),
]
export const memoizedCreateXMLExample = memoizeN(createXMLExample, resolver)
export const memoizedSampleFromSchema = memoizeN(sampleFromSchema, resolver)

View File

@@ -1,52 +0,0 @@
/**
* @prettier
*/
export const applyArrayConstraints = (array, constraints = {}) => {
const { minItems, maxItems, uniqueItems } = constraints
const { contains, minContains, maxContains } = constraints
let constrainedArray = [...array]
if (contains != null && typeof contains === "object") {
if (Number.isInteger(minContains) && minContains > 1) {
const containsItem = constrainedArray.at(0)
for (let i = 1; i < minContains; i += 1) {
constrainedArray.unshift(containsItem)
}
}
if (Number.isInteger(maxContains) && maxContains > 0) {
/**
* This is noop. `minContains` already generate minimum required
* number of items that satisfies `contains`. `maxContains` would
* have no effect.
*/
}
}
if (Number.isInteger(maxItems) && maxItems > 0) {
constrainedArray = array.slice(0, maxItems)
}
if (Number.isInteger(minItems) && minItems > 0) {
for (let i = 0; constrainedArray.length < minItems; i += 1) {
constrainedArray.push(constrainedArray[i % constrainedArray.length])
}
}
if (uniqueItems === true) {
/**
* If uniqueItems is true, it implies that every item in the array must be unique.
* This overrides any minItems constraint that cannot be satisfied with unique items.
* So if minItems is greater than the number of unique items,
* it should be reduced to the number of unique items.
*/
constrainedArray = Array.from(new Set(constrainedArray))
}
return constrainedArray
}
const arrayType = (schema, { sample }) => {
return applyArrayConstraints(sample, schema)
}
export default arrayType

View File

@@ -1,9 +0,0 @@
/**
* @prettier
*/
const booleanType = (schema) => {
return typeof schema.default === "boolean" ? schema.default : true
}
export default booleanType

View File

@@ -1,30 +0,0 @@
/**
* @prettier
*/
import arrayType from "./array"
import objectType from "./object"
import stringType from "./string"
import numberType from "./number"
import integerType from "./integer"
import booleanType from "./boolean"
import nullType from "./null"
const typeMap = {
array: arrayType,
object: objectType,
string: stringType,
number: numberType,
integer: integerType,
boolean: booleanType,
null: nullType,
}
export default new Proxy(typeMap, {
get(target, prop) {
if (typeof prop === "string" && Object.hasOwn(target, prop)) {
return target[prop]
}
return () => `Unknown Type: ${prop}`
},
})

View File

@@ -1,38 +0,0 @@
/**
* @prettier
*/
import { integer as randomInteger } from "../core/random"
import formatAPI from "../api/formatAPI"
import int32Generator from "../generators/int32"
import int64Generator from "../generators/int64"
const generateFormat = (schema) => {
const { format } = schema
const formatGenerator = formatAPI(format)
if (typeof formatGenerator === "function") {
return formatGenerator(schema)
}
switch (format) {
case "int32": {
return int32Generator()
}
case "int64": {
return int64Generator()
}
}
return randomInteger()
}
const integerType = (schema) => {
const { format } = schema
if (typeof format === "string") {
return generateFormat(schema)
}
return randomInteger()
}
export default integerType

View File

@@ -1,9 +0,0 @@
/**
* @prettier
*/
const nullType = () => {
return null
}
export default nullType

View File

@@ -1,76 +0,0 @@
/**
* @prettier
*/
import { number as randomNumber } from "../core/random"
import formatAPI from "../api/formatAPI"
import floatGenerator from "../generators/float"
import doubleGenerator from "../generators/double"
const generateFormat = (schema) => {
const { format } = schema
const formatGenerator = formatAPI(format)
if (typeof formatGenerator === "function") {
return formatGenerator(schema)
}
switch (format) {
case "float": {
return floatGenerator()
}
case "double": {
return doubleGenerator()
}
}
return randomNumber()
}
const applyNumberConstraints = (number, constraints = {}) => {
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = constraints
const { multipleOf } = constraints
const epsilon = Number.isInteger(number) ? 1 : Number.EPSILON
let minValue = typeof minimum === "number" ? minimum : null
let maxValue = typeof maximum === "number" ? maximum : null
let constrainedNumber = number
if (typeof exclusiveMinimum === "number") {
minValue =
minValue !== null
? Math.max(minValue, exclusiveMinimum + epsilon)
: exclusiveMinimum + epsilon
}
if (typeof exclusiveMaximum === "number") {
maxValue =
maxValue !== null
? Math.min(maxValue, exclusiveMaximum - epsilon)
: exclusiveMaximum - epsilon
}
constrainedNumber =
(minValue > maxValue && number) || minValue || maxValue || constrainedNumber
if (typeof multipleOf === "number" && multipleOf > 0) {
const remainder = constrainedNumber % multipleOf
constrainedNumber =
remainder === 0
? constrainedNumber
: constrainedNumber + multipleOf - remainder
}
return constrainedNumber
}
const numberType = (schema) => {
const { format } = schema
let generatedNumber
if (typeof format === "string") {
generatedNumber = generateFormat(schema)
} else {
generatedNumber = randomNumber()
}
return applyNumberConstraints(generatedNumber, schema)
}
export default numberType

View File

@@ -1,9 +0,0 @@
/**
* @prettier
*/
const objectType = () => {
throw new Error("Not implemented")
}
export default objectType

View File

@@ -1,154 +0,0 @@
/**
* @prettier
*/
import identity from "lodash/identity"
import { string as randomString, randexp } from "../core/random"
import { isJSONSchema } from "../core/predicates"
import emailGenerator from "../generators/email"
import idnEmailGenerator from "../generators/idn-email"
import hostnameGenerator from "../generators/hostname"
import idnHostnameGenerator from "../generators/idn-hostname"
import ipv4Generator from "../generators/ipv4"
import ipv6Generator from "../generators/ipv6"
import uriGenerator from "../generators/uri"
import uriReferenceGenerator from "../generators/uri-reference"
import iriGenerator from "../generators/iri"
import iriReferenceGenerator from "../generators/iri-reference"
import uuidGenerator from "../generators/uuid"
import uriTemplateGenerator from "../generators/uri-template"
import jsonPointerGenerator from "../generators/json-pointer"
import relativeJsonPointerGenerator from "../generators/relative-json-pointer"
import dateTimeGenerator from "../generators/date-time"
import dateGenerator from "../generators/date"
import timeGenerator from "../generators/time"
import durationGenerator from "../generators/duration"
import passwordGenerator from "../generators/password"
import regexGenerator from "../generators/regex"
import formatAPI from "../api/formatAPI"
import encoderAPI from "../api/encoderAPI"
import mediaTypeAPI from "../api/mediaTypeAPI"
const generateFormat = (schema) => {
const { format } = schema
const formatGenerator = formatAPI(format)
if (typeof formatGenerator === "function") {
return formatGenerator(schema)
}
switch (format) {
case "email": {
return emailGenerator()
}
case "idn-email": {
return idnEmailGenerator()
}
case "hostname": {
return hostnameGenerator()
}
case "idn-hostname": {
return idnHostnameGenerator()
}
case "ipv4": {
return ipv4Generator()
}
case "ipv6": {
return ipv6Generator()
}
case "uri": {
return uriGenerator()
}
case "uri-reference": {
return uriReferenceGenerator()
}
case "iri": {
return iriGenerator()
}
case "iri-reference": {
return iriReferenceGenerator()
}
case "uuid": {
return uuidGenerator()
}
case "uri-template": {
return uriTemplateGenerator()
}
case "json-pointer": {
return jsonPointerGenerator()
}
case "relative-json-pointer": {
return relativeJsonPointerGenerator()
}
case "date-time": {
return dateTimeGenerator()
}
case "date": {
return dateGenerator()
}
case "time": {
return timeGenerator()
}
case "duration": {
return durationGenerator()
}
case "password": {
return passwordGenerator()
}
case "regex": {
return regexGenerator()
}
}
return randomString()
}
const applyStringConstraints = (string, constraints = {}) => {
const { maxLength, minLength } = constraints
let constrainedString = string
if (Number.isInteger(maxLength) && maxLength > 0) {
constrainedString = constrainedString.slice(0, maxLength)
}
if (Number.isInteger(minLength) && minLength > 0) {
let i = 0
while (constrainedString.length < minLength) {
constrainedString += constrainedString[i++ % constrainedString.length]
}
}
return constrainedString
}
const stringType = (schema, { sample } = {}) => {
const { contentEncoding, contentMediaType, contentSchema } = schema
const { pattern, format } = schema
const encode = encoderAPI(contentEncoding) || identity
let generatedString
if (typeof pattern === "string") {
generatedString = randexp(pattern)
} else if (typeof format === "string") {
generatedString = generateFormat(schema)
} else if (
isJSONSchema(contentSchema) &&
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 === "string") {
const mediaTypeGenerator = mediaTypeAPI(contentMediaType)
if (typeof mediaTypeGenerator === "function") {
generatedString = mediaTypeGenerator(schema)
}
} else {
generatedString = randomString()
}
return encode(applyStringConstraints(generatedString, schema))
}
export default stringType