UNPKG

@mintlify/validation

Version:

Validates mint.json files

381 lines (380 loc) 20.1 kB
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)]; }