@scalar/oas-utils
Version:
Open API spec and Yaml handling utilities
279 lines (277 loc) • 12.3 kB
JavaScript
/** Hard limit for rendering circular references */
const MAX_LEVELS_DEEP = 5;
/** Sets the max number of properties after the third level to prevent exponential horizontal growth */
const MAX_PROPERTIES = 10;
const genericExampleValues = {
// 'date-time': '1970-01-01T00:00:00Z',
'date-time': new Date().toISOString(),
// 'date': '1970-01-01',
'date': new Date().toISOString().split('T')[0],
'email': 'hello@example.com',
'hostname': 'example.com',
// https://tools.ietf.org/html/rfc6531#section-3.3
'idn-email': 'jane.doe@example.com',
// https://tools.ietf.org/html/rfc5890#section-2.3.2.3
'idn-hostname': 'example.com',
'ipv4': '127.0.0.1',
'ipv6': '51d4:7fab:bfbf:b7d7:b2cb:d4b4:3dad:d998',
'iri-reference': '/entitiy/1',
// https://tools.ietf.org/html/rfc3987
'iri': 'https://example.com/entity/123',
'json-pointer': '/nested/objects',
'password': 'super-secret',
'regex': '/[a-z]/',
// https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01
'relative-json-pointer': '1/nested/objects',
// full-time in https://tools.ietf.org/html/rfc3339#section-5.6
// 'time': '00:00:00Z',
'time': new Date().toISOString().split('T')[1].split('.')[0],
// either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1
'uri-reference': '../folder',
'uri-template': 'https://example.com/{id}',
'uri': 'https://example.com',
'uuid': '123e4567-e89b-12d3-a456-426614174000',
'object-id': '6592008029c8c3e4dc76256c',
};
/**
* We can use the `format` to generate some random values.
*/
function guessFromFormat(schema, fallback = '') {
return genericExampleValues[schema.format] ?? fallback;
}
/** Map of all the results */
const resultCache = new WeakMap();
/** Store result in the cache, and return the result */
function cache(schema, result) {
// Avoid unnecessary WeakMap operations for primitive values
if (typeof result !== 'object' || result === null) {
return result;
}
resultCache.set(schema, result);
return result;
}
/**
* This function takes an OpenAPI schema and generates an example from it
*/
const getExampleFromSchema = (schema, options, level = 0, parentSchema, name) => {
// Check if the result is already cached
if (resultCache.has(schema)) {
return resultCache.get(schema);
}
// Check whether it’s a circular reference
if (level === MAX_LEVELS_DEEP + 1) {
try {
// Fails if it contains a circular reference
JSON.stringify(schema);
}
catch {
return '[Circular Reference]';
}
}
// Sometimes, we just want the structure and no values.
// But if `emptyString` is set, we do want to see some values.
const makeUpRandomData = !!options?.emptyString;
// Check if the property is read-only/write-only
if ((options?.mode === 'write' && schema.readOnly) || (options?.mode === 'read' && schema.writeOnly)) {
return undefined;
}
// Use given variables as values
if (schema['x-variable']) {
const value = options?.variables?.[schema['x-variable']];
// Return the value if it’s defined
if (value !== undefined) {
// Type-casting
if (schema.type === 'number' || schema.type === 'integer') {
return Number.parseInt(value, 10);
}
return cache(schema, value);
}
}
// Use the first example, if there’s an array
if (Array.isArray(schema.examples) && schema.examples.length > 0) {
return cache(schema, schema.examples[0]);
}
// Use an example, if there’s one
if (schema.example !== undefined) {
return cache(schema, schema.example);
}
// Use a default value, if there’s one
if (schema.default !== undefined) {
return cache(schema, schema.default);
}
// enum: [ 'available', 'pending', 'sold' ]
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
return cache(schema, schema.enum[0]);
}
// Check if the property is required
const isObjectOrArray = schema.type === 'object' ||
schema.type === 'array' ||
!!schema.allOf?.at?.(0) ||
!!schema.anyOf?.at?.(0) ||
!!schema.oneOf?.at?.(0);
if (!isObjectOrArray && options?.omitEmptyAndOptionalProperties === true) {
const isRequired = schema.required === true ||
parentSchema?.required === true ||
parentSchema?.required?.includes(name ?? schema.name);
if (!isRequired) {
return undefined;
}
}
// Object
if (schema.type === 'object' || schema.properties !== undefined) {
const response = {};
let propertyCount = 0;
// Regular properties
if (schema.properties !== undefined) {
for (const propertyName in schema.properties) {
if (Object.prototype.hasOwnProperty.call(schema.properties, propertyName)) {
// Only apply property limit for nested levels (level > 0)
if (level > 3 && propertyCount >= MAX_PROPERTIES) {
response['...'] = '[Additional Properties Truncated]';
break;
}
const property = schema.properties[propertyName];
const propertyXmlTagName = options?.xml ? property.xml?.name : undefined;
const value = getExampleFromSchema(property, options, level + 1, schema, propertyName);
if (typeof value !== 'undefined') {
response[propertyXmlTagName ?? propertyName] = value;
propertyCount++;
}
}
}
}
// Pattern properties (regex)
if (schema.patternProperties !== undefined) {
for (const pattern in schema.patternProperties) {
if (Object.prototype.hasOwnProperty.call(schema.patternProperties, pattern)) {
const property = schema.patternProperties[pattern];
// Use the regex pattern as an example key
const exampleKey = pattern;
response[exampleKey] = getExampleFromSchema(property, options, level + 1, schema, exampleKey);
}
}
}
// Additional properties
if (schema.additionalProperties !== undefined) {
const anyTypeIsValid =
// true
schema.additionalProperties === true ||
// or an empty object {}
(typeof schema.additionalProperties === 'object' && !Object.keys(schema.additionalProperties).length);
if (anyTypeIsValid) {
response['ANY_ADDITIONAL_PROPERTY'] = 'anything';
}
else if (schema.additionalProperties !== false) {
response['ANY_ADDITIONAL_PROPERTY'] = getExampleFromSchema(schema.additionalProperties, options, level + 1);
}
}
if (schema.anyOf !== undefined) {
Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1));
}
else if (schema.oneOf !== undefined) {
Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1));
}
else if (schema.allOf !== undefined) {
Object.assign(response, ...schema.allOf
.map((item) => getExampleFromSchema(item, options, level + 1, schema))
.filter((item) => item !== undefined));
}
return cache(schema, response);
}
// Array
if (schema.type === 'array' || schema.items !== undefined) {
const itemsXmlTagName = schema?.items?.xml?.name;
const wrapItems = !!(options?.xml && schema.xml?.wrapped && itemsXmlTagName);
if (schema.example !== undefined) {
return cache(schema, wrapItems ? { [itemsXmlTagName]: schema.example } : schema.example);
}
// Check whether the array has a anyOf, oneOf, or allOf rule
if (schema.items) {
// First handle allOf separately since it needs special handling
if (schema.items.allOf) {
// If the first item is an object type, merge all schemas
if (schema.items.allOf[0].type === 'object') {
const mergedExample = getExampleFromSchema({ type: 'object', allOf: schema.items.allOf }, options, level + 1, schema);
return cache(schema, wrapItems ? [{ [itemsXmlTagName]: mergedExample }] : [mergedExample]);
}
// For non-objects (like strings), collect all examples
const examples = schema.items.allOf
.map((item) => getExampleFromSchema(item, options, level + 1, schema))
.filter((item) => item !== undefined);
return cache(schema, wrapItems ? examples.map((example) => ({ [itemsXmlTagName]: example })) : examples);
}
// Handle other rules (anyOf, oneOf)
const rules = ['anyOf', 'oneOf'];
for (const rule of rules) {
if (!schema.items[rule]) {
continue;
}
const schemas = schema.items[rule].slice(0, 1);
const exampleFromRule = schemas
.map((item) => getExampleFromSchema(item, options, level + 1, schema))
.filter((item) => item !== undefined);
return cache(schema, wrapItems ? [{ [itemsXmlTagName]: exampleFromRule }] : exampleFromRule);
}
}
if (schema.items?.type) {
const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1);
return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema];
}
return [];
}
const exampleValues = {
string: makeUpRandomData ? guessFromFormat(schema, options?.emptyString) : '',
boolean: true,
integer: schema.min ?? 1,
number: schema.min ?? 1,
array: [],
};
if (schema.type !== undefined && exampleValues[schema.type] !== undefined) {
return cache(schema, exampleValues[schema.type]);
}
const discriminateSchema = schema.oneOf || schema.anyOf;
// Check if property has the `oneOf` | `anyOf` key
if (Array.isArray(discriminateSchema) && discriminateSchema.length > 0) {
// Get the first item from the `oneOf` | `anyOf` array
const firstOneOfItem = discriminateSchema[0];
// Return an example for the first item
return getExampleFromSchema(firstOneOfItem, options, level + 1);
}
// Check if schema has the `allOf` key
if (Array.isArray(schema.allOf)) {
let example = null;
// Loop through all `allOf` schemas
schema.allOf.forEach((allOfItem) => {
// Return an example from the schema
const newExample = getExampleFromSchema(allOfItem, options, level + 1);
// Merge or overwrite the example
example =
typeof newExample === 'object' && typeof example === 'object'
? {
...(example ?? {}),
...newExample,
}
: Array.isArray(newExample) && Array.isArray(example)
? [...(example ?? {}), ...newExample]
: newExample;
});
return cache(schema, example);
}
// Check if schema is a union type
if (Array.isArray(schema.type)) {
// Return null if the type is nullable
if (schema.type.includes('null')) {
return null;
}
// Return an example for the first type in the union
const exampleValue = exampleValues[schema.type[0]];
if (exampleValue !== undefined) {
return cache(schema, exampleValue);
}
}
// Warn if the type is unknown …
// console.warn(`[getExampleFromSchema] Unknown property type "${schema.type}".`)
// … and just return null for now.
return null;
};
export { getExampleFromSchema };