Merge pull request #595 from postmanlabs/fix/CircularReferences

Fix circular references in bundle
This commit is contained in:
Erik Mendoza
2022-07-08 18:25:11 -05:00
committed by GitHub
8 changed files with 203 additions and 195 deletions

View File

@@ -1,187 +0,0 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0.10",
"title": "Petstore 224",
"description": "A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "Postman"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
"x-postman-projectname": "PostmanPetstore"
},
"paths": {
"/pets": {
"parameters": [
{
"name": "GlobalParam",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "GlobalCookie",
"in": "cookie",
"schema": {
"type": "string"
}
}
],
"post": {
"description": "Creates a new pet in the store. Duplicates are allowed",
"operationId": "addPet",
"requestBody": {
"description": "Pet to add to the store",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"responses": {
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"examples": {
"Single Droplet Create Request": {
"$ref": "#/components/examples/_example2.yaml-_droplet_create_request"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Pet": {
"type": "object",
"description": "A pet",
"required": [
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"typeOf": {
"type": "string",
"enum": [
"Cat",
"Dog",
"Bird",
"Reptile"
]
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
},
"StoreItem": {
"description": "Single store item",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"typeOf": {
"type": "string",
"enum": [
"Cat",
"Dog",
"Bird",
"Reptile"
]
},
"tag": {
"type": "string"
},
"value": {
"type": "number",
"format": "float",
"description": "price"
},
"saleValue": {
"type": "number",
"format": "float",
"description": "price if on sale"
},
"onSale": {
"type": "boolean",
"default": false,
"description": "True if item is on sale, false if not."
}
}
},
"Error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
},
"examples": {
"_example2.yaml-_droplet_create_request": {
"value": {
"name": "example.com",
"region": "nyc3",
"size": "s-1vcpu-1gb",
"image": "ubuntu-20-04-x64",
"ssh_keys": [
289794,
"3b:16:e4:bf:8b:00:8b:b8:59:8c:a9:d3:f0:19:fa:45"
],
"backups": true,
"ipv6": true,
"monitoring": true,
"tags": [
"env:prod",
"web"
],
"user_data": "#cloud-config\nruncmd:\n - touch /test.txt\n",
"vpc_uuid": "760e09ef-dc84-11e8-981e-3cfdfeaae000"
}
}
}
}
}

View File

@@ -550,6 +550,18 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
}
});
});
return {
resRoot: traverseUtility(rootContent).map(function () {
if (this.circular) {
this.update({ [this.key]: '- Circular' });
}
}),
newComponents: traverseUtility(components).map(function () {
if (this.circular) {
this.update({ [this.key]: '- Circular' });
}
})
};
}
/**
@@ -634,6 +646,7 @@ module.exports = {
let algorithm = new DFS(),
components = {},
commonPathFromData = '',
finalElements = {},
rootContextData;
commonPathFromData = Utils.findCommonSubpath(allData.map((fileData) => {
return fileData.fileName;
@@ -653,7 +666,7 @@ module.exports = {
specRoot.parsed.oasObject,
rootContextData.nodeContents
);
generateComponentsObject(
finalElements = generateComponentsObject(
rootContextData,
rootContextData.nodeContents[specRoot.fileName],
isExtRef,
@@ -661,8 +674,8 @@ module.exports = {
version
);
return {
fileContent: rootContextData.nodeContents[specRoot.fileName],
components,
fileContent: finalElements.resRoot,
components: finalElements.newComponents,
fileName: specRoot.fileName,
referenceMap: getReferenceMap(rootContextData.globalReferences)
};

View File

@@ -1,4 +1,5 @@
const VERSION_30 = { key: 'openapi', version: '3.0' },
const _ = require('lodash'),
VERSION_30 = { key: 'openapi', version: '3.0' },
VERSION_31 = { key: 'openapi', version: '3.1' },
VERSION_20 = { key: 'swagger', version: '2.0' },
GENERIC_VERSION2 = { key: 'swagger', version: '2.' },
@@ -306,6 +307,20 @@ function getBundleRulesDataByVersion(version) {
}
}
/**
* Gets the version of a parsed Spec
* @param {object} spec The parsed openapi spec
* @returns {object} The bundling rules related with the spec
*/
function getVersionFromSpec(spec) {
if (!_.isNil(spec) && _.has(spec, 'swagger')) {
return spec.swagger;
}
if (!_.isNil(spec) && _.has(spec, 'openapi')) {
return spec.openapi;
}
}
module.exports = {
getSpecVersion,
getConcreteSchemaUtils,
@@ -316,5 +331,6 @@ module.exports = {
SWAGGER_VERSION,
VERSION_3_1,
validateSupportedVersion,
getBundleRulesDataByVersion
getBundleRulesDataByVersion,
getVersionFromSpec
};

View File

@@ -6,7 +6,7 @@ var yaml = require('js-yaml'),
pathBrowserify = require('path-browserify'),
resolver = require('oas-resolver-browser'),
yamlParse = require('yaml'),
{ compareVersion } = require('./common/versionUtils.js');
{ compareVersion, getVersionFromSpec } = require('./common/versionUtils.js');
const BROWSER = 'browser',
YAML_FORMAT = 'yaml',
JSON_FORMAT = 'json',
@@ -131,7 +131,7 @@ module.exports = {
}
if (inputValidation.validateSpec(oasObject, options).result) {
if (specificationVersion) {
if (compareVersion(specificationVersion, oasObject.openapi)) {
if (compareVersion(specificationVersion, getVersionFromSpec(oasObject))) {
rootFilesArray.push(filePath.fileName);
}
}

View File

@@ -0,0 +1,76 @@
{
"openapi": "3.0.2",
"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",
"schema": {
"type": "array",
"items": {
"$ref\"": "./schemas/schemas.yaml"
}
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail"
}
}
}
}
}
},
"components": {
"schemas": {
"_schemas_schemas.yaml-components_schemas_ErrorDetail": {
"type": "object",
"description": "The error detail.",
"properties": {
"code": {
"readOnly": true,
"type": "string",
"description": "The error code."
},
"message": {
"readOnly": true,
"type": "string",
"description": "The error message."
},
"target": {
"readOnly": true,
"type": "string",
"description": "The error target."
},
"details": {
"readOnly": true,
"type": "array",
"items": {
"items": "- Circular"
},
"description": "The error details."
}
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
openapi: "3.0.2"
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
schema:
type: array
items:
$ref": "./schemas/schemas.yaml"
default:
description: unexpected error
schema:
$ref: "./schemas/schemas.yaml#components/schemas/ErrorDetail"

View File

@@ -0,0 +1,24 @@
components:
schemas:
ErrorDetail:
type: object
description: The error detail.
properties:
code:
readOnly: true
type: string
description: The error code.
message:
readOnly: true
type: string
description: The error message.
target:
readOnly: true
type: string
description: The error target.
details:
readOnly: true
type: array
items:
$ref: "#/components/schemas/ErrorDetail"
description: The error details.

View File

@@ -47,7 +47,8 @@ let expect = require('chai').expect,
referencedPath = path.join(__dirname, BUNDLES_FOLDER + '/referenced_path'),
referencedPathSchema = path.join(__dirname, BUNDLES_FOLDER + '/paths_schema'),
exampleValue = path.join(__dirname, BUNDLES_FOLDER + '/example_value'),
example2 = path.join(__dirname, BUNDLES_FOLDER + '/example2');
example2 = path.join(__dirname, BUNDLES_FOLDER + '/example2'),
schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference');
describe('bundle files method - 3.0', function () {
it('Should return bundled file as json - schema_from_response', async function () {
@@ -2622,6 +2623,38 @@ describe('bundle files method - 3.0', function () {
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);
});
it('Should resolve circular reference in schema correctly', async function () {
let contentRootFile = fs.readFileSync(schemaCircularRef + '/root.yaml', 'utf8'),
schema = fs.readFileSync(schemaCircularRef + '/schemas/schemas.yaml', 'utf8'),
expected = fs.readFileSync(schemaCircularRef + '/expected.json', 'utf8'),
input = {
type: 'multiFile',
specificationVersion: '3.0',
rootFiles: [
{
path: '/root.yaml'
}
],
data: [
{
path: '/root.yaml',
content: contentRootFile
},
{
path: '/schemas/schemas.yaml',
content: schema
}
],
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() {