Merge pull request #585 from postmanlabs/fix/componentsObjectReferenced20

Support root components referenced from external files
This commit is contained in:
Carlos-Veloz
2022-07-05 10:52:03 -05:00
committed by GitHub
19 changed files with 519 additions and 9 deletions

View File

@@ -247,7 +247,14 @@ function getTraceFromParentKeyInComponents(nodeContext, tempRef, mainKeys, versi
[key, ...parentKeys], [key, ...parentKeys],
nodeTrace = getRootFileTrace(nodeParentsKey), nodeTrace = getRootFileTrace(nodeParentsKey),
componentKey = createComponentMainKey(tempRef, mainKeys), componentKey = createComponentMainKey(tempRef, mainKeys),
keyTraceInComponents = getKeyInComponents(nodeTrace, componentKey, version, commonPathFromData); parentNodeKey = nodeContext.parent.key,
keyTraceInComponents = getKeyInComponents(
nodeTrace,
componentKey,
version,
commonPathFromData,
parentNodeKey
);
return keyTraceInComponents; return keyTraceInComponents;
} }
@@ -518,10 +525,14 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
* Generates the components object wrapper * Generates the components object wrapper
* @param {object} parsedOasObject The parsed root * @param {object} parsedOasObject The parsed root
* @param {string} version - The current version * @param {string} version - The current version
* @param {object} nodesContent - The nodes content
* @returns {object} The components object wrapper * @returns {object} The components object wrapper
*/ */
function generateComponentsWrapper(parsedOasObject, version) { function generateComponentsWrapper(parsedOasObject, version, nodesContent = {}) {
let components = {}; let components = _.isNil(parsedOasObject.components) ?
{} :
parsedOasObject.components,
componentsAreReferenced = components.$ref !== undefined && !_.isEmpty(nodesContent);
if (isSwagger(version)) { if (isSwagger(version)) {
getBundleRulesDataByVersion(version).COMPONENTS_KEYS.forEach((property) => { getBundleRulesDataByVersion(version).COMPONENTS_KEYS.forEach((property) => {
@@ -531,7 +542,10 @@ function generateComponentsWrapper(parsedOasObject, version) {
}); });
} }
else if (parsedOasObject.hasOwnProperty('components')) { else if (parsedOasObject.hasOwnProperty('components')) {
components = parsedOasObject.components; if (componentsAreReferenced) {
components = _.merge(parsedOasObject.components, nodesContent[components.$ref]);
delete components.$ref;
}
} }
return components; return components;
@@ -604,7 +618,11 @@ module.exports = {
rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => { rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => {
return getNodeContentAndReferences(currentNode, allData, specRoot, version, initialMainKeys, commonPathFromData); return getNodeContentAndReferences(currentNode, allData, specRoot, version, initialMainKeys, commonPathFromData);
}); });
components = generateComponentsWrapper(specRoot.parsed.oasObject, version); components = generateComponentsWrapper(
specRoot.parsed.oasObject,
version,
rootContextData.nodeContents
);
generateComponentsObject( generateComponentsObject(
rootContextData, rootContextData,
rootContextData.nodeContents[specRoot.fileName], rootContextData.nodeContents[specRoot.fileName],

View File

@@ -52,9 +52,10 @@ function generateObjectName(filePathName, hash = '') {
* @param {string} mainKey - The generated mainKey for the components * @param {string} mainKey - The generated mainKey for the components
* @param {string} version - The current spec version * @param {string} version - The current spec version
* @param {string} commonPathFromData - The common path in the file's paths * @param {string} commonPathFromData - The common path in the file's paths
* @param {string} parentNodeKey - The key from the parent element of the trace
* @returns {Array} - the calculated keys in an array representing each nesting property name * @returns {Array} - the calculated keys in an array representing each nesting property name
*/ */
function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromData) { function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromData, parentNodeKey = undefined) {
const { const {
CONTAINERS, CONTAINERS,
DEFINITIONS, DEFINITIONS,
@@ -70,9 +71,11 @@ function getKeyInComponents(traceFromParent, mainKey, version, commonPathFromDat
].reverse(), ].reverse(),
traceToKey = [], traceToKey = [],
matchFound = false, matchFound = false,
isRootAndReusableItemsContainer = ROOT_CONTAINERS_KEYS.includes(traceFromParent[0]); hasNotParent = parentNodeKey === undefined,
isRootAndReusableItemsContainer = ROOT_CONTAINERS_KEYS.includes(traceFromParent[0]),
isAComponentKeyReferenced = COMPONENTS_KEYS.includes(traceFromParent[0]) && hasNotParent;
if (isRootAndReusableItemsContainer) { if (isRootAndReusableItemsContainer || isAComponentKeyReferenced) {
return []; return [];
} }

View File

@@ -0,0 +1,4 @@
responses:
$ref: './responses.yaml'
schemas:
$ref: './schemas.yaml'

View File

@@ -0,0 +1,82 @@
{
"openapi": "3.0.0",
"info": {
"title": "Sample API",
"description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.",
"version": "0.1.9"
},
"paths": {
"/hello": {
"get": {
"description": "Returns all pets alesuada ac...",
"operationId": "findPets",
"responses": {
"$ref": "#/components/responses/responseA"
}
}
}
},
"components": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"address": {
"type": "string"
}
}
}
}
}
},
"responseA": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_schemaA.yaml"
}
}
}
},
"responseB": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"fromB": {
"type": "string"
},
"addressFromB": {
"type": "string"
}
}
}
}
}
}
},
"schemas": {
"_schemaA.yaml": {
"type": "object",
"properties": {
"fromA": {
"type": "string"
},
"addressFromA": {
"type": "string"
}
}
},
"schemaB": {
"type": "integer"
}
}
}
}

