Skip to content

Commit 682eb77

Browse files
authored
Merge pull request #605 from postmanlabs/fix/inlineCircularRefs
Support circularRefs inline
2 parents 9b267a9 + 5fec203 commit 682eb77

File tree

7 files changed

+237
-33
lines changed

7 files changed

+237
-33
lines changed

lib/bundle.js

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let path = require('path'),
2222
{ DFS } = require('./dfs'),
2323
deref = require('./deref.js'),
2424
{ isSwagger, getBundleRulesDataByVersion } = require('./common/versionUtils'),
25-
CIRCULAR_REF_EXT_PROP = 'x-circularRef';
25+
CIRCULAR_OR_REF_EXT_PROP = 'x-orRef';
2626

2727

2828
/**
@@ -481,6 +481,34 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac
481481
return relatedRef;
482482
}
483483

484+
/**
485+
* Verifies if a node has same content as one of the parents so it is a circular ref
486+
* @param {function} traverseContext - The context of the traverse function
487+
* @param {object} contentFromTrace - The resolved content of the node to deref
488+
* @returns {boolean} whether is circular reference or not.
489+
*/
490+
function isCircularReference(traverseContext, contentFromTrace) {
491+
return traverseContext.parents.find((parent) => { return parent.node === contentFromTrace; }) !== undefined;
492+
}
493+
494+
/**
495+
* Modifies content of a node if it is circular reference.
496+
*
497+
* @param {function} traverseContext - The context of the traverse function
498+
* @param {object} documentContext The document context from root
499+
* @returns {undefined} nothing
500+
*/
501+
function handleCircularReference(traverseContext, documentContext) {
502+
let relatedRef = '';
503+
if (traverseContext.circular) {
504+
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, traverseContext.circular.key);
505+
traverseContext.update({ $ref: relatedRef });
506+
}
507+
if (traverseContext.keys && traverseContext.keys.includes(CIRCULAR_OR_REF_EXT_PROP)) {
508+
traverseContext.update({ $ref: traverseContext.node[CIRCULAR_OR_REF_EXT_PROP] });
509+
}
510+
}
511+
484512
/**
485513
* Generates the components object from the documentContext data
486514
* @param {object} documentContext The document context from root
@@ -492,8 +520,9 @@ function findReferenceByMainKeyInTraceFromContext(documentContext, mainKeyInTrac
492520
*/
493521
function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) {
494522
let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => {
495-
return value.keyInComponents.length !== 0;
496-
});
523+
return value.keyInComponents.length !== 0;
524+
}),
525+
circularRefsSet = new Set();
497526
const { COMPONENTS_KEYS } = getBundleRulesDataByVersion(version);
498527
notInLine.forEach(([key, value]) => {
499528
let [, partial] = key.split(localPointer);
@@ -541,6 +570,17 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
541570
if (!contentFromTrace) {
542571
refData.nodeContent = { $ref: `${localPointer + local}` };
543572
}
573+
else if (isCircularReference(this, contentFromTrace)) {
574+
if (refData.inline) {
575+
refData.nodeContent = { [CIRCULAR_OR_REF_EXT_PROP]: tempRef };
576+
circularRefsSet.add(tempRef);
577+
}
578+
else {
579+
refData.node = { [CIRCULAR_OR_REF_EXT_PROP]: refData.reference };
580+
refData.nodeContent = contentFromTrace;
581+
circularRefsSet.add(refData.reference);
582+
}
583+
}
544584
else {
545585
refData.nodeContent = contentFromTrace;
546586
}
@@ -551,39 +591,30 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver
551591
refData.node = hasSiblings ?
552592
_.merge(referenceSiblings, refData.nodeContent) :
553593
refData.nodeContent;
554-
documentContext.globalReferences[property.$ref].reference =
594+
documentContext.globalReferences[tempRef].reference =
555595
resolveJsonPointerInlineNodes(this.parents, this.key);
556596
}
557-
this.update(refData.node);
558-
if (!refData.inline) {
559-
if (documentContext.globalReferences[tempRef].refHasContent) {
560-
setValueInComponents(
561-
refData.keyInComponents,
562-
components,
563-
refData.nodeContent,
564-
COMPONENTS_KEYS
565-
);
566-
}
597+
else if (refData.refHasContent) {
598+
setValueInComponents(
599+
refData.keyInComponents,
600+
components,
601+
refData.nodeContent,
602+
COMPONENTS_KEYS
603+
);
567604
}
605+
this.update(refData.node);
568606
}
569607
}
570608
});
571609
});
572610
return {
573611
resRoot: traverseUtility(rootContent).map(function () {
574-
let relatedRef = '';
575-
if (this.circular) {
576-
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
577-
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
578-
}
612+
handleCircularReference(this, documentContext);
579613
}),
580614
newComponents: traverseUtility(components).map(function () {
581-
let relatedRef = '';
582-
if (this.circular) {
583-
relatedRef = findReferenceByMainKeyInTraceFromContext(documentContext, this.circular.key);
584-
this.update({ $ref: relatedRef, [CIRCULAR_REF_EXT_PROP]: true });
585-
}
586-
})
615+
handleCircularReference(this, documentContext);
616+
}),
617+
circularRefs: [...circularRefsSet]
587618
};
588619
}
589620

