From 6ade03062ad569d63823b93963884f5da212f9a3 Mon Sep 17 00:00:00 2001 From: Luis Tejeda <46000487+LuisTejedaS@users.noreply.github.com> Date: Tue, 14 Jun 2022 12:25:48 -0500 Subject: [PATCH] Add reusable referenced path item object Add reusable referenced path item object --- lib/31XUtils/componentsParentMatcher.js | 109 ++++++++++++++++++ lib/bundle.js | 63 +++++----- lib/common/versionUtils.js | 6 +- lib/jsonPointer.js | 17 ++- lib/schemaUtils.js | 13 ++- .../referenced_paths_31/expected.json | 108 +++++++++++++++++ .../referenced_paths_31/path.yaml | 29 +++++ .../referenced_paths_31/root.yaml | 41 +++++++ test/unit/bundle31.test.js | 42 +++++++ 9 files changed, 386 insertions(+), 42 deletions(-) create mode 100644 lib/31XUtils/componentsParentMatcher.js create mode 100644 test/data/toBundleExamples/referenced_paths_31/expected.json create mode 100644 test/data/toBundleExamples/referenced_paths_31/path.yaml create mode 100644 test/data/toBundleExamples/referenced_paths_31/root.yaml create mode 100644 test/unit/bundle31.test.js diff --git a/lib/31XUtils/componentsParentMatcher.js b/lib/31XUtils/componentsParentMatcher.js new file mode 100644 index 0000000..1dc097a --- /dev/null +++ b/lib/31XUtils/componentsParentMatcher.js @@ -0,0 +1,109 @@ +const COMPONENTS_KEYS_31 = [ + 'schemas', + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'securitySchemes', + 'links', + 'callbacks', + 'pathItems' + ], + SCHEMA_CONTAINERS = [ + 'allOf', + 'oneOf', + 'anyOf', + 'not', + 'additionalProperties', + 'items', + 'schema' + ], + EXAMPLE_CONTAINERS = [ + 'example' + ], + PROPERTY_DEFINITION = [ + 'properties' + ], + RESPONSE_DEFINITION = [ + 'responses' + ], + REQUEST_BODY_CONTAINER = [ + 'requestBody' + ], + LINKS_CONTAINER = [ + 'links' + ], + HEADER_DEFINITION = [ + 'headers' + ], + CALLBACK_DEFINITION = [ + 'callbacks' + ], + PATH_ITEM_CONTAINER = [ + 'paths' + ]; + +module.exports = { + /** + * Generates the trace to the key that will wrap de component using 3.0 version + * @param {array} traceFromParent - The trace from the parent key + * @param {string} filePathName - The filePath name from the file + * @param {string} localPart - The local path part + * @param {function} jsonPointerDecodeAndReplace - Function to decode a json pointer + * @returns {array} The trace to the container key + */ + getKeyInComponents31: function (traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace) { + let res = traceFromParent, + trace = [], + traceToKey = [], + matchFound = false, + isInComponents = traceFromParent[0] === 'components'; + + if (isInComponents) { + return []; + } + + res.push(jsonPointerDecodeAndReplace(`${filePathName}${localPart}`)); + trace = [...res].reverse(); + + for (let [index, item] of trace.entries()) { + if (SCHEMA_CONTAINERS.includes(item)) { + item = 'schemas'; + } + if (EXAMPLE_CONTAINERS.includes(item)) { + item = 'examples'; + } + if (REQUEST_BODY_CONTAINER.includes(item)) { + item = 'requestBodies'; + } + if (LINKS_CONTAINER.includes(trace[index + 2])) { + trace[index + 1] = 'links'; + } + if (PATH_ITEM_CONTAINER.includes(trace[index + 2])) { + trace[index + 1] = 'pathItems'; + } + if (PROPERTY_DEFINITION.includes(trace[index + 2])) { + trace[index + 1] = 'schemas'; + } + traceToKey.push(item); + if (COMPONENTS_KEYS_31.includes(item)) { + matchFound = true; + break; + } + if (RESPONSE_DEFINITION.includes(trace[index + 2])) { + trace[index + 1] = 'responses'; + } + if (HEADER_DEFINITION.includes(trace[index + 2])) { + trace[index + 1] = 'headers'; + } + if (CALLBACK_DEFINITION.includes(trace[index + 2])) { + trace[index + 1] = 'callbacks'; + } + } + return matchFound ? + traceToKey.reverse() : + []; + }, + COMPONENTS_KEYS_31 +}; diff --git a/lib/bundle.js b/lib/bundle.js index 7684daa..1b5c09b 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -1,4 +1,5 @@ -const { +const { COMPONENTS_KEYS_30 } = require('./30XUtils/componentsParentMatcher'), + { isExtRef, getKeyInComponents, getJsonPointerRelationToRoot, @@ -10,24 +11,13 @@ const { } = require('./jsonPointer'), traverseUtility = require('traverse'), parse = require('./parse.js'), + { COMPONENTS_KEYS_31 } = require('./31XUtils/componentsParentMatcher'), { ParseError } = require('./common/ParseError'); let path = require('path'), pathBrowserify = require('path-browserify'), BROWSER = 'browser', { DFS } = require('./dfs'), - COMPONENTS_KEYS = [ - 'schemas', - 'schema', - 'responses', - 'parameters', - 'examples', - 'requestBodies', - 'headers', - 'securitySchemes', - 'links', - 'callbacks' - ], deref = require('./deref.js'); @@ -162,9 +152,11 @@ function getContentFromTrace(content, partial) { * @param {array} keyInComponents - The trace to the key in components * @param {object} components - A global components object * @param {object} value - The value from node matched with data + * @param {string} version - The current version * @returns {null} It modifies components global context */ -function setValueInComponents(keyInComponents, components, value) { +function setValueInComponents(keyInComponents, components, value, version) { + const COMPONENTS_KEYS = version === '3.1' ? COMPONENTS_KEYS_31 : COMPONENTS_KEYS_30; let currentPlace = components, target = keyInComponents[keyInComponents.length - 2], key = keyInComponents.length === 2 && COMPONENTS_KEYS.includes(keyInComponents[0]) ? @@ -196,9 +188,10 @@ function setValueInComponents(keyInComponents, components, value) { * Return a trace from the current node's root to the place where we find a $ref * @param {object} nodeContext - The current node we are processing * @param {object} property - The current property that contains the $ref + * @param {string} version - The current version of the spec * @returns {array} The trace to the place where the $ref appears */ -function getTraceFromParentKeyInComponents(nodeContext, property) { +function getTraceFromParentKeyInComponents(nodeContext, property, version) { const parents = [...nodeContext.parents].reverse(), isArrayKeyRegexp = new RegExp('^\\d$', 'g'), key = nodeContext.key, @@ -211,7 +204,7 @@ function getTraceFromParentKeyInComponents(nodeContext, property) { [key, ...parentKeys], nodeTrace = getRootFileTrace(nodeParentsKey), [file, local] = property.split(localPointer), - keyTraceInComponents = getKeyInComponents(nodeTrace, file, local); + keyTraceInComponents = getKeyInComponents(nodeTrace, file, local, version); return keyTraceInComponents; } @@ -221,11 +214,10 @@ function getTraceFromParentKeyInComponents(nodeContext, property) { * @param {Function} isOutOfRoot - A filter to know if the current components was called from root or not * @param {Function} pathSolver - function to resolve the Path * @param {string} parentFilename - The parent's filename - * @param {object} globalComponentsContext - The global context from root file - * @param {array} allData The data from files provided in the input - * @returns {object} - {path : $ref value} + * @param {object} version - The version of the spec we are bundling + * @returns {object} - The references in current node and the new content from the node */ -function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename) { +function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version) { let referencesInNode = [], nodeReferenceDirectory = {}; traverseUtility(currentNode).forEach(function (property) { @@ -242,10 +234,11 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename) { ); if (hasReferenceTypeKey) { const tempRef = calculatePath(parentFilename, property.$ref), - nodeTrace = getTraceFromParentKeyInComponents(this, tempRef), + nodeTrace = getTraceFromParentKeyInComponents(this, tempRef, version), referenceInDocument = getJsonPointerRelationToRoot( tempRef, - nodeTrace + nodeTrace, + version ), traceToParent = [...this.parents.map((item) => { return item.key; @@ -284,9 +277,10 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename) { * @param {object} currentNode - current { path, content} object * @param {Array} allData - array of { path, content} objects * @param {object} specRoot - root file information + * @param {string} version - The current version * @returns {object} - Detect root files result object */ -function getNodeContentAndReferences (currentNode, allData, specRoot) { +function getNodeContentAndReferences (currentNode, allData, specRoot, version) { let graphAdj = [], missingNodes = [], nodeContent; @@ -302,7 +296,8 @@ function getNodeContentAndReferences (currentNode, allData, specRoot) { nodeContent, currentNode.fileName !== specRoot.fileName, removeLocalReferenceFromPath, - currentNode.fileName + currentNode.fileName, + version ); referencesInNode.forEach((reference) => { @@ -332,9 +327,10 @@ function getNodeContentAndReferences (currentNode, allData, specRoot) { * @param {object} rootContent - The root's parsed content * @param {function} refTypeResolver - The resolver function to test if node has a reference * @param {object} components - The global components object + * @param {string} version - The current version * @returns {object} The components object related to the file */ -function generateComponentsObject (documentContext, rootContent, refTypeResolver, components) { +function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) { let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => { return value.keyInComponents.length !== 0; }); @@ -343,7 +339,8 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver setValueInComponents( value.keyInComponents, components, - getContentFromTrace(documentContext.nodeContents[key], partial) + getContentFromTrace(documentContext.nodeContents[key], partial), + version ); }); [rootContent, components].forEach((contentData) => { @@ -391,7 +388,8 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver setValueInComponents( refData.keyInComponents, components, - refData.nodeContent + refData.nodeContent, + version ); } } @@ -424,25 +422,26 @@ module.exports = { * @param {object} specRoot - root file information * @param {Array} allData - array of { path, content} objects * @param {Array} origin - process origin (BROWSER or node) + * @param {string} version - The version we are using * @returns {object} - Detect root files result object */ - getBundleContentAndComponents: function (specRoot, allData, origin) { + getBundleContentAndComponents: function (specRoot, allData, origin, version) { if (origin === BROWSER) { path = pathBrowserify; } let algorithm = new DFS(), components = {}, rootContextData; - rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => { - return getNodeContentAndReferences(currentNode, allData, specRoot); + return getNodeContentAndReferences(currentNode, allData, specRoot, version); }); - components = generateComponentsWrapper(specRoot.parsed.oasObject); + components = generateComponentsWrapper(specRoot.parsed.oasObject, version); generateComponentsObject( rootContextData, rootContextData.nodeContents[specRoot.fileName], isExtRef, - components + components, + version ); return { fileContent: rootContextData.nodeContents[specRoot.fileName], diff --git a/lib/common/versionUtils.js b/lib/common/versionUtils.js index 0eb72e8..5c48d27 100644 --- a/lib/common/versionUtils.js +++ b/lib/common/versionUtils.js @@ -4,6 +4,8 @@ const VERSION_30 = { key: 'openapi', version: '3.0' }, GENERIC_VERSION2 = { key: 'swagger', version: '2.' }, GENERIC_VERSION3 = { key: 'openapi', version: '3.' }, DEFAULT_SPEC_VERSION = VERSION_30.version, + SWAGGER_VERSION = VERSION_20.version, + VERSION_3_1 = VERSION_31.version, fs = require('fs'); /** @@ -257,5 +259,7 @@ module.exports = { filterOptionsByVersion, isSwagger, compareVersion, - getVersionRegexBySpecificationVersion + getVersionRegexBySpecificationVersion, + SWAGGER_VERSION, + VERSION_3_1 }; diff --git a/lib/jsonPointer.js b/lib/jsonPointer.js index bb80cdc..ed18e28 100644 --- a/lib/jsonPointer.js +++ b/lib/jsonPointer.js @@ -6,7 +6,9 @@ const slashes = /\//g, escapedTilde = /~0/g, jsonPointerLevelSeparator = '/', escapedTildeString = '~0', - { getKeyInComponents30 } = require('./30XUtils/componentsParentMatcher'); + { getKeyInComponents30 } = require('./30XUtils/componentsParentMatcher'), + { getKeyInComponents31 } = require('./31XUtils/componentsParentMatcher'), + { VERSION_3_1 } = require('./common/versionUtils'); /** * Encodes a filepath name so it can be a json pointer @@ -47,10 +49,17 @@ function jsonPointerDecodeAndReplace(filePathName) { * @param {string} version - The current spec version * @returns {Array} - the calculated keys in an array representing each nesting property name */ -function getKeyInComponents(traceFromParent, filePathName, localPath) { - const localPart = localPath ? `${localPointer}${localPath}` : ''; +function getKeyInComponents(traceFromParent, filePathName, localPath, version) { + const localPart = localPath ? `${localPointer}${localPath}` : '', + is31 = version === VERSION_3_1; let result; - result = getKeyInComponents30(traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace); + + if (is31) { + result = getKeyInComponents31(traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace); + } + else { + result = getKeyInComponents30(traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace); + } return result.map(generateObjectName); } diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 5af3736..8148208 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -6,7 +6,7 @@ const { ParseError } = require('./common/ParseError.js'); const { formatDataPath, checkIsCorrectType, isKnownType } = require('./common/schemaUtilsCommon.js'), - { getConcreteSchemaUtils } = require('./common/versionUtils.js'), + { getConcreteSchemaUtils, SWAGGER_VERSION } = require('./common/versionUtils.js'), async = require('async'), sdk = require('postman-collection'), schemaFaker = require('../assets/json-schema-faker.js'), @@ -4874,9 +4874,9 @@ module.exports = { * * @returns {object} process result { rootFile, bundledContent } */ - getBundledFileData(parsedRootFiles, inputData, origin, format) { + getBundledFileData(parsedRootFiles, inputData, origin, format, version) { const data = parsedRootFiles.map((root) => { - let bundleData = getBundleContentAndComponents(root, inputData, origin); + let bundleData = getBundleContentAndComponents(root, inputData, origin, version); return bundleData; }); @@ -4904,10 +4904,13 @@ module.exports = { let parsedContent = parseFileOrThrow(rootFile.content); return { fileName: rootFile.fileName, content: rootFile.content, parsed: parsedContent }; }).filter((rootWithParsedContent) => { - return compareVersion(version, rootWithParsedContent.parsed.oasObject.openapi); + let fileVersion = version === SWAGGER_VERSION ? + rootWithParsedContent.parsed.oasObject.swagger : + rootWithParsedContent.parsed.oasObject.openapi; + return compareVersion(version, fileVersion); }), data = toBundle ? - this.getBundledFileData(parsedRootFiles, inputData, origin, bundleFormat) : + this.getBundledFileData(parsedRootFiles, inputData, origin, bundleFormat, version) : this.getRelatedFilesData(parsedRootFiles, inputData, origin); return data; diff --git a/test/data/toBundleExamples/referenced_paths_31/expected.json b/test/data/toBundleExamples/referenced_paths_31/expected.json new file mode 100644 index 0000000..189b022 --- /dev/null +++ b/test/data/toBundleExamples/referenced_paths_31/expected.json @@ -0,0 +1,108 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/pets": { + "$ref": "#/components/pathItems/_path.yaml" + } + }, + "components": { + "schemas": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "pathItems": { + "_path.yaml": { + "get": { + "description": "Returns pets based on ID", + "summary": "Find pets by ID", + "operationId": "getPetsById", + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to use", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "simple" + } + ] + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_paths_31/path.yaml b/test/data/toBundleExamples/referenced_paths_31/path.yaml new file mode 100644 index 0000000..0f9fd29 --- /dev/null +++ b/test/data/toBundleExamples/referenced_paths_31/path.yaml @@ -0,0 +1,29 @@ +get: + description: Returns pets based on ID + summary: Find pets by ID + operationId: getPetsById + responses: + '200': + description: pet response + content: + application/json: + schema: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +parameters: +- name: id + in: path + description: ID of pet to use + required: true + schema: + type: array + items: + type: string + style: simple diff --git a/test/data/toBundleExamples/referenced_paths_31/root.yaml b/test/data/toBundleExamples/referenced_paths_31/root.yaml new file mode 100644 index 0000000..777a24e --- /dev/null +++ b/test/data/toBundleExamples/referenced_paths_31/root.yaml @@ -0,0 +1,41 @@ + +openapi: "3.1.0" +info: + version: 1.0.0 + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification + termsOfService: http://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +paths: + /pets: + "$ref": "./path.yaml" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/test/unit/bundle31.test.js b/test/unit/bundle31.test.js new file mode 100644 index 0000000..65a3da4 --- /dev/null +++ b/test/unit/bundle31.test.js @@ -0,0 +1,42 @@ +const expect = require('chai').expect, + Converter = require('../../index.js'), + fs = require('fs'), + path = require('path'), + BUNDLES_FOLDER = '../data/toBundleExamples', + pathItem31 = path.join(__dirname, BUNDLES_FOLDER + '/referenced_paths_31'); + + +describe('bundle files method - 3.1', function () { + it('Should return bundled file as json - Path item object', async function () { + let contentRootFile = fs.readFileSync(pathItem31 + '/root.yaml', 'utf8'), + user = fs.readFileSync(pathItem31 + '/path.yaml', 'utf8'), + expected = fs.readFileSync(pathItem31 + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.1', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/path.yaml', + content: user + } + ], + options: {}, + bundleFormat: 'JSON' + }; + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.1'); + expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); + }); +});