Skip to content

fix(Paths): Solve circular references #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions lib/open-api-mocker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const jsonRefs = require('json-refs');
const SwaggerParser = require('@apidevtools/swagger-parser');

const { Parser: OpenApiParser } = require('./openapi');
const { Parser: ServersParser } = require('./servers');
Expand All @@ -10,6 +10,7 @@ const OpenAPISchemaInvalid = require('./errors/openapi-schema-invalid-error');

const optionsBuilder = require('./utils/options-builder');
const ExplicitSchemaLoader = require('./schema-loaders/explicit-loader');
const replaceCircularReferencesFromObject = require('./utils/replace-circular-references-from-object');

class OpenApiMocker {

Expand Down Expand Up @@ -41,16 +42,17 @@ class OpenApiMocker {
this.schema = await this.schema;

try {
const parsedSchemas = await jsonRefs.resolveRefs(this.schema);

this.schema = parsedSchemas.resolved;
this.schema = await SwaggerParser.validate(this.schema);

const openApiParser = new OpenApiParser();
const openapi = openApiParser.parse(this.schema);

const serversParser = new ServersParser();
const servers = serversParser.parse(this.schema);

const defaultValue = {'type' : 'string'}
this.schema = replaceCircularReferencesFromObject(this.schema, defaultValue, 3);

const pathsParser = new PathsParser();
const paths = pathsParser.parse(this.schema);

Expand Down
78 changes: 78 additions & 0 deletions lib/utils/replace-circular-references-from-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Returns the given object without circular references.
* Replaces circular references with the given default value if the number of circular references exceeds the given depth limit.
* @param {Object} obj - The object to remove circular references from.
* @param {*} defaultValue - The default value to use for replacing circular references.
* @param {number} depthLimit - The maximum number of circular references allowed before using the default value.
* @returns {Object} - The object without circular references.
*/
function replaceCircularReferencesFromObject(obj, defaultValue, depthLimit) {
const circularReferenceReplacer = new CircularReferenceReplacer(defaultValue, depthLimit);
const objWithoutCircularReferences = circularReferenceReplacer.replace(obj);
return objWithoutCircularReferences;
}

class CircularReferenceReplacer {
#depthLimit;
#defaultValue;

/**
* @param {*} defaultValue - The default value to use for replacing circular references.
* @param {number} depthLimit - The maximum number of circular references allowed before using the default value.
*/
constructor(defaultValue, depthLimit) {
this.#depthLimit = depthLimit;
this.#defaultValue = defaultValue;
}

replace(obj) {
return this.#replaceRecursively(obj, []);
}

#replaceRecursively(obj, ancestors) {
if (!this.#isJSONObjectOrArray(obj)) {
return obj;
}
if (this.#hasCircularReferences(obj, ancestors) && this.#exceedsDepthLimit(obj, ancestors)) {
return this.#defaultValue;
}
ancestors.push(obj); // push the original reference
obj = Array.isArray(obj) ? [...obj] : { ...obj };
for (let key in obj) {
obj[key] = this.#replaceRecursively(
obj[key],
[...ancestors] // copy of ancestors avoiding mutation by siblings
);
}
return obj;
}

#hasCircularReferences(obj, ancestors) {
const hasCircularReferences = ancestors.includes(obj);
return hasCircularReferences;
}

#exceedsDepthLimit(obj, ancestors) {
const exceedsCircularReferencesLimit = this.#countOccurrences(obj, ancestors) >= this.#depthLimit;
return exceedsCircularReferencesLimit;
}

#countOccurrences(value, array) {
const count = array.reduce((accumulator, currentValue) => (currentValue === value ? accumulator + 1 : accumulator), 0);
return count;
}

#isJSONObjectOrArray(obj) {
return (
typeof obj === 'object' &&
obj !== null &&
!(obj instanceof Boolean) &&
!(obj instanceof Date) &&
!(obj instanceof Number) &&
!(obj instanceof RegExp) &&
!(obj instanceof String)
);
}
}

module.exports = replaceCircularReferencesFromObject;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"sinon": "^15.1.0"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@faker-js/faker": "^8.0.2",
"ajv": "^6.12.6",
"ajv-openapi": "^2.0.0",
Expand All @@ -48,7 +49,6 @@
"cors": "^2.8.5",
"express": "^4.18.2",
"js-yaml": "^4.1.0",
"json-refs": "^3.0.15",
"lllog": "^1.1.2",
"micro-memoize": "^4.1.2",
"parse-prefer-header": "^1.0.0",
Expand Down