UNPKG

@mintlify/validation

Version:

Validates mint.json files

312 lines (311 loc) 13 kB
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; } }