feat(samples): add support for custom formats, encoders and media types (#8905)

This change is specific to JSON Schema 2020-12
and OpenAPI 3.1.0.

Refs #8577
This commit is contained in:
Vladimír Gorej
2023-06-09 15:34:39 +02:00
committed by GitHub
parent 1925622113
commit d72b72c5c6
55 changed files with 877 additions and 371 deletions

View File

@@ -50,7 +50,10 @@ import {
createXMLExample,
memoizedSampleFromSchema,
memoizedCreateXMLExample,
} from "./samples-extensions/fn"
encoderAPI,
mediaTypeAPI,
formatAPI,
} from "./samples-extensions/fn/index"
import { JSONSchemaDeepExpansionContext } from "./context"
import { useFn, useConfig, useComponent, useIsExpandedDeeply } from "./hooks"
import { withJSONSchemaContext } from "./hoc"
@@ -113,6 +116,9 @@ const JSONSchema202012Plugin = () => ({
useIsExpandedDeeply,
sampleFromSchema,
sampleFromSchemaGeneric,
sampleEncoderAPI: encoderAPI,
sampleFormatAPI: formatAPI,
sampleMediaTypeAPI: mediaTypeAPI,
createXMLExample,
memoizedSampleFromSchema,
memoizedCreateXMLExample,

View File

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

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

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

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

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

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

@@ -0,0 +1,22 @@
/**
* @prettier
*/
import randomBytes from "randombytes"
import RandExp from "randexp"
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 string = () => "string"
export const number = () => 0
export const integer = () => 0

View File

@@ -0,0 +1,10 @@
/**
* @prettier
*/
export const isURI = (uri) => {
try {
return new URL(uri) && true
} catch {
return false
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -2,378 +2,23 @@
* @prettier
*/
import XML from "xml"
import RandExp from "randexp"
import isEmpty from "lodash/isEmpty"
import randomBytes from "randombytes"
import { objectify, isFunc, normalizeArray, deeplyStripKey } from "core/utils"
import memoizeN from "../../../../helpers/memoizeN"
const twentyFiveRandomBytesString = randomBytes(25).toString("binary")
const stringFromRegex = (pattern) => {
try {
const randexp = new RandExp(pattern)
return randexp.gen()
} catch {
// invalid regex should not cause a crash (regex syntax varies across languages)
return "string"
}
}
const contentEncodings = {
"7bit": (content) => Buffer.from(content).toString("ascii"),
"8bit": (content) => Buffer.from(content).toString("utf8"),
binary: (content) => Buffer.from(content).toString("binary"),
"quoted-printable": (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
},
base16: (content) => Buffer.from(content).toString("hex"),
base32: (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
},
base64: (content) => Buffer.from(content).toString("base64"),
}
const encodeContent = (content, encoding) => {
if (typeof contentEncodings[encoding] === "function") {
return contentEncodings[encoding](content)
}
return content
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const contentMediaTypes = {
// text media type subtypes
"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",
// image media type subtypes
"image/*": () => twentyFiveRandomBytesString,
// audio media type subtypes
"audio/*": () => twentyFiveRandomBytesString,
// video media type subtypes
"video/*": () => twentyFiveRandomBytesString,
// application media type subtypes
"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/*": () => twentyFiveRandomBytesString,
}
const contentFromMediaType = (mediaType) => {
const mediaTypeNoParams = mediaType.split(";").at(0)
const topLevelMediaType = `${mediaTypeNoParams.split("/").at(0)}/*`
if (typeof contentMediaTypes[mediaTypeNoParams] === "function") {
return contentMediaTypes[mediaTypeNoParams]()
}
if (typeof contentMediaTypes[topLevelMediaType] === "function") {
return contentMediaTypes[topLevelMediaType]()
}
return "string"
}
/* eslint-disable camelcase */
const primitives = {
string: (schema) => {
const { pattern, contentEncoding, contentMediaType } = schema
const content =
typeof pattern === "string"
? stringFromRegex(pattern)
: typeof contentMediaType === "string"
? contentFromMediaType(contentMediaType)
: "string"
return encodeContent(content, contentEncoding)
},
string_email: (schema) => {
const { contentEncoding } = schema
const content = "user@example.com"
return encodeContent(content, contentEncoding)
},
"string_idn-email": (schema) => {
const { contentEncoding } = schema
const content = "실례@example.com"
return encodeContent(content, contentEncoding)
},
string_hostname: (schema) => {
const { contentEncoding } = schema
const content = "example.com"
return encodeContent(content, contentEncoding)
},
"string_idn-hostname": (schema) => {
const { contentEncoding } = schema
const content = "실례.com"
return encodeContent(content, contentEncoding)
},
string_ipv4: (schema) => {
const { contentEncoding } = schema
const content = "198.51.100.42"
return encodeContent(content, contentEncoding)
},
string_ipv6: (schema) => {
const { contentEncoding } = schema
const content = "2001:0db8:5b96:0000:0000:426f:8e17:642a"
return encodeContent(content, contentEncoding)
},
string_uri: (schema) => {
const { contentEncoding } = schema
const content = "https://example.com/"
return encodeContent(content, contentEncoding)
},
"string_uri-reference": (schema) => {
const { contentEncoding } = schema
const content = "path/index.html"
return encodeContent(content, contentEncoding)
},
string_iri: (schema) => {
const { contentEncoding } = schema
const content = "https://실례.com/"
return encodeContent(content, contentEncoding)
},
"string_iri-reference": (schema) => {
const { contentEncoding } = schema
const content = "path/실례.html"
return encodeContent(content, contentEncoding)
},
string_uuid: (schema) => {
const { contentEncoding } = schema
const content = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
return encodeContent(content, contentEncoding)
},
"string_uri-template": (schema) => {
const { contentEncoding } = schema
const content = "https://example.com/dictionary/{term:1}/{term}"
return encodeContent(content, contentEncoding)
},
"string_json-pointer": (schema) => {
const { contentEncoding } = schema
const content = "/a/b/c"
return encodeContent(content, contentEncoding)
},
"string_relative-json-pointer": (schema) => {
const { contentEncoding } = schema
const content = "1/0"
return encodeContent(content, contentEncoding)
},
"string_date-time": (schema) => {
const { contentEncoding } = schema
const content = new Date().toISOString()
return encodeContent(content, contentEncoding)
},
string_date: (schema) => {
const { contentEncoding } = schema
const content = new Date().toISOString().substring(0, 10)
return encodeContent(content, contentEncoding)
},
string_time: (schema) => {
const { contentEncoding } = schema
const content = new Date().toISOString().substring(11)
return encodeContent(content, contentEncoding)
},
string_duration: (schema) => {
const { contentEncoding } = schema
const content = "P3D" // expresses a duration of 3 days
return encodeContent(content, contentEncoding)
},
string_password: (schema) => {
const { contentEncoding } = schema
const content = "********"
return encodeContent(content, contentEncoding)
},
string_regex: (schema) => {
const { contentEncoding } = schema
const content = "^[a-z]+$"
return encodeContent(content, contentEncoding)
},
number: () => 0,
number_float: () => 0.1,
number_double: () => 0.1,
integer: () => 0,
integer_int32: () => (2 ** 30) >>> 0,
integer_int64: () => 2 ** 53 - 1,
boolean: (schema) =>
typeof schema.default === "boolean" ? schema.default : true,
null: () => null,
}
/* eslint-enable camelcase */
import memoizeN from "../../../../../helpers/memoizeN"
import typeMap from "./types/index"
import { isURI } from "./core/utils"
const primitive = (schema) => {
schema = objectify(schema)
const { type: typeList, format } = schema
const { type: typeList } = schema
const type = Array.isArray(typeList) ? typeList.at(0) : typeList
const fn = primitives[`${type}_${format}`] || primitives[type]
return typeof fn === "function" ? fn(schema) : `Unknown Type: ${schema.type}`
}
const isURI = (uri) => {
try {
return new URL(uri) && true
} catch {
return false
}
}
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 (Object.hasOwn(typeMap, type)) {
return typeMap[type](schema)
}
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 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 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
return `Unknown Type: ${type}`
}
/**
@@ -823,7 +468,7 @@ export const sampleFromSchemaGeneric = (
]
}
itemSamples = applyArrayConstraints(itemSamples, schema)
itemSamples = typeMap.array(schema, itemSamples)
if (xml.wrapped) {
res[displayName] = itemSamples
if (!isEmpty(_attr)) {
@@ -961,7 +606,7 @@ export const sampleFromSchemaGeneric = (
}
}
sampleArray = applyArrayConstraints(sampleArray, schema)
sampleArray = typeMap.array(schema, sampleArray)
if (respectXML && xml.wrapped) {
res[displayName] = sampleArray
if (!isEmpty(_attr)) {
@@ -1055,12 +700,6 @@ export const sampleFromSchemaGeneric = (
} else if (schema) {
// display schema default
value = primitive(schema)
if (typeof value === "number") {
value = applyNumberConstraints(value, schema)
}
if (typeof value === "string") {
value = applyStringConstraints(value, schema)
}
} else {
return
}

View File

@@ -0,0 +1,52 @@
/**
* @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, sampleArray) => {
return applyArrayConstraints(sampleArray, schema)
}
export default arrayType

View File

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

View File

@@ -0,0 +1,22 @@
/**
* @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 typeMap

View File

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

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

View File

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

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

View File

@@ -0,0 +1,142 @@
/**
* @prettier
*/
import identity from "lodash/identity"
import { string as randomString, randexp } from "../core/random"
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) => {
const { pattern, format, contentEncoding, contentMediaType } = 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 (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