Support circularRefs inline

Support circularRefs inline
This commit is contained in:
Luis Tejeda
2022-07-13 15:50:39 -05:00
parent 4611e7ba9e
commit 70ea0fe3c1
7 changed files with 229 additions and 27 deletions

View File

@@ -22,7 +22,8 @@ let path = require('path'),
{ DFS } = require('./dfs'),
deref = require('./deref.js'),
{ isSwagger, getBundleRulesDataByVersion } = require('./common/versionUtils'),
CIRCULAR_REF_EXT_PROP = 'x-circularRef';
CIRCULAR_REF_EXT_PROP = 'x-circularRef',
CIRCULAR_Or_REF_EXT_PROP = 'x-orRef';
/**
@@ -481,6 +482,33 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac
return relatedRef;
}
/**
* Verifies if a node has same content than one of the parents so it is a circular ref
* @param {function} traverseContext - The context of the traverse function
* @param {object} contentFromTrace - The resolved content of the node to deref
* @returns {boolean} whether is circular reference or not.
*/
function isCircularReference(traverseContext, contentFromTrace) {
return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined;
}
/**
* Modifies content of a node if it is circular reference.
* @param {function} traverseContext - The context of the traverse function
* @param {object} documentContext The document context from root
* @returns {undefined} nothing
*/
function handleCircularReference(traverseContext, documentContext) {
let relatedRef = '';
if (traverseContext.circular) {
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, traverseContext.circular.key);
traverseContext.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
}
if (traverseContext.keys && traverseContext.keys.includes(CIRCULAR_Or_REF_EXT_PROP)) {
traverseContext.update({ $ref: traverseContext.node[CIRCULAR_Or_REF_EXT_PROP], [CIRCULAR_REF_EXT_PROP]: true });
}
}
/**
* Generates the components object from the documentContext data
* @param {object} documentContext The document context from root
@@ -541,6 +569,16 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
if (!contentFromTrace) {
refData.nodeContent = { $ref: `${localPointer + local}` };
}
else if (isCircularReference(this, contentFromTrace)) {
if (refData.inline) {
refData.nodeContent = { [CIRCULAR_Or_REF_EXT_PROP]: tempRef, [CIRCULAR_REF_EXT_PROP]: true };
}
else {
refData.node = { [CIRCULAR_Or_REF_EXT_PROP]: refData.reference,
[CIRCULAR_REF_EXT_PROP]: true };
refData.nodeContent = contentFromTrace;
}
}
else {
refData.nodeContent = contentFromTrace;
}
@@ -551,38 +589,28 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
refData.node = hasSiblings ?
_.merge(referenceSiblings, refData.nodeContent) :
refData.nodeContent;
documentContext.globalReferences[property.$ref].reference =
documentContext.globalReferences[tempRef].reference =
resolveJsonPointerInlineNodes(this.parents, this.key);
}
this.update(refData.node);
if (!refData.inline) {
if (documentContext.globalReferences[tempRef].refHasContent) {
setValueInComponents(
refData.keyInComponents,
components,
refData.nodeContent,
COMPONENTS_KEYS
);
}
else if (refData.refHasContent) {
setValueInComponents(
refData.keyInComponents,
components,
refData.nodeContent,
COMPONENTS_KEYS
);
}
this.update(refData.node);
}
}
});
});
return {
resRoot: traverseUtility(rootContent).map(function () {
let relatedRef = '';
if (this.circular) {
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
}
handleCircularReference(this, documentContext);
}),
newComponents: traverseUtility(components).map(function () {
let relatedRef = '';
if (this.circular) {
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
}
handleCircularReference(this, documentContext);
})
};
}

View File

@@ -28,7 +28,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail"
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail"
}
}
}
@@ -40,7 +40,7 @@
},
"components": {
"schemas": {
"_schemas_schemas.yaml-components_schemas_ErrorDetail": {
"_schemas_schemas.yaml-_components_schemas_ErrorDetail": {
"type": "object",
"description": "The error detail.",
"properties": {
@@ -63,7 +63,7 @@
"readOnly": true,
"type": "array",
"items": {
"$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail",
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail",
"x-circularRef": true
},
"description": "The error details."

View File

@@ -25,4 +25,4 @@ paths:
schema:
type: array
items:
$ref: "./schemas/schemas.yaml#components/schemas/ErrorDetail"
$ref: "./schemas/schemas.yaml#/components/schemas/ErrorDetail"

View File

@@ -0,0 +1,82 @@
{
"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": "An paged array of pets",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorResponse"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"_schemas_schemas.yaml-_components_schemas_ErrorResponse": {
"title": "Error response",
"description": "Common error response for all Azure Resource Manager APIs to return error details for failed operations. (This also follows the OData error response format.).",
"type": "object",
"properties": {
"error": {
"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": "/schemas/schemas.yaml#components/schemas/ErrorDetail",
"x-circularRef": true
},
"description": "The error details."
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
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: An paged array of pets
content:
application/json:
schema:
type: array
items:
$ref: "./schemas/schemas.yaml#/components/schemas/ErrorResponse"

View File

@@ -0,0 +1,32 @@
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.
ErrorResponse:
title: "Error response"
description: "Common error response for all Azure Resource Manager APIs to return error details for failed operations. (This also follows the OData error response format.)."
type: "object"
properties:
error:
description: "The error object."
$ref: "#components/schemas/ErrorDetail"

View File

@@ -48,7 +48,8 @@ let expect = require('chai').expect,
referencedPathSchema = path.join(__dirname, BUNDLES_FOLDER + '/paths_schema'),
exampleValue = path.join(__dirname, BUNDLES_FOLDER + '/example_value'),
example2 = path.join(__dirname, BUNDLES_FOLDER + '/example2'),
schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference');
schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference'),
schemaCircularRefInline = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference_inline');
describe('bundle files method - 3.0', function () {
it('Should return bundled file as json - schema_from_response', async function () {
@@ -2646,6 +2647,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 resolved inline', async function () {
let contentRootFile = fs.readFileSync(schemaCircularRefInline + '/root.yaml', 'utf8'),
schema = fs.readFileSync(schemaCircularRefInline + '/schemas/schemas.yaml', 'utf8'),
expected = fs.readFileSync(schemaCircularRefInline + '/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() {