Add reusable referenced path item object

Add reusable referenced path item object
This commit is contained in:
Luis Tejeda
2022-06-14 12:25:48 -05:00
parent 5c27400ebc
commit 6ade03062a
9 changed files with 386 additions and 42 deletions

View File

@@ -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
};

View File

@@ -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],

View File

@@ -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
};

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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"
}
]
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
});
});