UNPKG

openapi-merge

Version:

A tool to merge numerous OpenAPI files into a single openapi definition.

269 lines (268 loc) 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mergePathsAndComponents = void 0; const atlassian_openapi_1 = require("atlassian-openapi"); const reference_walker_1 = require("./reference-walker"); const lodash_1 = __importDefault(require("lodash")); const operation_selection_1 = require("./operation-selection"); const component_equivalence_1 = require("./component-equivalence"); const dispute_1 = require("./dispute"); function removeFromStart(input, trim) { if (input.startsWith(trim)) { return input.substring(trim.length); } return input; } function processComponents(results, components, areEqual, dispute, addModifiedReference) { for (const key in components) { /* eslint-disable-next-line no-prototype-builtins */ if (components.hasOwnProperty(key)) { const component = components[key]; const modifiedKey = dispute_1.applyDispute(dispute, key, 'undisputed'); if (modifiedKey !== key) { addModifiedReference(key, modifiedKey); } if (results[modifiedKey] === undefined || areEqual(results[modifiedKey], component)) { // Add the schema results[modifiedKey] = component; } else { // Distnguish the name and then add the element let schemaPlaced = false; // Try and use the dispute prefix first if (dispute !== undefined) { const preferredSchemaKey = dispute_1.applyDispute(dispute, key, 'disputed'); if (results[preferredSchemaKey] === undefined || areEqual(results[preferredSchemaKey], component)) { results[preferredSchemaKey] = component; addModifiedReference(key, preferredSchemaKey); schemaPlaced = true; } } // Incrementally find the right prefix for (let antiConflict = 1; schemaPlaced === false && antiConflict < 1000; antiConflict++) { const trySchemaKey = `${key}${antiConflict}`; if (results[trySchemaKey] === undefined) { results[trySchemaKey] = component; addModifiedReference(key, trySchemaKey); schemaPlaced = true; } } // In the unlikely event that we can't find a duplicate, return an error if (schemaPlaced === false) { return { type: 'component-definition-conflict', message: `The "${key}" definition had a duplicate in a previous input and could not be deduplicated.` }; } } } } } function countOperationsInPathItem(pathItem) { let count = 0; count += pathItem.get !== undefined ? 1 : 0; count += pathItem.put !== undefined ? 1 : 0; count += pathItem.post !== undefined ? 1 : 0; count += pathItem.delete !== undefined ? 1 : 0; count += pathItem.options !== undefined ? 1 : 0; count += pathItem.head !== undefined ? 1 : 0; count += pathItem.patch !== undefined ? 1 : 0; count += pathItem.trace !== undefined ? 1 : 0; return count; } function dropPathItemsWithNoOperations(originalOas) { const oas = lodash_1.default.cloneDeep(originalOas); for (const path in oas.paths) { /* eslint-disable-next-line no-prototype-builtins */ if (oas.paths.hasOwnProperty(path)) { const pathItem = oas.paths[path]; if (countOperationsInPathItem(pathItem) === 0) { delete oas.paths[path]; } } } return oas; } function findUniqueOperationId(operationId, seenOperationIds, dispute) { if (!seenOperationIds.has(operationId)) { return operationId; } // Try the dispute prefix if (dispute !== undefined) { const disputeOpId = dispute_1.applyDispute(dispute, operationId, 'disputed'); if (!seenOperationIds.has(disputeOpId)) { return disputeOpId; } } // Incrementally find the right prefix for (let antiConflict = 1; antiConflict < 1000; antiConflict++) { const tryOpId = `${operationId}${antiConflict}`; if (!seenOperationIds.has(tryOpId)) { return tryOpId; } } // Fail with an error return { type: 'operation-id-conflict', message: `Could not resolve a conflict for the operationId '${operationId}'` }; } function ensureUniqueOperationId(operation, seenOperationIds, dispute) { if (operation.operationId !== undefined) { const opId = findUniqueOperationId(operation.operationId, seenOperationIds, dispute); if (typeof opId === 'string') { operation.operationId = opId; seenOperationIds.add(opId); } else { return opId; } } } function ensureUniqueOperationIds(pathItem, seenOperationIds, dispute) { const operations = [ pathItem.get, pathItem.put, pathItem.post, pathItem.delete, pathItem.patch, pathItem.head, pathItem.trace, pathItem.options ]; for (let opIndex = 0; opIndex < operations.length; opIndex++) { const operation = operations[opIndex]; if (operation !== undefined) { const result = ensureUniqueOperationId(operation, seenOperationIds, dispute); if (result !== undefined) { return result; } } } } /** * Merge algorithm: * * Generate reference mappings for the components. Eliminating duplicates. * Generate reference mappings for the paths. * Copy the elements into the new location. * Update all of the paths and components to the new references. * * @param inputs */ function mergePathsAndComponents(inputs) { const seenOperationIds = new Set(); const result = { paths: {}, components: {}, }; for (let inputIndex = 0; inputIndex < inputs.length; inputIndex++) { const input = inputs[inputIndex]; const { oas: originalOas, pathModification, operationSelection } = input; const dispute = dispute_1.getDispute(input); const oas = dropPathItemsWithNoOperations(operation_selection_1.runOperationSelection(lodash_1.default.cloneDeep(originalOas), operationSelection)); // Original references will be transformed to new non-conflicting references const referenceModification = {}; // For each component in the original input, place it in the output with deduplicate taking place if (oas.components !== undefined) { const resultLookup = new atlassian_openapi_1.SwaggerLookup.InternalLookup({ openapi: '3.0.1', info: { title: 'dummy', version: '0' }, paths: {}, components: result.components }); const currentLookup = new atlassian_openapi_1.SwaggerLookup.InternalLookup(oas); if (oas.components.schemas !== undefined) { result.components.schemas = result.components.schemas || {}; processComponents(result.components.schemas, oas.components.schemas, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/schemas/${from}`] = `#/components/schemas/${to}`; }); } if (oas.components.responses !== undefined) { result.components.responses = result.components.responses || {}; processComponents(result.components.responses, oas.components.responses, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/responses/${from}`] = `#/components/responses/${to}`; }); } if (oas.components.parameters !== undefined) { result.components.parameters = result.components.parameters || {}; processComponents(result.components.parameters, oas.components.parameters, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/parameters/${from}`] = `#/components/parameters/${to}`; }); } // examples if (oas.components.examples !== undefined) { result.components.examples = result.components.examples || {}; processComponents(result.components.examples, oas.components.examples, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/examples/${from}`] = `#/components/examples/${to}`; }); } // requestBodies if (oas.components.requestBodies !== undefined) { result.components.requestBodies = result.components.requestBodies || {}; processComponents(result.components.requestBodies, oas.components.requestBodies, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/requestBodies/${from}`] = `#/components/requestBodies/${to}`; }); } // headers if (oas.components.headers !== undefined) { result.components.headers = result.components.headers || {}; processComponents(result.components.headers, oas.components.headers, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/headers/${from}`] = `#/components/headers/${to}`; }); } // security schemes are different, we just take the security schemes from the first file that has any if (oas.components.securitySchemes !== undefined && Object.keys(oas.components.securitySchemes).length > 0 && result.components.securitySchemes === undefined) { result.components.securitySchemes = oas.components.securitySchemes; } // links if (oas.components.links !== undefined) { result.components.links = result.components.links || {}; processComponents(result.components.links, oas.components.links, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/links/${from}`] = `#/components/links/${to}`; }); } // callbacks if (oas.components.callbacks !== undefined) { result.components.callbacks = result.components.callbacks || {}; processComponents(result.components.callbacks, oas.components.callbacks, component_equivalence_1.deepEquality(resultLookup, currentLookup), dispute, (from, to) => { referenceModification[`#/components/callbacks/${from}`] = `#/components/callbacks/${to}`; }); } } // For each path, convert it into the right format (looking out for duplicates) const paths = Object.keys(oas.paths || {}); for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) { const originalPath = paths[pathIndex]; const newPath = pathModification === undefined ? originalPath : `${pathModification.prepend || ''}${removeFromStart(originalPath, pathModification.stripStart || '')}`; if (originalPath !== newPath) { referenceModification[`#/paths/${originalPath}`] = `#/paths/${newPath}`; } // TODO perform more advanced matching for an existing path than an equals check if (result.paths[newPath] !== undefined) { return { type: 'duplicate-paths', message: `Input ${inputIndex}: The path '${originalPath}' maps to '${newPath}' and this has already been added by another input file` }; } const copyPathItem = oas.paths[originalPath]; ensureUniqueOperationIds(copyPathItem, seenOperationIds, dispute); result.paths[newPath] = copyPathItem; } // Update the references to point to the right location const modifiedKeys = Object.keys(referenceModification); reference_walker_1.walkAllReferences(oas, ref => { if (referenceModification[ref] !== undefined) { return referenceModification[ref]; } const matchingKeys = modifiedKeys.filter(key => key.startsWith(`${ref}/`)); if (matchingKeys.length > 1) { throw new Error(`Found more than one matching key for reference '${ref}': ${JSON.stringify(matchingKeys)}`); } else if (matchingKeys.length === 1) { return referenceModification[matchingKeys[0]]; } return ref; }); } return result; } exports.mergePathsAndComponents = mergePathsAndComponents;