mirror of
https://github.com/Redocly/redoc.git
synced 2025-10-25 21:11:03 +03:00
522 lines
17 KiB
TypeScript
522 lines
17 KiB
TypeScript
import { MergedOpenAPISchema } from '../services';
|
|
import { OpenAPISchema } from '../types';
|
|
import { json2xml } from './jsonToXml';
|
|
|
|
export interface ConfigAccessOptions {
|
|
includeReadOnly?: boolean;
|
|
includeWriteOnly?: boolean;
|
|
}
|
|
|
|
export interface ExampleConfig extends ConfigAccessOptions {
|
|
includeDeprecated?: boolean;
|
|
useXmlTagForProp?: boolean;
|
|
}
|
|
|
|
export interface GenerateExampleProps extends ConfigAccessOptions {
|
|
schema: MergedOpenAPISchema | undefined;
|
|
}
|
|
|
|
export interface FinalExamples {
|
|
exampleDescription: string;
|
|
exampleId: string;
|
|
exampleSummary: string;
|
|
exampleValue: string;
|
|
}
|
|
|
|
const mergePropertyExamples = (
|
|
obj: { [x: string]: any },
|
|
propertyName: string,
|
|
propExamples: never[],
|
|
) => {
|
|
// Create an example for each variant of the propertyExample, merging them with the current (parent) example
|
|
let i = 0;
|
|
const maxCombinations = 10;
|
|
const mergedObj = {};
|
|
for (const exampleKey in obj) {
|
|
for (const propExampleKey in propExamples) {
|
|
const exampleId = getExampleId(i + 1);
|
|
mergedObj[exampleId] = { ...obj[exampleKey] };
|
|
mergedObj[exampleId][propertyName] = propExamples[propExampleKey];
|
|
i++;
|
|
if (i >= maxCombinations) {
|
|
break;
|
|
}
|
|
}
|
|
if (i >= maxCombinations) {
|
|
break;
|
|
}
|
|
}
|
|
return mergedObj;
|
|
};
|
|
|
|
const addSchemaInfoToExample = (schema: OpenAPISchema, obj: any) => {
|
|
if (typeof obj !== 'object' || obj === null) {
|
|
return;
|
|
}
|
|
if (schema.title) {
|
|
obj['::TITLE'] = schema.title;
|
|
}
|
|
if (schema.description) {
|
|
obj['::DESCRIPTION'] = schema.description;
|
|
}
|
|
if (schema.xml?.name) {
|
|
obj['::XML_TAG'] = schema.xml?.name;
|
|
}
|
|
if (schema.xml?.wrapped) {
|
|
obj['::XML_WRAP'] = schema.xml?.wrapped.toString();
|
|
}
|
|
};
|
|
|
|
const addPropertyExampleToObjectExamples = (example: any, obj: object, propertyKey: string) => {
|
|
for (const key in obj) {
|
|
obj[key][propertyKey] = example;
|
|
}
|
|
};
|
|
|
|
const getSampleValueByType = (schemaObj: OpenAPISchema) => {
|
|
const example = schemaObj.examples
|
|
? schemaObj.examples[0]
|
|
: schemaObj.example === null
|
|
? null
|
|
: schemaObj.example || undefined;
|
|
if (example === '') {
|
|
return '';
|
|
}
|
|
if (example === null) {
|
|
return null;
|
|
}
|
|
if (example === 0) {
|
|
return 0;
|
|
}
|
|
if (example === false) {
|
|
return false;
|
|
}
|
|
if (example instanceof Date) {
|
|
switch (schemaObj.format?.toLowerCase()) {
|
|
case 'date':
|
|
return example.toISOString().split('T')[0];
|
|
case 'time':
|
|
return example.toISOString().split('T')[1];
|
|
default:
|
|
return example.toISOString();
|
|
}
|
|
}
|
|
if (example) {
|
|
return example;
|
|
}
|
|
|
|
if (Object.keys(schemaObj).length === 0) {
|
|
return null;
|
|
}
|
|
if (schemaObj.$ref) {
|
|
// Indicates a Circular ref
|
|
return schemaObj.$ref;
|
|
}
|
|
if (schemaObj.const === null || schemaObj.const === '') {
|
|
return schemaObj.const;
|
|
}
|
|
if (schemaObj.const) {
|
|
return schemaObj.const;
|
|
}
|
|
const typeValue = Array.isArray(schemaObj.type) ? schemaObj.type[0] : schemaObj.type;
|
|
if (!typeValue) {
|
|
return '?';
|
|
}
|
|
if (typeValue.match(/^integer|^number/g)) {
|
|
const multipleOf = Number.isNaN(Number(schemaObj.multipleOf))
|
|
? undefined
|
|
: Number(schemaObj.multipleOf);
|
|
const maximum = Number.isNaN(Number(schemaObj.maximum)) ? undefined : Number(schemaObj.maximum);
|
|
const minimumPossibleVal = Number.isNaN(Number(schemaObj.minimum))
|
|
? Number.isNaN(Number(schemaObj.exclusiveMinimum))
|
|
? maximum || 0
|
|
: Number(schemaObj.exclusiveMinimum) + (typeValue.startsWith('integer') ? 1 : 0.001)
|
|
: Number(schemaObj.minimum);
|
|
return multipleOf
|
|
? multipleOf >= minimumPossibleVal
|
|
? multipleOf
|
|
: minimumPossibleVal % multipleOf === 0
|
|
? minimumPossibleVal
|
|
: Math.ceil(minimumPossibleVal / multipleOf) * multipleOf
|
|
: minimumPossibleVal;
|
|
}
|
|
if (typeValue.match(/^boolean/g)) {
|
|
return false;
|
|
}
|
|
if (typeValue.match(/^null/g)) {
|
|
return null;
|
|
}
|
|
if (typeValue.match(/^string/g)) {
|
|
if (schemaObj.enum) {
|
|
return schemaObj.enum[0];
|
|
}
|
|
if (schemaObj.const) {
|
|
return schemaObj.const;
|
|
}
|
|
if (schemaObj.pattern) {
|
|
return schemaObj.pattern;
|
|
}
|
|
if (schemaObj.format) {
|
|
const u = `${Date.now().toString(16)}${Math.random().toString(16)}0`.repeat(16);
|
|
switch (schemaObj.format.toLowerCase()) {
|
|
case 'url':
|
|
case 'uri':
|
|
return 'http://example.com';
|
|
case 'date':
|
|
return new Date(0).toISOString().split('T')[0];
|
|
case 'time':
|
|
return new Date(0).toISOString().split('T')[1];
|
|
case 'date-time':
|
|
return new Date(0).toISOString();
|
|
case 'duration':
|
|
return 'P3Y6M4DT12H30M5S'; // P=Period 3-Years 6-Months 4-Days 12-Hours 30-Minutes 5-Seconds
|
|
case 'email':
|
|
case 'idn-email':
|
|
return 'user@example.com';
|
|
case 'hostname':
|
|
case 'idn-hostname':
|
|
return 'www.example.com';
|
|
case 'ipv4':
|
|
return '198.51.100.42';
|
|
case 'ipv6':
|
|
return '2001:0db8:5b96:0000:0000:426f:8e17:642a';
|
|
case 'uuid':
|
|
return [
|
|
u.substr(0, 8),
|
|
u.substr(8, 4),
|
|
`4000-8${u.substr(13, 3)}`,
|
|
u.substr(16, 12),
|
|
].join('-');
|
|
case 'byte':
|
|
return 'ZXhhbXBsZQ=='; // 'example' base64 encoded. See https://spec.openapis.org/oas/v3.0.0#data-types
|
|
default:
|
|
return '';
|
|
}
|
|
} else {
|
|
const minLength = Number.isNaN(schemaObj.minLength) ? undefined : Number(schemaObj.minLength);
|
|
const maxLength = Number.isNaN(schemaObj.maxLength) ? undefined : Number(schemaObj.maxLength);
|
|
const finalLength = minLength || (maxLength && maxLength > 6 ? 6 : maxLength || undefined);
|
|
return finalLength ? 'A'.repeat(finalLength) : 'string';
|
|
}
|
|
}
|
|
// If type cannot be determined
|
|
return '?';
|
|
};
|
|
|
|
const getExampleId = (id: number = 1): string => `Example ${id}`;
|
|
|
|
/* For changing JSON-Schema to a Sample Object, as per the schema (to generate examples based on schema) */
|
|
const schemaToSampleObj = (schema: OpenAPISchema, config: ExampleConfig = {}) => {
|
|
let obj = {};
|
|
if (!schema) {
|
|
return;
|
|
}
|
|
const defaultExampleId = getExampleId();
|
|
if (schema.allOf) {
|
|
const objWithAllProps = {};
|
|
|
|
if (schema.allOf.length === 1 && !schema.allOf[0]?.properties && !schema.allOf[0]?.items) {
|
|
// If allOf has single item and the type is not an object or array, then its a primitive
|
|
if (schema.allOf[0].$ref) {
|
|
return '{ }';
|
|
}
|
|
if (schema.allOf[0].readOnly && config.includeReadOnly) {
|
|
const tempSchema = schema.allOf[0];
|
|
return getSampleValueByType(tempSchema);
|
|
}
|
|
return;
|
|
}
|
|
|
|
schema.allOf.forEach(v => {
|
|
if (v.type === 'object' || v.properties || v.allOf || v.anyOf || v.oneOf) {
|
|
const partialObj = schemaToSampleObj(v, config);
|
|
Object.assign(objWithAllProps, partialObj);
|
|
} else if (v.type === 'array' || v.items) {
|
|
const partialObj = [schemaToSampleObj(v, config)];
|
|
Object.assign(objWithAllProps, partialObj);
|
|
} else if (v.type) {
|
|
const prop = `prop${Object.keys(objWithAllProps).length}`;
|
|
objWithAllProps[prop] = getSampleValueByType(v);
|
|
} else {
|
|
return '';
|
|
}
|
|
});
|
|
|
|
obj = objWithAllProps;
|
|
} else if (schema.oneOf) {
|
|
// 1. First create example with scheme.properties
|
|
const objWithSchemaProps = {};
|
|
if (schema.properties) {
|
|
for (const propertyName in schema.properties) {
|
|
if (
|
|
schema.properties[propertyName].properties ||
|
|
schema.properties[propertyName].properties?.items
|
|
) {
|
|
objWithSchemaProps[propertyName] = schemaToSampleObj(
|
|
schema.properties[propertyName],
|
|
config,
|
|
);
|
|
} else {
|
|
objWithSchemaProps[propertyName] = getSampleValueByType(schema.properties[propertyName]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (schema.oneOf.length > 0) {
|
|
/**
|
|
* @example
|
|
* oneOf:
|
|
* - type: object
|
|
* properties:
|
|
* option1_PropA:
|
|
* type: string
|
|
* option1_PropB:
|
|
* type: string
|
|
* - type: object
|
|
* properties:
|
|
* option2_PropX:
|
|
* type: string
|
|
* properties:
|
|
* prop1:
|
|
* type: string
|
|
* prop2:
|
|
* type: string
|
|
* minLength: 10
|
|
*
|
|
* The above Schema should generate the following 2 examples
|
|
*
|
|
* Example 1
|
|
* {
|
|
* prop1: 'string',
|
|
* prop2: 'AAAAAAAAAA', <-- min-length 10
|
|
* option1_PropA: 'string',
|
|
* option1_PropB: 'string'
|
|
* }
|
|
*
|
|
* Example 2
|
|
* {
|
|
* prop1: 'string',
|
|
* prop2: 'AAAAAAAAAA', <-- min-length 10
|
|
* option2_PropX: 'string'
|
|
* }
|
|
*/
|
|
let i = 0;
|
|
// Merge all examples of each oneOf-schema
|
|
for (const key in schema.oneOf) {
|
|
const oneOfSamples = schemaToSampleObj(schema.oneOf[key], config);
|
|
for (const sampleKey in oneOfSamples) {
|
|
const exampleId = getExampleId(i + 1);
|
|
// 2. In the final example include a one-of item along with properties
|
|
let finalExample;
|
|
if (Object.keys(objWithSchemaProps).length > 0) {
|
|
if (oneOfSamples[sampleKey] === null || typeof oneOfSamples[sampleKey] !== 'object') {
|
|
// This doesn't really make sense since every oneOf schema _should_ be an object if there are common properties, so we'll skip this
|
|
continue;
|
|
} else {
|
|
finalExample = Object.assign(oneOfSamples[sampleKey], objWithSchemaProps);
|
|
}
|
|
} else {
|
|
finalExample = oneOfSamples[sampleKey];
|
|
}
|
|
obj[exampleId] = finalExample;
|
|
addSchemaInfoToExample(schema.oneOf[key], obj[exampleId]);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
} else if (schema.anyOf) {
|
|
// First generate values for regular properties
|
|
let commonObj;
|
|
if (schema.type === 'object' || schema.properties) {
|
|
commonObj = { [defaultExampleId]: {} };
|
|
for (const propertyName in schema.properties) {
|
|
if (schema.example) {
|
|
commonObj = schema;
|
|
break;
|
|
}
|
|
if (schema.properties[propertyName].deprecated && !config.includeDeprecated) {
|
|
continue;
|
|
}
|
|
if (schema.properties[propertyName].readOnly && !config.includeReadOnly) {
|
|
continue;
|
|
}
|
|
if (schema.properties[propertyName].writeOnly && !config.includeWriteOnly) {
|
|
continue;
|
|
}
|
|
commonObj = mergePropertyExamples(
|
|
commonObj,
|
|
propertyName,
|
|
schemaToSampleObj(schema.properties[propertyName], config),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Combine every variant of the regular properties with every variant of the anyOf samples
|
|
let i = 0;
|
|
for (const key in schema.anyOf) {
|
|
const anyOfSamples = schemaToSampleObj(schema.anyOf[key], config);
|
|
for (const sampleKey in anyOfSamples) {
|
|
const exampleId = getExampleId(i + 1);
|
|
if (typeof commonObj !== 'undefined') {
|
|
for (const commonKey in commonObj) {
|
|
obj[exampleId] = { ...commonObj[commonKey], ...anyOfSamples[sampleKey] };
|
|
}
|
|
} else {
|
|
obj[exampleId] = anyOfSamples[sampleKey];
|
|
}
|
|
addSchemaInfoToExample(schema.anyOf[key], obj[exampleId]);
|
|
i++;
|
|
}
|
|
}
|
|
} else if (schema.type === 'object' || schema.properties) {
|
|
obj[defaultExampleId] = {};
|
|
addSchemaInfoToExample(schema, obj[defaultExampleId]);
|
|
if (schema.example) {
|
|
obj[defaultExampleId] = schema.example;
|
|
} else {
|
|
for (const propertyName in schema.properties) {
|
|
const prop = schema.properties[propertyName] as OpenAPISchema;
|
|
if (prop?.deprecated && !config.includeDeprecated) {
|
|
continue;
|
|
}
|
|
if (prop?.readOnly && !config.includeReadOnly) {
|
|
continue;
|
|
}
|
|
if (prop?.writeOnly && !config.includeWriteOnly) {
|
|
continue;
|
|
}
|
|
const propItems = prop?.items as OpenAPISchema;
|
|
if (prop?.type === 'array' || propItems) {
|
|
if (prop.example) {
|
|
addPropertyExampleToObjectExamples(prop.example, obj, propertyName);
|
|
} else if (propItems?.example) {
|
|
// schemas and properties support single example but not multiple examples.
|
|
addPropertyExampleToObjectExamples([propItems.example], obj, propertyName);
|
|
} else {
|
|
const itemSamples = schemaToSampleObj(
|
|
Array.isArray(propItems) ? { allOf: propItems } : propItems,
|
|
config,
|
|
);
|
|
if (config.useXmlTagForProp) {
|
|
const xmlTagName = prop.xml?.name || propertyName;
|
|
if (prop.xml?.wrapped) {
|
|
const wrappedItemSample = JSON.parse(
|
|
`{ "${xmlTagName}" : { "${xmlTagName}" : ${JSON.stringify(
|
|
itemSamples[defaultExampleId],
|
|
)} } }`,
|
|
);
|
|
obj = mergePropertyExamples(obj, xmlTagName, wrappedItemSample);
|
|
} else {
|
|
obj = mergePropertyExamples(obj, xmlTagName, itemSamples);
|
|
}
|
|
} else {
|
|
const arraySamples = [];
|
|
for (const key in itemSamples) {
|
|
arraySamples[key] = [itemSamples[key]];
|
|
}
|
|
obj = mergePropertyExamples(obj, propertyName, arraySamples);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
obj = mergePropertyExamples(
|
|
obj,
|
|
propertyName,
|
|
schemaToSampleObj(schema.properties[propertyName], config),
|
|
);
|
|
}
|
|
}
|
|
} else if (schema.type === 'array' || schema.items) {
|
|
const schemaItems = schema.items as OpenAPISchema;
|
|
if (schemaItems || schema.example) {
|
|
if (schema.example) {
|
|
obj[defaultExampleId] = schema.example;
|
|
} else if (schemaItems?.example) {
|
|
// schemas and properties support single example but not multiple examples.
|
|
obj[defaultExampleId] = [schemaItems.example];
|
|
} else {
|
|
const samples = schemaToSampleObj(schemaItems, config);
|
|
let i = 0;
|
|
for (const key in samples) {
|
|
const exampleId = getExampleId(i + 1);
|
|
obj[exampleId] = [samples[key]];
|
|
addSchemaInfoToExample(schemaItems, obj[exampleId]);
|
|
i++;
|
|
}
|
|
}
|
|
} else {
|
|
obj[defaultExampleId] = [];
|
|
}
|
|
} else {
|
|
return { [defaultExampleId]: getSampleValueByType(schema) };
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2022 Mrinmoy Majumdar
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
export const generateXmlExample = ({
|
|
includeReadOnly = true,
|
|
includeWriteOnly = true,
|
|
schema,
|
|
}: GenerateExampleProps): FinalExamples[] => {
|
|
const finalExamples: FinalExamples[] = [];
|
|
|
|
if (!schema) return finalExamples;
|
|
|
|
const xmlRootStart = schema.xml?.name
|
|
? `<${schema.xml.name}${schema.xml.namespace ? ` xmlns="${schema.xml.namespace}"` : ''}>`
|
|
: '<root>';
|
|
const xmlRootEnd = schema.xml?.name ? `</${schema.xml.name}>` : '</root>';
|
|
const samples = schemaToSampleObj(schema, {
|
|
includeReadOnly,
|
|
includeWriteOnly,
|
|
includeDeprecated: true,
|
|
useXmlTagForProp: true,
|
|
});
|
|
let i = 0;
|
|
for (const samplesKey in samples) {
|
|
if (!samples[samplesKey]) {
|
|
continue;
|
|
}
|
|
const summary = samples[samplesKey]['::TITLE'] || `Example ${++i}`;
|
|
const description = samples[samplesKey]['::DESCRIPTION'] || '';
|
|
const exampleValue = `<?xml version="1.0" encoding="UTF-8"?>\n${xmlRootStart}${json2xml(
|
|
samples[samplesKey],
|
|
1,
|
|
)}\n${xmlRootEnd}`;
|
|
|
|
finalExamples.push({
|
|
exampleDescription: description,
|
|
exampleId: samplesKey,
|
|
exampleSummary: summary,
|
|
exampleValue,
|
|
});
|
|
}
|
|
|
|
return finalExamples;
|
|
};
|