@@ -618,7 +649,7 @@ function generateComponentsWrapper(parsedOasObject, version, nodesContent = {})
618649
}
619650

620651
/**
621-
* Generates a map of generated refernce to the original reference
652+
* Generates a map of generated reference to the original reference
622653
*
623654
* @param {object} globalReferences - Global references present at each root file context
624655
* @returns {object} reference map
@@ -712,7 +743,8 @@ module.exports = {
712743
fileContent: finalElements.resRoot,
713744
components: finalElements.newComponents,
714745
fileName: specRoot.fileName,
715-
referenceMap: getReferenceMap(rootContextData.globalReferences)
746+
referenceMap: getReferenceMap(rootContextData.globalReferences),
747+
circularRefs: finalElements.circularRefs
716748
};
717749
},
718750
getReferences,

test/data/toBundleExamples/circular_reference/expected.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"schema": {
2929
"type": "array",
3030
"items": {
31-
"$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail"
31+
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail"
3232
}
3333
}
3434
}
@@ -40,7 +40,7 @@
4040
},
4141
"components": {
4242
"schemas": {
43-
"_schemas_schemas.yaml-components_schemas_ErrorDetail": {
43+
"_schemas_schemas.yaml-_components_schemas_ErrorDetail": {
4444
"type": "object",
4545
"description": "The error detail.",
4646
"properties": {
@@ -63,8 +63,7 @@
6363
"readOnly": true,
6464
"type": "array",
6565
"items": {
66-
"$ref": "#/components/schemas/_schemas_schemas.yaml-components_schemas_ErrorDetail",
67-
"x-circularRef": true
66+
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorDetail"
6867
},
6968
"description": "The error details."
7069
}

test/data/toBundleExamples/circular_reference/root.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ paths:
2525
schema:
2626
type: array
2727
items:
28-
$ref: "./schemas/schemas.yaml#components/schemas/ErrorDetail"
28+
$ref: "./schemas/schemas.yaml#/components/schemas/ErrorDetail"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"openapi": "3.0.2",
3+
"info": {
4+
"version": "1.0.0",
5+
"title": "Swagger Petstore",
6+
"description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
7+
"termsOfService": "http://swagger.io/terms/",
8+
"contact": {
9+
"name": "Swagger API Team",
10+
"email": "apiteam@swagger.io",
11+
"url": "http://swagger.io"
12+
},
13+
"license": {
14+
"name": "Apache 2.0",
15+
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
16+
}
17+
},
18+
"paths": {
19+
"/pets": {
20+
"get": {
21+
"description": "Returns all pets alesuada ac...",
22+
"operationId": "findPets",
23+
"responses": {
24+
"200": {
25+
"description": "An paged array of pets",
26+
"content": {
27+
"application/json": {
28+
"schema": {
29+
"type": "array",
30+
"items": {
31+
"$ref": "#/components/schemas/_schemas_schemas.yaml-_components_schemas_ErrorResponse"
32+
}
33+
}
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}
40+
},
41+
"components": {
42+
"schemas": {
43+
"_schemas_schemas.yaml-_components_schemas_ErrorResponse": {
44+
"title": "Error response",
45+
"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.).",
46+
"type": "object",
47+
"properties": {
48+
"error": {
49+
"type": "object",
50+
"description": "The error detail.",
51+
"properties": {
52+
"code": {
53+
"readOnly": true,
54+
"type": "string",
55+
"description": "The error code."
56+
},
57+
"message": {
58+
"readOnly": true,
59+
"type": "string",
60+
"description": "The error message."
61+
},
62+
"target": {
63+
"readOnly": true,
64+
"type": "string",
65+
"description": "The error target."
66+
},
67+
"details": {
68+
"readOnly": true,
69+
"type": "array",
70+
"items": {
71+
"$ref": "/schemas/schemas.yaml#components/schemas/ErrorDetail"
72+
},
73+
"description": "The error details."
74+
}
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
openapi: "3.0.2"
2+
info:
3+
version: 1.0.0
4+
title: Swagger Petstore
5+
description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification
6+
termsOfService: http://swagger.io/terms/
7+
contact:
8+
name: Swagger API Team
9+
email: apiteam@swagger.io
10+
url: http://swagger.io
11+
license:
12+
name: Apache 2.0
13+
url: https://www.apache.org/licenses/LICENSE-2.0.html
14+
paths:
15+
/pets:
16+
get:
17+
description: Returns all pets alesuada ac...
18+
operationId: findPets
19+
responses:
20+
'200':
21+
description: An paged array of pets
22+
content:
23+
application/json:
24+
schema:
25+
type: array
26+
items:
27+
$ref: "./schemas/schemas.yaml#/components/schemas/ErrorResponse"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
components:
2+
schemas:
3+
ErrorDetail:
4+
type: object
5+
description: The error detail.
6+
properties:
7+
code:
8+
readOnly: true
9+
type: string
10+
description: The error code.
11+
message:
12+
readOnly: true
13+
type: string
14+
description: The error message.
15+
target:
16+
readOnly: true
17+
type: string
18+
description: The error target.
19+
details:
20+
readOnly: true
21+
type: array
22+
items:
23+
$ref: "#components/schemas/ErrorDetail"
24+
description: The error details.
25+
ErrorResponse:
26+
title: "Error response"
27+
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.)."
28+
type: "object"
29+
properties:
30+
error:
31+
description: "The error object."
32+
$ref: "#components/schemas/ErrorDetail"

test/unit/bundle.test.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ let expect = require('chai').expect,
4848
referencedPathSchema = path.join(__dirname, BUNDLES_FOLDER + '/paths_schema'),
4949
exampleValue = path.join(__dirname, BUNDLES_FOLDER + '/example_value'),
5050
example2 = path.join(__dirname, BUNDLES_FOLDER + '/example2'),
51-
schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference');
51+
schemaCircularRef = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference'),
52+
schemaCircularRefInline = path.join(__dirname, BUNDLES_FOLDER + '/circular_reference_inline');
5253

5354
describe('bundle files method - 3.0', function () {
5455
it('Should return bundled file as json - schema_from_response', async function () {
@@ -2646,6 +2647,38 @@ describe('bundle files method - 3.0', function () {
26462647
expect(res.output.specification.version).to.equal('3.0');
26472648
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
26482649
});
2650+
2651+
it('Should resolve circular reference in schema correctly resolved inline', async function () {
2652+
let contentRootFile = fs.readFileSync(schemaCircularRefInline + '/root.yaml', 'utf8'),
2653+
schema = fs.readFileSync(schemaCircularRefInline + '/schemas/schemas.yaml', 'utf8'),
2654+
expected = fs.readFileSync(schemaCircularRefInline + '/expected.json', 'utf8'),
2655+
input = {
2656+
type: 'multiFile',
2657+
specificationVersion: '3.0',
2658+
rootFiles: [
2659+
{
2660+
path: '/root.yaml'
2661+
}
2662+
],
2663+
data: [
2664+
{
2665+
path: '/root.yaml',
2666+
content: contentRootFile
2667+
},
2668+
{
2669+
path: '/schemas/schemas.yaml',
2670+
content: schema
2671+
}
2672+
],
2673+
options: {},
2674+
bundleFormat: 'JSON'
2675+
};
2676+
const res = await Converter.bundle(input);
2677+
expect(res).to.not.be.empty;
2678+
expect(res.result).to.be.true;
2679+
expect(res.output.specification.version).to.equal('3.0');
2680+
expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected);
2681+
});
26492682
});
26502683

26512684
describe('getReferences method when node does not have any reference', function() {

0 commit comments

Comments
 (0)