View File

@@ -0,0 +1,25 @@
"200":
content:
application/json:
schema:
type: object
properties:
name:
type: string
address:
type: string
"responseA":
content:
application/json:
schema:
$ref: './schemaA.yaml'
"responseB":
content:
application/json:
schema:
type: object
properties:
fromB:
type: string
addressFromB:
type: string

View File

@@ -0,0 +1,15 @@
openapi: 3.0.0
info:
title: Sample API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 0.1.9
paths:
/hello:
get:
description: Returns all pets alesuada ac...
operationId: findPets
responses:
$ref: '#/components/responses/responseA'
components:
$ref: './components.yaml'

View File

@@ -0,0 +1,6 @@
type: object
properties:
fromA:
type: string
addressFromA:
type: string

View File

@@ -0,0 +1,2 @@
schemaB:
type: integer

View File

@@ -0,0 +1,22 @@
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,141 @@
{
"swagger": "2.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": {
"get": {
"description": "Returns all pets alesuada ac...",
"operationId": "findPets",
"responses": {
"200": {
"description": "pet response",
"content": {
"application/json": {
"schema": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
},
"definitions": {
"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"
}
}
}
},
"securityDefinitions": {
"type": "oauth2",
"authorizationUrl": "http://swagger.io/api/oauth/dialog",
"flow": "implicit",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
},
"tags": [
{
"name": "Authorization",
"x-bx-tag": "authorization",
"x-bx-priority": true
},
{
"name": "Bx Sign",
"x-bx-tag": "sign_requests"
}
],
"responses": {
"200": {
"description": "A simple string response",
"schema": {
"type": "string"
}
},
"400": {
"description": "A simple string response from 400 code",
"schema": {
"type": "string"
}
}
}
}

View File

@@ -0,0 +1,11 @@
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

View File

@@ -0,0 +1,31 @@
description: Returns all pets alesuada ac...
operationId: findPets
responses:
"200":
description: pet response
content:
application/json:
schema:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
default:
description: unexpected error
content:
application/json:
schema:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -0,0 +1,3 @@
/pets:
get:
"$ref": "./path.yaml"

View File

@@ -0,0 +1,8 @@
'200':
description: A simple string response
schema:
type: string
'400':
description: A simple string response from 400 code
schema:
type: string

View File

@@ -0,0 +1,13 @@
swagger: '2.0'
info:
$ref: './info.yaml'
paths:
"$ref": "./paths/paths.yaml"
definitions:
$ref: './definitions.yaml'
securityDefinitions:
$ref: './securitySchemes.yaml'
tags:
$ref: './tags.yaml'
responses:
$ref: './responses.yaml'

View File

@@ -0,0 +1,6 @@
type: oauth2
authorizationUrl: http://swagger.io/api/oauth/dialog
flow: implicit
scopes:
write:pets: modify pets in your account
read:pets: read your pets

View File

@@ -0,0 +1,6 @@
- name: Authorization
x-bx-tag: authorization
x-bx-priority: true
- name: Bx Sign
x-bx-tag: sign_requests

View File

@@ -43,6 +43,7 @@ let expect = require('chai').expect,
schemaCollisionWRootComponent = path.join(__dirname, BUNDLES_FOLDER + '/schema_collision_w_root_components'), schemaCollisionWRootComponent = path.join(__dirname, BUNDLES_FOLDER + '/schema_collision_w_root_components'),
nestedExamplesAsValue = path.join(__dirname, BUNDLES_FOLDER + '/nested_examples_as_value'), nestedExamplesAsValue = path.join(__dirname, BUNDLES_FOLDER + '/nested_examples_as_value'),
referencedProperties = path.join(__dirname, BUNDLES_FOLDER + '/referenced_properties'), referencedProperties = path.join(__dirname, BUNDLES_FOLDER + '/referenced_properties'),
referencedComponents = path.join(__dirname, BUNDLES_FOLDER + '/referenced_components'),
referencedPath = path.join(__dirname, BUNDLES_FOLDER + '/referenced_path'); referencedPath = path.join(__dirname, BUNDLES_FOLDER + '/referenced_path');
describe('bundle files method - 3.0', function () { describe('bundle files method - 3.0', function () {
@@ -2402,6 +2403,54 @@ describe('bundle files method - 3.0', function () {
expect(res.result).to.be.true; expect(res.result).to.be.true;
expect(res.output.data[0].referenceMap).to.deep.equal(expected); expect(res.output.data[0].referenceMap).to.deep.equal(expected);
}); });
it('Should return bundled file - referenced-components', async function () {
let contentRootFile = fs.readFileSync(referencedComponents + '/root.yaml', 'utf8'),
components = fs.readFileSync(referencedComponents + '/components.yaml', 'utf8'),
responses = fs.readFileSync(referencedComponents + '/responses.yaml', 'utf8'),
schemas = fs.readFileSync(referencedComponents + '/schemas.yaml', 'utf8'),
schemaA = fs.readFileSync(referencedComponents + '/schemaA.yaml', 'utf8'),
expected = fs.readFileSync(referencedComponents + '/expected.json', 'utf8'),
input = {
type: 'multiFile',
specificationVersion: '3.0',
rootFiles: [
{
path: '/root.yaml'
}
],
data: [
{
path: '/root.yaml',
content: contentRootFile
},
{
path: '/components.yaml',
content: components
},
{
path: '/responses.yaml',
content: responses
},
{
path: '/schemas.yaml',
content: schemas
},
{
path: '/schemaA.yaml',
content: schemaA
}
],
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.0');
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
});
}); });
describe('getReferences method when node does not have any reference', function() { describe('getReferences method when node does not have any reference', function() {

View File

@@ -27,7 +27,9 @@ let expect = require('chai').expect,
schemaCollision = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER + schemaCollision = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER +
'/schema_collision_from_responses'), '/schema_collision_from_responses'),
schemaCollisionWRootComponent = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER + schemaCollisionWRootComponent = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER +
'/schema_collision_w_root_components'); '/schema_collision_w_root_components'),
referencedRootComponents = path.join(__dirname, SWAGGER_MULTIFILE_FOLDER +
'/referenced_root_components');
describe('bundle files method - 2.0', function() { describe('bundle files method - 2.0', function() {
it('Should return bundled result from - nestedProperties20', async function() { it('Should return bundled result from - nestedProperties20', async function() {
@@ -906,4 +908,67 @@ describe('bundle files method - 2.0', function() {
expect(res.output.specification.version).to.equal('2.0'); expect(res.output.specification.version).to.equal('2.0');
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
}); });
it('Should return bundled file as json - referenced_root_components', async function () {
let contentRootFile = fs.readFileSync(referencedRootComponents + '/root.yaml', 'utf8'),
definitions = fs.readFileSync(referencedRootComponents + '/definitions.yaml', 'utf8'),
info = fs.readFileSync(referencedRootComponents + '/info.yaml', 'utf8'),
responses = fs.readFileSync(referencedRootComponents + '/responses.yaml', 'utf8'),
securitySchemes = fs.readFileSync(referencedRootComponents + '/securitySchemes.yaml', 'utf8'),
tags = fs.readFileSync(referencedRootComponents + '/tags.yaml', 'utf8'),
paths = fs.readFileSync(referencedRootComponents + '/paths/paths.yaml', 'utf8'),
singlePath = fs.readFileSync(referencedRootComponents + '/paths/path.yaml', 'utf8'),
expected = fs.readFileSync(referencedRootComponents + '/expected.json', 'utf8'),
input = {
type: 'multiFile',
specificationVersion: '2.0',
rootFiles: [
{
path: '/root.yaml'
}
],
data: [
{
path: '/root.yaml',
content: contentRootFile
},
{
path: '/definitions.yaml',
content: definitions
},
{
path: '/info.yaml',
content: info
},
{
path: '/responses.yaml',
content: responses
},
{
path: '/securitySchemes.yaml',
content: securitySchemes
},
{
path: '/tags.yaml',
content: tags
},
{
path: '/paths/paths.yaml',
content: paths
},
{
path: '/paths/path.yaml',
content: singlePath
}
],
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('2.0');
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
});
}); });