@mintlify/validation
Version:
Validates mint.json files
381 lines (380 loc) • 20.1 kB
JavaScript
import { inferType } from './SchemaConverter.js';
import { addKeyIfDefined, copyExampleIfDefined, copyKeyIfDefined, dereference, mergeTypes, normalizeMinMax, stringFileFormats, structuredDataContentTypes, sortSchemas, combineExamples, combineProperties, combineDescription, combineAdditionalProperties, copyAndCombineKeys, combineTitle, discriminatorAndSchemaRefsMatch, } from './utils.js';
function hasSchemaContent(schema) {
const meaningfulKeys = Object.keys(schema).filter((key) => !key.startsWith('_'));
return meaningfulKeys.length > 0;
}
/**
* Given an OpenAPI 3.1 SchemaObject or ReferenceObject containing any number of
* refs or compositions, this function returns the schema in sum-of-products form.
*
* When given the following schema:
*
* ```yaml
* title: 'A'
* oneOf:
* - { title: 'B' }
* - { title: 'C' }
* allOf:
* - { title: 'D' }
* - { title: 'E' }
* ```
*
* this function returns the following sum of products:
*
* ```js
* [
* [{ title: 'B' }, { title: 'D' }, { title: 'E' }, { title: 'A' }],
* [{ title: 'C' }, { title: 'D' }, { title: 'E' }, { title: 'A' }],
* ]
* ```
*
* @param schema The schema or ref to reduce
* @param componentSchemas The value of `document.components.schemas`, to be used when dereferencing
* @returns The schema in sum-of-products form
*/
export function reduceToSumOfProducts(schemaOrRef, componentSchemas, opts) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
let schema;
const _depth = (_a = opts === null || opts === void 0 ? void 0 : opts._depth) !== null && _a !== void 0 ? _a : 0;
if ('$ref' in schemaOrRef) {
const refIdentifier = schemaOrRef.$ref;
const dereferencedSchema = dereference('schemas', schemaOrRef.$ref, componentSchemas);
if (!dereferencedSchema)
return [[{}]];
if (dereferencedSchema.type === undefined &&
((_b = dereferencedSchema.allOf) === null || _b === void 0 ? void 0 : _b.find((subschema) => 'type' in subschema && subschema.type === 'object'))) {
dereferencedSchema.type = 'object';
}
// @ts-expect-error some customers use title even though it's not part of the spec
const dereferencedSchemaTitle = (_c = schemaOrRef.title) !== null && _c !== void 0 ? _c : dereferencedSchema.title;
schema = Object.assign(Object.assign({}, dereferencedSchema), { title: dereferencedSchemaTitle, refIdentifier: refIdentifier, description: (_d = schemaOrRef.description) !== null && _d !== void 0 ? _d : dereferencedSchema.description, isOneOf: schemaOrRef.isOneOf, isAnyOf: schemaOrRef.isAnyOf, isAllOf: schemaOrRef.isAllOf });
}
else {
schema = schemaOrRef;
}
if ((opts === null || opts === void 0 ? void 0 : opts.isRoot) &&
schema.type === 'array' &&
((_e = schema.discriminator) === null || _e === void 0 ? void 0 : _e.mapping) &&
!discriminatorAndSchemaRefsMatch(schema)) {
const discriminator = schema.discriminator;
const baseItemSchema = schema.items;
delete schema.discriminator;
const discriminatedRefs = Object.values((_f = discriminator.mapping) !== null && _f !== void 0 ? _f : {}).map((ref) => ({
$ref: ref,
}));
let extraItems = [];
// depth + 1 because we go one level deeper when we process this oneOf
if ('oneOf' in baseItemSchema) {
extraItems =
(_h = (_g = baseItemSchema.oneOf) === null || _g === void 0 ? void 0 : _g.filter((item) => {
var _a;
if ('$ref' in item) {
return !Object.values((_a = discriminator.mapping) !== null && _a !== void 0 ? _a : {}).includes(item.$ref);
}
return true;
})) !== null && _h !== void 0 ? _h : [];
extraItems = extraItems.map((item) => (Object.assign(Object.assign({}, item), { _depth: _depth + 1 })));
delete baseItemSchema.oneOf;
}
// depth + 1 because we go one level deeper when we process the discriminated types into oneOf
const processedDiscriminatedTypes = discriminatedRefs.map((refObj) => {
var _a, _b;
const dereferenced = dereference('schemas', refObj.$ref, componentSchemas);
if (!dereferenced)
return Object.assign(Object.assign({}, baseItemSchema), { _depth: _depth + 1 });
const resolvedBaseSchema = '$ref' in baseItemSchema
? dereference('schemas', baseItemSchema.$ref, componentSchemas)
: baseItemSchema;
const baseProperties = (_a = resolvedBaseSchema === null || resolvedBaseSchema === void 0 ? void 0 : resolvedBaseSchema.properties) !== null && _a !== void 0 ? _a : {};
const dereferencedProperties = (_b = dereferenced.properties) !== null && _b !== void 0 ? _b : {};
return Object.assign(Object.assign(Object.assign({}, baseItemSchema), dereferenced), { properties: Object.assign(Object.assign({}, baseProperties), dereferencedProperties), _depth: _depth + 1 });
});
schema = Object.assign(Object.assign({}, schema), { items: Object.assign(Object.assign({}, baseItemSchema), { oneOf: [...processedDiscriminatedTypes, ...extraItems] }) });
}
else if ((opts === null || opts === void 0 ? void 0 : opts.isRoot) &&
((_j = schema.discriminator) === null || _j === void 0 ? void 0 : _j.mapping) &&
!schema.oneOf &&
!schema.allOf &&
!discriminatorAndSchemaRefsMatch(schema)) {
const properties = Object.values(schema.discriminator.mapping);
if (properties.length) {
delete schema.discriminator;
schema = Object.assign(Object.assign({}, schema), { allOf: [
{
oneOf: [
...properties.map((prop) => ({
$ref: prop,
})),
],
},
] });
}
}
// handle v3 `nullable` property
const nullable = schema.nullable;
if (!schema.oneOf && !schema.allOf && !schema.anyOf && !Array.isArray(schema.type) && !nullable) {
return [[Object.assign(Object.assign({}, schema), { _depth: _depth })]];
}
const baseSchema = Object.assign(Object.assign({}, schema), { _depth: _depth });
delete baseSchema.oneOf;
delete baseSchema.anyOf;
delete baseSchema.allOf;
delete baseSchema.not;
const baseSchemaTypes = Array.isArray(baseSchema.type) ? [...baseSchema.type] : [baseSchema.type];
if (nullable && !baseSchemaTypes.includes('null')) {
baseSchemaTypes.push('null');
}
const baseSchemaArr = hasSchemaContent(baseSchema)
? [baseSchemaTypes.map((type) => [Object.assign(Object.assign({}, baseSchema), { type })])]
: [];
const reducedOneOfs = (_k = schema.oneOf) === null || _k === void 0 ? void 0 : _k.map((subschema) => {
if ('refIdentifier' in schema) {
subschema.isOneOf = schema.refIdentifier;
}
return reduceToSumOfProducts(subschema, componentSchemas, { _depth: _depth + 1 });
});
const reducedAnyOfs = (_l = schema.anyOf) === null || _l === void 0 ? void 0 : _l.map((subschema) => {
if ('refIdentifier' in schema) {
subschema.isAnyOf = schema.refIdentifier;
}
return reduceToSumOfProducts(subschema, componentSchemas, { _depth: _depth + 1 });
});
const reducedAllOfs = (_m = schema.allOf) === null || _m === void 0 ? void 0 : _m.map((subschema) => {
if ('refIdentifier' in schema) {
subschema.isAllOf = schema.refIdentifier;
}
return reduceToSumOfProducts(subschema, componentSchemas, { _depth: _depth + 1 });
});
const combinedOneOfs = reducedOneOfs ? [addIncrementalSchemas(reducedOneOfs)] : [];
const combinedAnyOfs = reducedAnyOfs ? [addIncrementalSchemas(reducedAnyOfs)] : [];
const multipliedAllOfs = reducedAllOfs ? [multiplyIncrementalSchemas(reducedAllOfs)] : [];
const result = multiplyIncrementalSchemas([
...combinedOneOfs,
...combinedAnyOfs,
...multipliedAllOfs,
...baseSchemaArr,
]);
// Propagate composition tracking properties to all schemas in the result
const resultWithCompositionProps = result.map((product) => product.map((schema) => (Object.assign(Object.assign(Object.assign(Object.assign({}, schema), (schema.isOneOf && { isOneOf: schema.isOneOf })), (schema.isAnyOf && { isAnyOf: schema.isAnyOf })), (schema.isAllOf && { isAllOf: schema.isAllOf })))));
return resultWithCompositionProps;
}
/**
* Adds an array of schemas in sum-of-products form, returning the result
* as a single schema in sum-of-products form.
*
* (AB + CD) + (E + F) + (G + H) => AB + CD + E + F + G + H
*/
function addIncrementalSchemas(schemas) {
// this one's easy! just concatenate all the sums
return schemas.flat();
}
/**
* Multiplies an array of schemas in sum-of-products form, returning the result
* as a single schema in sum-of-products form.
*
* (AB + CD)(E + F)(G + H) => ABEG + ABEH + ABFG + ABFH + CDEG + CDEH + CDFG + CDFH
*/
function multiplyIncrementalSchemas(schemas) {
// walking through this function, we'll use the example (AB + CD)(E + F)(G + H)
// base case, which allows us to essentially do (G + H)(1) = (G + H)
if (!schemas[0]) {
return [[]];
}
// now we evaluate using the distributive property:
// (AB + CD)(EG + EH + FG + FH) = (ABEG + ABEH + ABFG + ABFH) + (CDEG + CDEH + CDFG + CDFH)
// first, we recursively evaluate all remaining terms so we only have to deal with the situation above.
// in our scenario, the remaining terms are (E + F)(G + H), which gives us (EG + EH + FG + FH)
const remainingSumOfProducts = multiplyIncrementalSchemas(schemas.slice(1));
return schemas[0].flatMap((product /* AB */) => {
return remainingSumOfProducts.map((remainingProduct /* EF */) => {
// Combine all schemas in the product
const combinedProduct = [...product, ...remainingProduct];
// Propagate composition tracking properties from all schemas to each schema
return combinedProduct.map((schema) => {
const compositionProps = {};
// Collect all composition properties from all schemas in the product
combinedProduct.forEach((otherSchema) => {
if (otherSchema.isOneOf)
compositionProps.isOneOf = otherSchema.isOneOf;
if (otherSchema.isAnyOf)
compositionProps.isAnyOf = otherSchema.isAnyOf;
if (otherSchema.isAllOf)
compositionProps.isAllOf = otherSchema.isAllOf;
});
// Filter out composition properties that match refIdentifier
if (schema.refIdentifier) {
if (compositionProps.isOneOf === schema.refIdentifier) {
delete compositionProps.isOneOf;
}
if (compositionProps.isAnyOf === schema.refIdentifier) {
delete compositionProps.isAnyOf;
}
if (compositionProps.isAllOf === schema.refIdentifier) {
delete compositionProps.isAllOf;
}
}
return Object.assign(Object.assign({}, schema), compositionProps);
});
});
});
}
/**
* This function logically combines an array of simple schemas (schemas that contain no compositions)
* in preparation for conversion to `IncrementalDataSchema`. This is akin to "multiplying" all of the schemas,
* to continue our math analogy. The result is a single simple schema, which is easier to work with.
*
* How fields are combined depends on the field. For fields like `title` and `description` where
* it doesn't make sense to combine, we just take the last. For `required` we combine arrays,
* for `maximum` we take the minimum value, etc.
*
* @param schemas An array of simple schemas to combine
* @param componentSchemas The value of `document.components.schemas`. In this function, it is only used to check if properties are readOnly/writeOnly
* @param location Whether the schema is part of the request, response, or neither. Used for filtering readOnly/writeOnly properties
* @returns A single simple schema that satisfies all the input schemas
*/
export function combineSimpleSchemas(schemas, componentSchemas, location) {
sortSchemas(schemas);
return schemas.reduce((acc, curr) => {
var _a;
mergeTypes(acc, curr);
for (const schema of [acc, curr]) {
normalizeMinMax(schema);
}
combineTitle(acc, curr);
copyAndCombineKeys(acc, curr);
combineExamples(acc, curr);
if (curr.items) {
const items = (_a = acc.items) !== null && _a !== void 0 ? _a : { allOf: [] };
items.allOf.push(curr.items);
acc.items = items;
}
combineProperties(acc, curr, componentSchemas, location);
combineDescription(acc, curr, schemas);
combineAdditionalProperties(acc, curr);
return acc;
}, {});
}
function convertCombinedSchema(schema, required) {
var _a, _b;
const sharedProps = {};
addKeyIfDefined('required', required, sharedProps);
copyKeyIfDefined('title', schema, sharedProps);
copyKeyIfDefined('description', schema, sharedProps);
copyKeyIfDefined('readOnly', schema, sharedProps);
copyKeyIfDefined('writeOnly', schema, sharedProps);
copyKeyIfDefined('deprecated', schema, sharedProps);
copyKeyIfDefined('refIdentifier', schema, sharedProps);
copyKeyIfDefined('examples', schema, sharedProps);
if (schema.type === undefined) {
const inferredType = inferType(schema);
if (inferredType === undefined) {
return Object.assign({ type: 'any' }, sharedProps);
}
schema.type = inferredType;
}
switch (schema.type) {
case 'boolean':
const booleanProps = sharedProps;
copyKeyIfDefined('default', schema, booleanProps);
copyKeyIfDefined('x-default', schema, booleanProps);
copyExampleIfDefined(schema, booleanProps);
return Object.assign({ type: schema.type }, booleanProps);
case 'number':
case 'integer':
if (schema.enum) {
const numberEnumProps = sharedProps;
copyKeyIfDefined('default', schema, numberEnumProps);
copyKeyIfDefined('x-default', schema, numberEnumProps);
copyExampleIfDefined(schema, numberEnumProps);
return Object.assign({ type: schema.type === 'number' ? 'enum<number>' : 'enum<integer>', enum: schema.enum.filter((option) => typeof option === 'number') }, numberEnumProps);
}
const numberProps = sharedProps;
copyKeyIfDefined('multipleOf', schema, numberProps);
copyKeyIfDefined('maximum', schema, numberProps);
copyKeyIfDefined('exclusiveMaximum', schema, numberProps);
copyKeyIfDefined('minimum', schema, numberProps);
copyKeyIfDefined('exclusiveMinimum', schema, numberProps);
copyKeyIfDefined('default', schema, numberProps);
copyKeyIfDefined('x-default', schema, numberProps);
copyExampleIfDefined(schema, numberProps);
return Object.assign({ type: schema.type }, numberProps);
case 'string':
if (schema.enum) {
const stringEnumProps = sharedProps;
copyKeyIfDefined('default', schema, stringEnumProps);
copyKeyIfDefined('x-default', schema, stringEnumProps);
copyExampleIfDefined(schema, stringEnumProps);
return Object.assign({ type: 'enum<string>', enum: schema.enum.filter((option) => typeof option === 'string') }, stringEnumProps);
}
if (schema.format && stringFileFormats.includes(schema.format)) {
const fileProps = sharedProps;
return Object.assign({ type: 'file', contentEncoding: schema.format }, fileProps);
}
const stringProps = sharedProps;
copyKeyIfDefined('format', schema, stringProps);
copyKeyIfDefined('pattern', schema, stringProps);
copyKeyIfDefined('maxLength', schema, stringProps);
copyKeyIfDefined('minLength', schema, stringProps);
copyKeyIfDefined('default', schema, stringProps);
copyKeyIfDefined('x-default', schema, stringProps);
copyKeyIfDefined('const', schema, stringProps);
copyExampleIfDefined(schema, stringProps);
return Object.assign({ type: schema.type }, stringProps);
case 'array':
const arrayProps = sharedProps;
copyKeyIfDefined('maxItems', schema, arrayProps);
copyKeyIfDefined('minItems', schema, arrayProps);
copyKeyIfDefined('uniqueItems', schema, arrayProps);
copyKeyIfDefined('default', schema, arrayProps);
copyKeyIfDefined('x-default', schema, arrayProps);
copyExampleIfDefined(schema, arrayProps);
return Object.assign({ type: schema.type, items: (_a = schema.items) !== null && _a !== void 0 ? _a : {} }, arrayProps);
case 'object':
const objectProperties = sharedProps;
addKeyIfDefined('requiredProperties', schema.required, objectProperties);
copyKeyIfDefined('additionalProperties', schema, objectProperties);
copyKeyIfDefined('maxProperties', schema, objectProperties);
copyKeyIfDefined('minProperties', schema, objectProperties);
copyKeyIfDefined('default', schema, objectProperties);
copyKeyIfDefined('x-default', schema, objectProperties);
copyExampleIfDefined(schema, objectProperties);
return Object.assign({ type: schema.type, properties: (_b = schema.properties) !== null && _b !== void 0 ? _b : {} }, objectProperties);
case 'null':
const nullProps = sharedProps;
copyKeyIfDefined('default', schema, nullProps);
copyKeyIfDefined('x-default', schema, nullProps);
copyExampleIfDefined(schema, nullProps);
return Object.assign({ type: schema.type }, nullProps);
default:
throw new Error();
}
}
export function generateFirstIncrementalSchema(schema, componentSchemas, required, location, contentType) {
if (schema === undefined) {
// If the content type can feasibly be interpreted as a file (i.e. if it does NOT start
// with "application/json" or another structured data format), return a file type.
if (contentType && !structuredDataContentTypes.some((type) => contentType.startsWith(type))) {
return [{ type: 'file', contentMediaType: contentType }];
}
return [{ type: 'any' }];
}
return generateNextIncrementalSchema(schema, componentSchemas, required, location);
}
export function generateNextIncrementalSchema(schema, componentSchemas, required, location) {
const sumOfProducts = reduceToSumOfProducts(schema, componentSchemas, { isRoot: true });
const incrementalDataSchemaArray = sumOfProducts.flatMap((product) => {
try {
const combinedSchema = combineSimpleSchemas(product, componentSchemas, location);
return [convertCombinedSchema(combinedSchema, required)];
}
catch (_a) {
return [];
}
});
if (!incrementalDataSchemaArray[0]) {
// TODO: throw when `safeParse: false`
return [{ type: 'any' }];
}
return [incrementalDataSchemaArray[0], ...incrementalDataSchemaArray.slice(1)];
}