@mintlify/validation
Version:
Validates mint.json files
312 lines (311 loc) • 13 kB
JavaScript
import lcm from 'lcm';
export const stringFileFormats = ['binary', 'base64'];
export const structuredDataContentTypes = [
'multipart/form-data',
'application/json',
'application/x-www-form-urlencoded',
];
// the number of times a $ref can point to another $ref before we give up
const MAX_DEREFERENCE_DEPTH = 5;
export function dereference(section, $ref, components, maxDepth = MAX_DEREFERENCE_DEPTH) {
const sectionPrefix = `#/components/${section}/`;
if (!$ref.startsWith(sectionPrefix))
return undefined;
const key = $ref.slice(sectionPrefix.length);
const value = components === null || components === void 0 ? void 0 : components[key];
// if a $ref points to another $ref, keep resolving until we hit our max depth
if (value && '$ref' in value) {
if (maxDepth > 0 && typeof value.$ref === 'string') {
return dereference(section, value.$ref, components, maxDepth - 1);
}
return undefined;
}
return value;
}
export const addKeyIfDefined = (key, value, destination) => {
if (value !== undefined) {
destination[key] = value;
}
};
export const copyKeyIfDefined = (key, source, destination) => {
// eslint does not recognize that D[K] could be undefined
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (source[key] !== undefined) {
destination[key] = source[key];
}
};
export const copyExampleIfDefined = (source, destination) => {
var _a;
const example = source.example !== undefined ? source.example : (_a = source.examples) === null || _a === void 0 ? void 0 : _a[0];
if (example !== undefined) {
destination.example = example;
}
};
export function recursivelyFindDescription(schema, name) {
var _a;
if (typeof schema !== 'object' || name === undefined) {
return undefined;
}
if (schema.discriminator && schema.discriminator.mapping) {
const mapping = schema.discriminator.mapping;
if (name in mapping && ((_a = mapping[name]) === null || _a === void 0 ? void 0 : _a.description)) {
return mapping[name].description;
}
}
if (Array.isArray(schema)) {
for (const subschema of schema) {
const description = recursivelyFindDescription(subschema, name);
if (description)
return description;
}
return undefined;
}
if ('type' in schema &&
schema.type === name &&
'description' in schema &&
typeof schema.description === 'string') {
return schema.description;
}
for (const [key, value] of Object.entries(schema)) {
if (key === name &&
typeof value === 'object' &&
value !== null &&
'description' in value &&
typeof value.description === 'string') {
return value.description;
}
const description = recursivelyFindDescription(value, name);
if (description)
return description;
}
return undefined;
}
export function sortSchemas(schemas) {
// all schemas with no `type` field go at the end of the array to avoid
// caling `copyKeyIfDefined` for discriminators with empty types unless
// we're certain the schema's title is the last possible option
schemas.sort((a, b) => {
var _a, _b;
const aDepth = (_a = a._depth) !== null && _a !== void 0 ? _a : 0;
const bDepth = (_b = b._depth) !== null && _b !== void 0 ? _b : 0;
if (aDepth !== bDepth) {
return aDepth - bDepth;
}
if (a.type && !b.type)
return -1;
if (!a.type && b.type)
return 1;
return 0;
});
}
export function mergeTypes(acc, curr) {
// schemas are meant to be immutable, so copy the type
let currType = curr.type;
// don't throw an error if type is being constricted
if (acc.type === 'integer' && currType === 'number') {
currType = 'integer';
}
else if (acc.type === 'number' && currType === 'integer') {
acc.type = 'integer';
}
else if (acc.type === undefined && currType !== undefined) {
acc.type = currType;
}
else if (acc.type !== undefined && currType === undefined) {
currType = acc.type;
}
if (acc.type !== currType) {
throw new Error(`${acc.type} vs ${currType}`);
}
}
export function normalizeMinMax(schema) {
// we're technically breaking immutability rules here, but it's probably okay because
// it will be the same every time - we're just normalizing the maximum/minimum
// and exclusiveMaximum/exclusiveMinimum properties
if (typeof schema.exclusiveMaximum === 'number') {
if (schema.maximum === undefined || schema.maximum >= schema.exclusiveMaximum) {
schema.maximum = schema.exclusiveMaximum;
schema.exclusiveMaximum = true;
}
else {
schema.exclusiveMaximum = undefined;
}
}
if (typeof schema.exclusiveMinimum === 'number') {
if (schema.minimum === undefined || schema.minimum <= schema.exclusiveMinimum) {
schema.minimum = schema.exclusiveMinimum;
schema.exclusiveMinimum = true;
}
else {
schema.exclusiveMinimum = undefined;
}
}
}
const combine = (schema1, schema2, key, transform) => {
var _a;
return schema1[key] !== undefined && schema2[key] !== undefined
? transform(schema1[key], schema2[key])
: (_a = schema1[key]) !== null && _a !== void 0 ? _a : schema2[key];
};
const combineKeyIfDefined = (key, source, destination, transform) => {
addKeyIfDefined(key, combine(source, destination, key, transform), destination);
};
export function combineTitle(acc, curr) {
if (curr.discriminator == undefined &&
(curr.type != undefined || acc.title == undefined) &&
!(curr.isAllOf && curr.refIdentifier && curr.refIdentifier !== curr.isAllOf)) {
copyKeyIfDefined('title', curr, acc);
}
}
export function copyAndCombineKeys(acc, curr) {
copyKeyIfDefined('refIdentifier', curr, acc);
copyKeyIfDefined('examples', curr, acc);
copyKeyIfDefined('format', curr, acc);
copyKeyIfDefined('default', curr, acc);
copyKeyIfDefined('x-default', curr, acc);
copyKeyIfDefined('const', curr, acc);
combineKeyIfDefined('multipleOf', curr, acc, lcm);
combineKeyIfDefined('maxLength', curr, acc, Math.min);
combineKeyIfDefined('minLength', curr, acc, Math.max);
combineKeyIfDefined('maxItems', curr, acc, Math.min);
combineKeyIfDefined('minItems', curr, acc, Math.max);
combineKeyIfDefined('maxProperties', curr, acc, Math.min);
combineKeyIfDefined('minProperties', curr, acc, Math.max);
combineKeyIfDefined('required', curr, acc, (a, b) => b.concat(a.filter((value) => !b.includes(value))));
combineKeyIfDefined('enum', curr, acc, (a, b) => b.filter((value) => a.includes(value)));
combineKeyIfDefined('readOnly', curr, acc, (a, b) => a && b);
combineKeyIfDefined('writeOnly', curr, acc, (a, b) => a && b);
combineKeyIfDefined('deprecated', curr, acc, (a, b) => a || b);
const combinedMaximum = combine(curr, acc, 'maximum', Math.min);
const combinedMinimum = combine(curr, acc, 'minimum', Math.max);
const exclusiveMaximum = (acc.maximum === combinedMaximum ? acc.exclusiveMaximum : undefined) ||
(curr.maximum === combinedMaximum ? curr.exclusiveMaximum : undefined);
addKeyIfDefined('exclusiveMaximum', exclusiveMaximum, acc);
const exclusiveMinimum = (acc.minimum === combinedMinimum ? acc.exclusiveMinimum : undefined) ||
(curr.minimum === combinedMinimum ? curr.exclusiveMinimum : undefined);
addKeyIfDefined('exclusiveMinimum', exclusiveMinimum, acc);
addKeyIfDefined('maximum', combinedMaximum, acc);
addKeyIfDefined('minimum', combinedMinimum, acc);
}
export function combineExamples(acc, curr) {
var _a, _b;
// don't use coalesce operator, since null is a valid example
const example1 = ((_a = acc.examples) === null || _a === void 0 ? void 0 : _a[0]) !== undefined ? acc.examples[0] : acc.example;
const example2 = ((_b = curr.examples) === null || _b === void 0 ? void 0 : _b[0]) !== undefined ? curr.examples[0] : curr.example;
if (example1 && example2 && typeof example1 === 'object' && typeof example2 === 'object') {
acc.example = Object.assign(Object.assign({}, example1), example2);
}
else {
// don't use coalesce operator, since null is a valid example
addKeyIfDefined('example', example2 !== undefined ? example2 : example1, acc);
}
}
export function combineProperties(acc, curr, componentSchemas, location) {
if (curr.properties) {
Object.entries(curr.properties)
.filter(([_, subschema]) => {
// dereference just for the readOnly/writeOnly check
if ('$ref' in subschema) {
const dereferencedSchema = dereference('schemas', subschema.$ref, componentSchemas);
if (!dereferencedSchema)
return true;
subschema = dereferencedSchema;
}
if (subschema.readOnly && location === 'request')
return false;
if (subschema.writeOnly && location === 'response')
return false;
return true;
})
.forEach(([property, subschema]) => {
var _a;
const properties = (_a = acc.properties) !== null && _a !== void 0 ? _a : {};
const currSchemaArr = properties[property];
if (currSchemaArr) {
currSchemaArr.allOf.push(subschema);
}
else {
properties[property] = { allOf: [subschema] };
}
acc.properties = properties;
});
}
}
export function combineDescription(acc, curr, schemas) {
var _a, _b;
if (((_a = acc.properties) === null || _a === void 0 ? void 0 : _a.type) && curr.discriminator && !acc.description) {
let name = undefined;
const allOf = (_b = acc.properties.type.allOf[0]) !== null && _b !== void 0 ? _b : {};
if ('const' in allOf && typeof allOf.const === 'string') {
name = allOf.const;
}
const description = recursivelyFindDescription(curr, name) ||
schemas.flatMap((schema) => recursivelyFindDescription(schema, name)).filter(Boolean)[0];
if (description) {
acc.description = description;
}
}
else if (acc.description &&
curr.description &&
!curr.discriminator &&
!acc.description.includes(curr.description)) {
acc.description = `${acc.description}\n${curr.description}`;
}
else if (!acc.description) {
copyKeyIfDefined('description', curr, acc);
}
}
export function combineAdditionalProperties(acc, curr) {
var _a;
if (curr.additionalProperties === false) {
acc.additionalProperties = false;
}
else if (acc.additionalProperties !== false &&
curr.additionalProperties &&
typeof curr.additionalProperties === 'object') {
const additionalProperties = (_a = acc.additionalProperties) !== null && _a !== void 0 ? _a : { allOf: [] };
additionalProperties.allOf.push(curr.additionalProperties);
acc.additionalProperties = additionalProperties;
}
}
export function discriminatorAndSchemaRefsMatch(schema) {
const discriminator = schema.discriminator;
if (discriminator === null || discriminator === void 0 ? void 0 : discriminator.mapping) {
const discriminatedRefs = Object.values(discriminator.mapping).map((ref) => ref);
const schemaRefs = [];
if ('allOf' in schema) {
if (Array.isArray(schema.allOf)) {
schema.allOf.forEach((ref) => {
if ('$ref' in ref) {
schemaRefs.push(ref.$ref);
}
});
}
}
if ('oneOf' in schema) {
if (Array.isArray(schema.oneOf)) {
schema.oneOf.forEach((ref) => {
if ('$ref' in ref) {
schemaRefs.push(ref.$ref);
}
});
}
}
if ('anyOf' in schema) {
if (Array.isArray(schema.anyOf)) {
schema.anyOf.forEach((ref) => {
if ('$ref' in ref) {
schemaRefs.push(ref.$ref);
}
});
}
}
const discriminatedRefsSet = new Set(discriminatedRefs);
const schemaRefsSet = new Set(schemaRefs);
return (discriminatedRefsSet.size === schemaRefsSet.size &&
[...discriminatedRefsSet].every((ref) => schemaRefsSet.has(ref)));
}
else {
return false;
}
}