json-machete
Version:
849 lines (841 loc) • 41.9 kB
JavaScript
import { AggregateError, inspect } from '@graphql-tools/utils';
import JsonPointer from 'json-pointer';
import { path, process } from '@graphql-mesh/cross-helpers';
import urlJoin from 'url-join';
import { fetch } from '@whatwg-node/fetch';
import { defaultImportFn, DefaultLogger, readFileOrUrl } from '@graphql-mesh/utils';
import toJsonSchema from 'to-json-schema';
const identicalFn = (a) => a;
const objectFields = [
'additionalProperties',
'additionalItems',
'contains',
'else',
'if',
'items',
'not',
'then',
];
const dictFields = ['anyOf', 'allOf', 'oneOf', 'definitions', 'properties', 'patternProperties'];
async function visitJSONSchema(schema, { enter = identicalFn, leave = identicalFn, }, { visitedSubschemaResultMap, path } = {
visitedSubschemaResultMap: new WeakMap(),
path: '',
}) {
var _a;
if (typeof schema === 'object') {
if (!visitedSubschemaResultMap.has(schema)) {
const enterResult = await enter(schema, {
visitedSubschemaResultMap,
path,
});
visitedSubschemaResultMap.set(schema, enterResult);
for (const key of objectFields) {
if (enterResult[key]) {
enterResult[key] = await visitJSONSchema(enterResult[key], { enter, leave }, {
visitedSubschemaResultMap,
path: `${path}/${key}`,
});
}
}
for (const key of dictFields) {
if (enterResult[key]) {
const entries = Object.entries(enterResult[key]);
for (const [itemKey, itemValue] of entries) {
enterResult[key][itemKey] = await visitJSONSchema(itemValue, { enter, leave }, { visitedSubschemaResultMap, path: `${path}/${key}/${itemKey}` });
}
}
}
if ((_a = enterResult.components) === null || _a === void 0 ? void 0 : _a.schema) {
const entries = Object.entries(enterResult.components.schemas);
for (const [schemaName, subSchema] of entries) {
enterResult.components.schemas[schemaName] = await visitJSONSchema(subSchema, { enter, leave }, { visitedSubschemaResultMap, path: `${path}/components/schemas/${schemaName}` });
}
}
const leaveResult = await leave(enterResult, {
visitedSubschemaResultMap,
path,
});
visitedSubschemaResultMap.set(schema, leaveResult);
return leaveResult;
}
return visitedSubschemaResultMap.get(schema);
}
const enterResult = await enter(schema, {
visitedSubschemaResultMap,
path,
});
return leave(enterResult, {
visitedSubschemaResultMap,
path,
});
}
function handleUntitledDefinitions(schemaDocument) {
var _a, _b, _c;
const seen = new Map();
function handleDefinitions(definitions) {
for (const definitionName in definitions) {
const definition = definitions[definitionName];
if (!definition.$ref) {
if (!definition.title) {
definition.title = definitionName;
}
else {
const seenDefinition = seen.get(definition.title);
if (seenDefinition) {
definition.title = definitionName;
seenDefinition.definition.title = seenDefinition.definitionName;
}
seen.set(definition.title, { definitionName, definition });
}
}
}
}
if (schemaDocument.definitions) {
handleDefinitions(schemaDocument.definitions);
}
if ((_a = schemaDocument.components) === null || _a === void 0 ? void 0 : _a.schemas) {
handleDefinitions(schemaDocument.components.schemas);
}
const bodyTypeMap = {
responses: 'response',
requestBodies: 'request',
};
for (const bodyType in bodyTypeMap) {
const bodies = (_b = schemaDocument.components) === null || _b === void 0 ? void 0 : _b[bodyType];
if (bodies) {
for (const bodyName in bodies) {
const body = bodies[bodyName];
if (body.content) {
for (const contentType in body.content) {
const contentObj = body.content[contentType];
const contentSchema = contentObj.schema;
if (contentSchema && !contentSchema.$ref) {
if (!contentSchema.title) {
contentSchema.title = bodyName;
if (contentType !== 'application/json') {
contentSchema.title += `_${contentType.split('/')[1]}`;
}
const suffix = bodyTypeMap[bodyType];
contentSchema.title += '_' + suffix;
}
if (body.description && !contentSchema.description) {
contentSchema.description = body.description;
}
}
}
}
}
}
}
const inputTypeMap = {
parameters: 'parameter',
headers: 'header',
};
for (const inputType in inputTypeMap) {
const inputs = (_c = schemaDocument.components) === null || _c === void 0 ? void 0 : _c[inputType];
if (inputs) {
for (const inputName in inputs) {
const input = inputs[inputName];
const inputSchema = input.schema;
if (inputSchema && !inputSchema.$ref) {
if (!inputSchema.title) {
const suffix = inputTypeMap[inputType];
inputSchema.title = inputName + '_' + suffix;
}
if (input.description && !inputSchema.description) {
inputSchema.description = input.description;
}
}
}
}
}
}
const resolvePath = (path, root) => {
var _a;
try {
return JsonPointer.get(root, path);
}
catch (e) {
if ((_a = e.message) === null || _a === void 0 ? void 0 : _a.startsWith('Invalid reference')) {
return undefined;
}
throw e;
}
};
function isRefObject(obj) {
return typeof obj === 'object' && typeof obj.$ref === 'string';
}
function isURL(str) {
return /^https?:\/\//.test(str);
}
const getAbsolute$Ref = (given$ref, baseFilePath) => {
const [givenExternalFileRelativePath, givenRefPath] = given$ref.split('#');
if (givenExternalFileRelativePath) {
const cwd = isURL(baseFilePath) ? getCwdForUrl(baseFilePath) : path.dirname(baseFilePath);
const givenExternalFilePath = getAbsolutePath(givenExternalFileRelativePath, cwd);
if (givenRefPath) {
return `${givenExternalFilePath}#${givenRefPath}`;
}
return givenExternalFilePath;
}
return `${baseFilePath}#${givenRefPath}`;
};
function getCwdForUrl(url) {
const urlParts = url.split('/');
urlParts.pop();
return urlParts.join('/');
}
function normalizeUrl(url) {
return new URL(url).toString();
}
function getAbsolutePath(path$1, cwd) {
if (isURL(path$1)) {
return path$1;
}
if (isURL(cwd)) {
return normalizeUrl(urlJoin(cwd, path$1));
}
if (path.isAbsolute(path$1)) {
return path$1;
}
return path.join(cwd, path$1);
}
function getCwd(path$1) {
return isURL(path$1) ? getCwdForUrl(path$1) : path.dirname(path$1);
}
// eslint-disable-next-line @typescript-eslint/ban-types
async function dereferenceObject(obj, { cwd = process.cwd(), externalFileCache = new Map(), refMap = new Map(), root = obj, fetchFn: fetch$1 = fetch, importFn = defaultImportFn, logger = new DefaultLogger('dereferenceObject'), resolvedObjects = new WeakSet(), headers, } = {}) {
if (obj != null && typeof obj === 'object') {
if (isRefObject(obj)) {
const $ref = obj.$ref;
if (refMap.has($ref)) {
return refMap.get($ref);
}
else {
logger.debug(`Resolving ${$ref}`);
const [externalRelativeFilePath, refPath] = $ref.split('#');
if (externalRelativeFilePath) {
const externalFilePath = getAbsolutePath(externalRelativeFilePath, cwd);
const newCwd = getCwd(externalFilePath);
let externalFile = externalFileCache.get(externalFilePath);
if (!externalFile) {
externalFile = await readFileOrUrl(externalFilePath, {
fetch: fetch$1,
headers,
cwd,
importFn,
logger,
}).catch(() => {
throw new Error(`Unable to load ${externalRelativeFilePath} from ${cwd}`);
});
externalFileCache.set(externalFilePath, externalFile);
// Title should not be overwritten by the title given from the reference
// Usually Swagger and OpenAPI Schemas have this
handleUntitledDefinitions(externalFile);
}
const result = await dereferenceObject(refPath
? {
$ref: `#${refPath}`,
}
: externalFile, {
cwd: newCwd,
externalFileCache,
refMap: new Proxy(refMap, {
get: (originalRefMap, key) => {
switch (key) {
case 'has':
return (given$ref) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.has(original$Ref);
};
case 'get':
return (given$ref) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.get(original$Ref);
};
case 'set':
return (given$ref, val) => {
const original$Ref = getAbsolute$Ref(given$ref, externalFilePath);
return originalRefMap.set(original$Ref, val);
};
}
throw new Error('Not implemented ' + key.toString());
},
}),
fetchFn: fetch$1,
importFn,
logger,
headers,
root: externalFile,
resolvedObjects,
});
refMap.set($ref, result);
resolvedObjects.add(result);
if (result && !result.$resolvedRef) {
result.$resolvedRef = refPath;
}
if (obj.title && !result.title) {
result.title = obj.title;
}
return result;
}
else {
const resolvedObj = resolvePath(refPath, root);
if (resolvedObjects.has(resolvedObj)) {
refMap.set($ref, resolvedObj);
return resolvedObj;
}
/*
if (resolvedObj && !resolvedObj.$resolvedRef) {
resolvedObj.$resolvedRef = refPath;
}
*/
const result = await dereferenceObject(resolvedObj, {
cwd,
externalFileCache,
refMap,
root,
fetchFn: fetch$1,
importFn,
logger,
headers,
resolvedObjects,
});
if (!result) {
return obj;
}
resolvedObjects.add(result);
refMap.set($ref, result);
if (!result.$resolvedRef) {
result.$resolvedRef = refPath;
}
return result;
}
}
}
else {
if (!resolvedObjects.has(obj)) {
resolvedObjects.add(obj);
for (const key in obj) {
const val = obj[key];
if (typeof val === 'object') {
obj[key] = await dereferenceObject(val, {
cwd,
externalFileCache,
refMap,
root,
fetchFn: fetch$1,
headers,
resolvedObjects,
});
}
}
}
}
}
return obj;
}
async function compareJSONSchemas(oldSchema, newSchema) {
const breakingChanges = [];
await visitJSONSchema(oldSchema, {
enter: (oldSubSchema, { path }) => {
var _a, _b, _c, _d;
if (typeof newSchema === 'object') {
const newSubSchema = resolvePath(path, newSchema);
if (typeof oldSubSchema === 'boolean') {
if (newSubSchema !== oldSubSchema) {
breakingChanges.push(`${path} is changed from ${oldSubSchema} to ${newSubSchema}`);
}
}
else {
if (oldSubSchema.$ref) {
if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.$ref) !== oldSubSchema.$ref) {
breakingChanges.push(`${path}/$ref is changed from ${oldSubSchema.$ref} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.$ref}`);
}
}
if (oldSubSchema.const) {
if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.const) !== oldSubSchema.const) {
breakingChanges.push(`${path}/const is changed from ${oldSubSchema.const} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.const}`);
}
}
if (oldSubSchema.enum) {
for (const enumValue of oldSubSchema.enum) {
if (!((_a = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.enum) === null || _a === void 0 ? void 0 : _a.includes(enumValue))) {
breakingChanges.push(`${path}/enum doesn't have ${enumValue} anymore`);
}
}
}
if (oldSubSchema.format) {
if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.format) !== oldSubSchema.format) {
breakingChanges.push(`${path}/format is changed from ${oldSubSchema.format} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.format}`);
}
}
if (oldSubSchema.maxLength) {
if (oldSubSchema.maxLength > (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.maxLength)) {
breakingChanges.push(`${path}/maxLength is changed from ${oldSubSchema.maxLength} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.maxLength}`);
}
}
if (oldSubSchema.minLength) {
if (oldSubSchema.minLength < (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.minLength)) {
breakingChanges.push(`${path}/minLength is changed from ${oldSubSchema.minLength} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.minLength}`);
}
}
if (oldSubSchema.pattern) {
if (((_b = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.pattern) === null || _b === void 0 ? void 0 : _b.toString()) !== oldSubSchema.pattern.toString()) {
breakingChanges.push(`${path}/pattern is changed from ${oldSubSchema.pattern} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.pattern}`);
}
}
if (oldSubSchema.properties) {
for (const propertyName in oldSubSchema.properties) {
if (((_c = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.properties) === null || _c === void 0 ? void 0 : _c[propertyName]) == null) {
breakingChanges.push(`${path}/properties doesn't have ${propertyName}`);
}
}
}
if (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.required) {
for (const propertyName of newSubSchema.required) {
if (!((_d = oldSubSchema.required) === null || _d === void 0 ? void 0 : _d.includes(propertyName))) {
breakingChanges.push(`${path}/required has ${propertyName} an extra`);
}
}
}
if (oldSubSchema.title) {
if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.title) !== oldSubSchema.title) {
breakingChanges.push(`${path}/title is changed from ${oldSubSchema.title} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.title}`);
}
}
if (oldSubSchema.type) {
if (typeof (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type) === 'string'
? (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type) !== oldSubSchema.type
: Array.isArray(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type)
? Array.isArray(oldSubSchema.type)
? oldSubSchema.type.some(typeName => !(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type.includes(typeName)))
: !(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type.includes(oldSubSchema.type))
: true) {
breakingChanges.push(`${path}/type is changed from ${oldSubSchema.type} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type}`);
}
}
}
}
return oldSubSchema;
},
}, {
visitedSubschemaResultMap: new WeakMap(),
path: '',
});
if (breakingChanges.length > 0) {
throw new AggregateError(breakingChanges.map(breakingChange => new Error(breakingChange)), `Breaking changes are found:\n${breakingChanges.join('\n')}`);
}
}
const asArray = (value) => (Array.isArray(value) ? value : [value]);
const JSONSchemaStringFormats = [
'date',
'hostname',
'regex',
'json-pointer',
'relative-json-pointer',
'uri-reference',
'uri-template',
'date-time',
'time',
'email',
'ipv4',
'ipv6',
'uri',
'uuid',
'binary',
'byte',
'int64',
'int32',
'unix-time',
'double',
'float',
'decimal',
];
const AnySchema = {
title: 'Any',
oneOf: [
{ type: 'string' },
{ type: 'integer' },
{ type: 'boolean' },
{ type: 'number' },
{ type: 'object', additionalProperties: true },
],
};
async function healJSONSchema(schema, { logger = new DefaultLogger('healJSONSchema') } = {}) {
const schemaByTitle = new Map();
const anySchemaOneOfInspect = inspect(AnySchema.oneOf);
return visitJSONSchema(schema, {
enter: async function healSubschema(subSchema, { path }) {
if (typeof subSchema === 'object') {
if (subSchema.title === 'Any' || (subSchema.oneOf && inspect(subSchema.oneOf) === anySchemaOneOfInspect)) {
return AnySchema;
}
if (subSchema.title) {
if (Object.keys(subSchema).length === 2 && subSchema.type) {
delete subSchema.title;
}
else {
const existingSubSchema = schemaByTitle.get(subSchema.title);
if (existingSubSchema) {
if (inspect(subSchema) === inspect(existingSubSchema)) {
return existingSubSchema;
}
else {
delete subSchema.title;
}
}
else {
schemaByTitle.set(subSchema.title, subSchema);
}
}
}
else if (Object.keys(subSchema).length === 1 && subSchema.type && !Array.isArray(subSchema.type)) {
return subSchema;
}
const keys = Object.keys(subSchema).filter(key => key !== 'readOnly' && key !== 'writeOnly');
if (keys.length === 0) {
logger.debug(`${path} has an empty definition. Adding an object definition.`);
subSchema.type = 'object';
subSchema.additionalProperties = true;
}
if (typeof subSchema.additionalProperties === 'object') {
const additionalPropertiesKeys = Object.keys(subSchema.additionalProperties).filter(key => key !== 'readOnly' && key !== 'writeOnly');
if (additionalPropertiesKeys.length === 0 ||
(additionalPropertiesKeys.length === 1 && subSchema.additionalProperties.type === 'string')) {
logger.debug(`${path} has an empty additionalProperties object. So this is invalid. Replacing it with 'true'`);
subSchema.additionalProperties = true;
}
}
// Really edge case, but we need to support it
if (subSchema.allOf != null && subSchema.allOf.length === 1 && subSchema.allOf[0].oneOf && subSchema.oneOf) {
subSchema.oneOf.push(...subSchema.allOf[0].oneOf);
delete subSchema.allOf;
}
// If they have title, it makes sense to keep them to reflect the schema in a better way
if (!subSchema.title) {
if (subSchema.anyOf != null &&
subSchema.anyOf.length === 1 &&
!subSchema.properties &&
!subSchema.allOf &&
!subSchema.oneOf) {
logger.debug(`${path} has an "anyOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.anyOf[0];
delete subSchema.anyOf;
subSchema = realSubschema;
}
if (subSchema.allOf != null &&
subSchema.allOf.length === 1 &&
!subSchema.properties &&
!subSchema.anyOf &&
!subSchema.oneOf) {
logger.debug(`${path} has an "allOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.allOf[0];
delete subSchema.allOf;
subSchema = realSubschema;
}
}
if (subSchema.oneOf != null &&
subSchema.oneOf.length === 1 &&
!subSchema.properties &&
!subSchema.anyOf &&
!subSchema.allOf) {
logger.debug(`${path} has an "oneOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.oneOf[0];
delete subSchema.oneOf;
subSchema = realSubschema;
}
if (subSchema.description != null) {
subSchema.description = subSchema.description.trim();
if (keys.length === 1) {
logger.debug(`${path} has a description definition but has nothing else. Adding an object definition.`);
subSchema.type = 'object';
subSchema.additionalProperties = true;
}
}
// Some JSON Schemas use this broken pattern and refer the type using `items`
if (subSchema.type === 'object' && subSchema.items) {
logger.debug(`${path} has an object definition but with "items" which is not valid. So setting "items" to the actual definition.`);
const realSubschema = subSchema.items;
delete subSchema.items;
subSchema = realSubschema;
}
if (subSchema.properties && subSchema.type !== 'object') {
logger.debug(`${path} has "properties" with no type defined. Adding a type property with "object" value.`);
subSchema.type = 'object';
}
if (typeof subSchema.example === 'object' && !subSchema.type) {
logger.debug(`${path} has an example object but no type defined. Setting type to "object".`);
subSchema.type = 'object';
}
// Items only exist in arrays
if (subSchema.items) {
logger.debug(`${path} has an items definition but no type defined. Setting type to "array".`);
subSchema.type = 'array';
if (subSchema.properties) {
delete subSchema.properties;
}
// Items should be an object
if (Array.isArray(subSchema.items)) {
if (subSchema.items.length === 0) {
logger.debug(`${path} has an items array with a single value. Setting items to an object.`);
subSchema.items = subSchema.items[0];
}
else {
logger.debug(`${path} has an items array with multiple values. Setting items to an object with oneOf definition.`);
subSchema.items = {
oneOf: subSchema.items,
};
}
}
}
// Try to find the type
if (!subSchema.type) {
logger.debug(`${path} has no type defined. Trying to find it.`);
// If required exists without properties
if (subSchema.required && !subSchema.properties && !subSchema.anyOf && !subSchema.allOf) {
logger.debug(`${path} has a required definition but no properties or oneOf/allOf. Setting missing properties with Any schema.`);
// Add properties
subSchema.properties = {};
for (const missingPropertyName of subSchema.required) {
subSchema.properties[missingPropertyName] = AnySchema;
}
}
// Properties only exist in objects
if (subSchema.properties || subSchema.patternProperties || 'additionalProperties' in subSchema) {
logger.debug(`${path} has properties or patternProperties or additionalProperties. Setting type to "object".`);
subSchema.type = 'object';
}
switch (subSchema.format) {
case 'int64':
case 'int32':
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "integer".`);
subSchema.type = 'integer';
break;
case 'float':
case 'double':
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "number".`);
subSchema.type = 'number';
break;
default:
if (subSchema.format != null) {
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "string".`);
subSchema.type = 'string';
}
}
if (subSchema.minimum != null || subSchema.maximum != null) {
logger.debug(`${path} has a minimum or maximum. Setting type to "number".`);
subSchema.type = 'number';
}
}
if (subSchema.type === 'string' && !subSchema.format && (subSchema.examples || subSchema.example)) {
const examples = asArray(subSchema.examples || subSchema.example || []);
if (examples === null || examples === void 0 ? void 0 : examples.length) {
const { format } = toJsonSchema(examples[0]);
if (format && format !== 'utc-millisec' && format !== 'style') {
logger.debug(`${path} has a format of ${format} according to the example. Setting type to "string".`);
subSchema.format = format;
}
}
}
if (subSchema.format === 'dateTime') {
logger.debug(`${path} has a format of dateTime. It should be "date-time".`);
subSchema.format = 'date-time';
}
if (subSchema.format) {
if (!JSONSchemaStringFormats.includes(subSchema.format)) {
logger.debug(`${path} has a format of ${subSchema.format}. It should be one of ${JSONSchemaStringFormats.join(', ')}.`);
delete subSchema.format;
}
}
if (subSchema.required) {
if (!Array.isArray(subSchema.required)) {
logger.debug(`${path} has a required definition but it is not an array. Removing it.`);
delete subSchema.required;
}
}
// If it is an object type but no properties given while example is available
if (((subSchema.type === 'object' &&
!subSchema.properties &&
!subSchema.allOf &&
!subSchema.anyOf &&
!subSchema.oneOf) ||
!subSchema.type) &&
(subSchema.example || subSchema.examples)) {
const examples = asArray(subSchema.examples || subSchema.example || []);
const generatedSchema = toJsonSchema(examples[0], {
required: false,
objects: {
additionalProperties: false,
},
strings: {
detectFormat: true,
},
arrays: {
mode: 'first',
},
postProcessFnc(type, schema, value, defaultFunc) {
if (schema.type === 'object' && !schema.properties && Object.keys(value).length === 0) {
return AnySchema;
}
return defaultFunc(type, schema, value);
},
});
subSchema.type = asArray(generatedSchema.type)[0];
subSchema.properties = generatedSchema.properties;
// If type for properties is already given, use it
logger.debug(`${path} has an example but no type defined. Setting type to ${subSchema.type}.`);
// if (typeof subSchema.additionalProperties === 'object') {
// for (const propertyName in subSchema.properties) {
// subSchema.properties[propertyName] = subSchema.additionalProperties;
// }
// }
}
if (subSchema.enum && subSchema.enum.length === 1 && subSchema.type !== 'boolean') {
subSchema.const = subSchema.enum[0];
logger.debug(`${path} has an enum but with a single value. Converting it to const.`);
delete subSchema.enum;
}
if (!subSchema.title && !subSchema.$ref && subSchema.type !== 'array' && !subSchema.items) {
const realPath = subSchema.$resolvedRef || path;
// Try to get definition name if missing
const splitByDefinitions = realPath.includes('/components/schemas/')
? realPath.split('/components/schemas/')
: realPath.split('/definitions/');
const maybeDefinitionBasedPath = splitByDefinitions.length > 1 ? splitByDefinitions[splitByDefinitions.length - 1] : realPath;
const pathBasedName = maybeDefinitionBasedPath
.split('~1')
.join('/')
.split('/properties')
.join('')
.split('-')
.join('_')
.split('/')
.filter(Boolean)
.join('_');
switch (subSchema.type) {
case 'string':
// If it has special pattern, use path based name because it is specific
if (subSchema.pattern || subSchema.maxLength || subSchema.minLength || subSchema.enum) {
logger.debug(`${path} has a pattern or maxLength or minLength or enum but no title. Setting it to ${pathBasedName}`);
subSchema.title = pathBasedName;
// Otherwise use the format name
}
break;
case 'number':
case 'integer':
if (subSchema.enum || subSchema.pattern) {
logger.debug(`${path} has an enum or pattern but no title. Setting it to ${pathBasedName}`);
subSchema.title = pathBasedName;
// Otherwise use the format name
}
break;
case 'array':
break;
case 'boolean':
// pattern is unnecessary for boolean
if (subSchema.pattern) {
logger.debug(`${path} has a pattern for a boolean type. Removing it.`);
delete subSchema.pattern;
}
// enum is unnecessary for boolean
if (subSchema.enum) {
logger.debug(`${path} is an enum but a boolean type. Removing it.`);
delete subSchema.enum;
}
break;
default:
logger.debug(`${path} has no title. Setting it to ${pathBasedName}`);
subSchema.title = subSchema.title || pathBasedName;
}
if (subSchema.const) {
subSchema.title = subSchema.const.toString() + '_const';
const existingSubSchema = schemaByTitle.get(subSchema.title);
if (existingSubSchema) {
return existingSubSchema;
}
schemaByTitle.set(subSchema.title, subSchema);
}
}
if (subSchema.type === 'object' && subSchema.properties && Object.keys(subSchema.properties).length === 0) {
logger.debug(`${path} has an empty properties object. Removing it and adding "additionalProperties": true.`);
delete subSchema.properties;
subSchema.additionalProperties = true;
}
if (subSchema.properties) {
const propertyValues = Object.values(subSchema.properties);
if (propertyValues.every(property => property.writeOnly && !property.readOnly)) {
subSchema.writeOnly = true;
}
if (propertyValues.every(property => property.readOnly && !property.writeOnly)) {
subSchema.readOnly = true;
}
}
}
return subSchema;
},
}, {
visitedSubschemaResultMap: new WeakMap(),
path: '',
});
}
async function referenceJSONSchema(schema, logger = new DefaultLogger('referenceJSONSchema')) {
const initialDefinitions = {};
const { $ref: initialRef } = await visitJSONSchema(schema, {
enter: (subSchema, { path }) => {
if (typeof subSchema === 'object') {
// Remove $id refs
delete subSchema.$id;
if (subSchema.$ref) {
return subSchema;
}
else if (subSchema.title) {
logger.debug(`Referencing ${path}`);
if (subSchema.title in initialDefinitions) {
let cnt = 2;
while (`${subSchema.title}${cnt}` in initialDefinitions) {
cnt++;
}
const definitionProp = `${subSchema.title}${cnt}`.split(' ').join('_SPACE_');
initialDefinitions[definitionProp] = subSchema;
return {
$ref: `#/definitions/${definitionProp}`,
...subSchema,
};
}
else {
const definitionProp = subSchema.title.split(' ').join('_SPACE_');
initialDefinitions[definitionProp] = subSchema;
return {
$ref: `#/definitions/${definitionProp}`,
...subSchema,
};
}
}
else if (subSchema.type === 'object') {
logger.debug(`${path} cannot be referenced because it has no title`);
}
}
return subSchema;
},
});
const { definitions: finalDefinitions } = await visitJSONSchema({
definitions: initialDefinitions,
}, {
enter: subSchema => {
if (typeof subSchema === 'object') {
if (subSchema.$ref) {
return {
$ref: subSchema.$ref,
};
}
}
return subSchema;
},
});
return {
$ref: initialRef,
definitions: finalDefinitions,
};
}
export { AnySchema, compareJSONSchemas, dereferenceObject, getAbsolutePath, getCwd, handleUntitledDefinitions, healJSONSchema, referenceJSONSchema, resolvePath, visitJSONSchema };