Skip to content

Commit

Permalink
fix(reference): add support for external cycles detection in OpenAPI …
Browse files Browse the repository at this point in the history
…3.0.x dereference strategy (#3870)

Refs #3863
  • Loading branch information
char0n committed Feb 28, 2024
1 parent be012d7 commit 0735471
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 3 deletions.
Expand Up @@ -62,13 +62,22 @@ const OpenApi3_0DereferenceVisitor = stampit({
reference: null,
options: null,
ancestors: null,
refractCache: null,
},
init({ indirections = [], reference, namespace, options, ancestors = new AncestorLineage() }) {
init({
indirections = [],
reference,
namespace,
options,
ancestors = new AncestorLineage(),
refractCache = new Map(),
}) {
this.indirections = indirections;
this.namespace = namespace;
this.reference = reference;
this.options = options;
this.ancestors = new AncestorLineage(...ancestors);
this.refractCache = refractCache;
},
methods: {
toBaseURI(uri: string): string {
Expand Down Expand Up @@ -154,15 +163,20 @@ const OpenApi3_0DereferenceVisitor = stampit({
// applying semantics to a fragment
if (isPrimitiveElement(referencedElement)) {
const referencedElementType = toValue(referencingElement.meta.get('referenced-element'));
const cacheKey = `${referencedElementType}-${toValue(identityManager.identify(referencedElement))}`;

if (isReferenceLikeElement(referencedElement)) {
if (this.refractCache.has(cacheKey)) {
referencedElement = this.refractCache.get(cacheKey);
} else if (isReferenceLikeElement(referencedElement)) {
// handling indirect references
referencedElement = ReferenceElement.refract(referencedElement);
referencedElement.setMetaProperty('referenced-element', referencedElementType);
this.refractCache.set(cacheKey, referencedElement);
} else {
// handling direct references
const ElementClass = this.namespace.getElementClass(referencedElementType);
referencedElement = ElementClass.refract(referencedElement);
this.refractCache.set(cacheKey, referencedElement);
}
}

Expand All @@ -188,6 +202,7 @@ const OpenApi3_0DereferenceVisitor = stampit({
indirections: [...this.indirections],
options: this.options,
ancestors: ancestorsLineage,
refractCache: this.refractCache,
});
referencedElement = await visitAsync(referencedElement, visitor, {
keyMap,
Expand Down Expand Up @@ -277,7 +292,14 @@ const OpenApi3_0DereferenceVisitor = stampit({

// applying semantics to a referenced element
if (isPrimitiveElement(referencedElement)) {
referencedElement = PathItemElement.refract(referencedElement);
const cacheKey = `pathItem-${toValue(identityManager.identify(referencedElement))}`;

if (this.refractCache.has(cacheKey)) {
referencedElement = this.refractCache.get(cacheKey);
} else {
referencedElement = PathItemElement.refract(referencedElement);
this.refractCache.set(cacheKey, referencedElement);
}
}

// detect direct or indirect reference
Expand All @@ -302,6 +324,7 @@ const OpenApi3_0DereferenceVisitor = stampit({
indirections: [...this.indirections],
options: this.options,
ancestors: ancestorsLineage,
refractCache: this.refractCache,
});
referencedElement = await visitAsync(referencedElement, visitor, {
keyMap,
Expand Down
@@ -0,0 +1,13 @@
{
"summary": "path item summary",
"description": "path item description",
"get": {
"callbacks": {
"myCallback": {
"{$request.query.queryUrl}": {
"$ref": "#"
}
}
}
}
}
@@ -0,0 +1,8 @@
{
"openapi": "3.0.3",
"paths": {
"/path1": {
"$ref": "./ex.json"
}
}
}
Expand Up @@ -2,6 +2,7 @@ import path from 'node:path';
import { assert } from 'chai';
import { toValue } from '@swagger-api/apidom-core';
import { mediaTypes } from '@swagger-api/apidom-ns-openapi-3-0';
import { evaluate } from '@swagger-api/apidom-json-pointer';

import { loadJsonFile } from '../../../../helpers';
import { dereference } from '../../../../../src';
Expand Down Expand Up @@ -114,6 +115,24 @@ describe('dereference', function () {
});
});

context('given $ref field pointing to external cycles', function () {
const fixturePath = path.join(rootFixturePath, 'external-cycle');

specify('should dereference', async function () {
const rootFilePath = path.join(fixturePath, 'root.json');
const dereferenced = await dereference(rootFilePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const parent = evaluate('/0/paths/~1path1/get', dereferenced);
const cyclicParent = evaluate(
'/0/paths/~1path1/get/callbacks/myCallback/{$request.query.queryUrl}/get',
dereferenced,
);

assert.strictEqual(parent, cyclicParent);
});
});

context('given $ref field pointing to external indirections', function () {
const fixturePath = path.join(rootFixturePath, 'external-indirections');

Expand Down
@@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"parent": {
"$ref": "#"
}
}
}
@@ -0,0 +1,10 @@
{
"openapi": "3.0.3",
"components": {
"schemas": {
"externalSchema": {
"$ref": "./ex.json"
}
}
}
}
Expand Up @@ -84,6 +84,27 @@ describe('dereference', function () {
});
});

context('given Reference Objects pointing to external cycles', function () {
const fixturePath = path.join(rootFixturePath, 'external-cycle');

specify('should dereference', async function () {
const rootFilePath = path.join(fixturePath, 'root.json');
const dereferenced = await dereference(rootFilePath, {
parse: { mediaType: mediaTypes.latest('json') },
});
const parent = evaluate(
'/0/components/schemas/externalSchema/properties',
dereferenced,
);
const cyclicParent = evaluate(
'/0/components/schemas/externalSchema/properties/parent/properties',
dereferenced,
);

assert.strictEqual(parent, cyclicParent);
});
});

context('given Reference Objects pointing to external indirections', function () {
const fixturePath = path.join(rootFixturePath, 'external-indirections');

Expand Down

0 comments on commit 0735471

Please sign in to comment.