Skip to content

Commit

Permalink
fix(resolver): fix useCircularStructure option support in OpenAPI 3.1
Browse files Browse the repository at this point in the history
Refs #2755
  • Loading branch information
char0n committed Jan 17, 2023
1 parent 12dd8de commit 8e974b5
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,12 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
props: {
useCircularStructures: true,
allowMetaPatches: false,
ancestors: [],
},
init({
visited = {
SchemaElement: new WeakSet(),
SchemaElementReference: new WeakSet(),
SchemaElementNoReference: new WeakSet(),
},
useCircularStructures,
allowMetaPatches,
}) {
this.visited = visited;
init({ useCircularStructures, allowMetaPatches, ancestors = this.ancestors }) {
this.useCircularStructures = useCircularStructures;
this.allowMetaPatches = allowMetaPatches;
this.ancestors = [...ancestors];
},
methods: {
async ReferenceElement(referenceElement) {
Expand Down Expand Up @@ -245,27 +238,22 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
return mergedResult;
},

async SchemaElement(referencingElement) {
/**
* Skip traversal for already visited schemas.
* visit function detects cycles in path automatically.
*/
if (this.visited.SchemaElementNoReference.has(referencingElement)) {
return false;
}
if (this.visited.SchemaElementReference.has(referencingElement)) {
return undefined;
}
async SchemaElement(referencingElement, key, parent, path, ancestors) {
// compute full ancestors lineage
const ancestorsLineage = [...this.ancestors, ...ancestors];

// skip current referencing schema as $ref keyword was not defined
if (!isStringElement(referencingElement.$ref)) {
// mark current referencing schema as visited
this.visited.SchemaElement.add(referencingElement);
this.visited.SchemaElementNoReference.add(referencingElement);
// skip traversing this schema but traverse all it's child schemas
return undefined;
}

// detect possible cycle and avoid it
if (ancestorsLineage.includes(referencingElement)) {
// skip processing this schema but all it's child schemas
return false;
}

// compute baseURI using rules around $id and $ref keywords
const retrieveURI = this.reference.uri;
const $refBaseURI = resolveSchema$refField(retrieveURI, referencingElement);
Expand All @@ -277,9 +265,6 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c

// ignore resolving external Schema Objects
if (!this.options.resolve.external && isExternal) {
// mark current referencing schema as visited
this.visited.SchemaElement.add(referencingElement);
this.visited.SchemaElementReference.add(referencingElement);
// skip traversing this schema but traverse all it's child schemas
return undefined;
}
Expand Down Expand Up @@ -338,10 +323,6 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
}
}

// mark current referencing schema as visited
this.visited.SchemaElement.add(referencingElement);
this.visited.SchemaElementReference.add(referencingElement);

// detect direct or indirect reference
if (this.indirections.includes(referencedElement)) {
throw new Error('Recursive JSON Pointer detected');
Expand All @@ -354,34 +335,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c
);
}

// detect possible cycle and avoid it
if (!this.useCircularStructures && this.visited.SchemaElement.has(referencedElement)) {
if (url.isHttpUrl(reference.uri) || url.isFileSystemPath(reference.uri)) {
// make the referencing URL or file system path absolute
const absoluteJSONPointerURL = url.resolve(
reference.uri,
referencingElement.$ref?.toValue()
);
referencingElement.set('$ref', absoluteJSONPointerURL);
}
// skip processing this schema and all it's child schemas
return false;
}
// append referencing schema to ancestors lineage
ancestorsLineage.push(referencingElement);

// dive deep into the fragment
const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({
reference,
namespace: this.namespace,
indirections: [...this.indirections],
options: this.options,
// SchemaElementReference must be reset for deep dive, as we want to dereference all indirections
visited: {
SchemaElement: this.visited.SchemaElement,
SchemaElementReference: new WeakSet(),
SchemaElementNoReference: this.visited.SchemaElementNoReference,
},
useCircularStructures: this.useCircularStructures,
allowMetaPatches: this.allowMetaPatches,
ancestors: ancestorsLineage,
});
referencedElement = await visitAsync(referencedElement, visitor, {
keyMap,
Expand All @@ -390,52 +355,66 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c

this.indirections.pop();

// Boolean JSON Schemas
if (isBooleanJsonSchemaElement(referencedElement)) {
const referencedElementClone = referencedElement.clone();
// Boolean JSON Schema
const jsonSchemaBooleanElement = referencedElement.clone();
// annotate referenced element with info about original referencing element
referencedElementClone.setMetaProperty('ref-fields', {
jsonSchemaBooleanElement.setMetaProperty('ref-fields', {
$ref: referencingElement.$ref?.toValue(),
});
// annotate referenced element with info about origin
referencedElementClone.setMetaProperty('ref-origin', reference.uri);
return referencedElementClone;
jsonSchemaBooleanElement.setMetaProperty('ref-origin', reference.uri);

return jsonSchemaBooleanElement;
}

// useCircularStructures option processing
const hasCycle = referencedElement.content.some((memberElement) =>
ancestorsLineage.includes(memberElement)
);
if (hasCycle && !this.useCircularStructures) {
if (url.isHttpUrl(reference.uri) || url.isFileSystemPath(reference.uri)) {
// make the referencing URL or file system path absolute
const absoluteURI = url.resolve(reference.uri, referencingElement.$ref?.toValue());
referencingElement.set('$ref', absoluteURI);
}

// skip processing this schema but traverse all it's child schemas
return undefined;
}

// Schema Object - merge keywords from referenced schema with referencing schema
const mergedResult = new SchemaElement(
const mergedSchemaElement = new SchemaElement(
// @ts-ignore
[...referencedElement.content],
referencedElement.meta.clone(),
referencedElement.attributes.clone()
);
// existing keywords from referencing schema overrides ones from referenced schema
referencingElement.forEach((value, key, item) => {
mergedResult.remove(key.toValue());
mergedResult.content.push(item);
referencingElement.forEach((memberValue, memberKey, member) => {
mergedSchemaElement.remove(memberKey.toValue());
mergedSchemaElement.content.push(member);
});
mergedResult.remove('$ref');
mergedSchemaElement.remove('$ref');

// annotate referenced element with info about original referencing element
mergedResult.setMetaProperty('ref-fields', {
mergedSchemaElement.setMetaProperty('ref-fields', {
$ref: referencingElement.$ref?.toValue(),
});
// annotate fragment with info about origin
mergedResult.setMetaProperty('ref-origin', reference.uri);
// apply meta patches
mergedSchemaElement.setMetaProperty('ref-origin', reference.uri);

// allowMetaPatches option processing
if (this.allowMetaPatches) {
// apply meta patch only when not already applied
if (typeof mergedResult.get('$$ref') === 'undefined') {
const absoluteJSONPointerURL = url.resolve(
reference.uri,
referencingElement.$ref?.toValue()
);
mergedResult.set('$$ref', absoluteJSONPointerURL);
if (typeof mergedSchemaElement.get('$$ref') === 'undefined') {
const absoluteURI = url.resolve(reference.uri, referencingElement.$ref?.toValue());
mergedSchemaElement.set('$$ref', absoluteURI);
}
}

// transclude referencing element with merged referenced element
return mergedResult;
return mergedSchemaElement;
},
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('dereference', () => {

describe('and using HTTP protocol', () => {
test('should make JSON Pointer absolute', async () => {
const fixturePath = path.join(rootFixturePath, 'cycle-external-disabled-http');
const fixturePath = path.join(rootFixturePath, 'cycle-internal-disabled-http');
const dereferenceThunk = async () => {
const httpServer = globalThis.createHTTPServer({ port: 8123, cwd: fixturePath });

Expand Down
48 changes: 45 additions & 3 deletions test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,20 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition should re
"content": {
"application/json": {
"schema": {
"$ref": "https://example.com/petstore.json#/components/schemas/Error",
"properties": {
"code": {
"format": "int32",
"type": "integer",
},
"message": {
"type": "string",
},
},
"required": [
"code",
"message",
],
"type": "object",
},
},
},
Expand Down Expand Up @@ -217,7 +230,23 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition should re
"content": {
"application/json": {
"schema": {
"$ref": "https://example.com/petstore.json#/components/schemas/Pet",
"properties": {
"id": {
"format": "int64",
"type": "integer",
},
"name": {
"type": "string",
},
"tag": {
"type": "string",
},
},
"required": [
"id",
"name",
],
"type": "object",
},
},
},
Expand All @@ -227,7 +256,20 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition should re
"content": {
"application/json": {
"schema": {
"$ref": "https://example.com/petstore.json#/components/schemas/Error",
"properties": {
"code": {
"format": "int32",
"type": "integer",
},
"message": {
"type": "string",
},
},
"required": [
"code",
"message",
],
"type": "object",
},
},
},
Expand Down

0 comments on commit 8e974b5

Please sign in to comment.