From 722251ff21f2b2d81e67d49bd6f411c9b95fa9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Wed, 17 Apr 2024 14:34:05 +0200 Subject: [PATCH] feat(resolver): align dereferencing with ApiDOM Dereference Architecture 2.0 (#3472) APiDOM Deference Architecture 2.0 - https://github.com/swagger-api/apidom/issues/3915 Refs #3467 --- config/jest/jest.unit.config.js | 1 + package-lock.json | 8 +- package.json | 8 +- .../openapi-3-1-swagger-client/index.js | 56 +- .../visitors/all-of.js | 71 +- .../visitors/dereference.js | 630 ++++++++++-------- .../strategies/openapi-3-1-apidom/resolve.js | 30 +- .../path-item-object/index.js | 80 --- .../dereferenced.json | 2 +- .../dereferenced.json | 2 +- .../dereferenced.json | 8 +- .../reference-object/index.js | 70 +- .../dereferenced.json | 8 +- .../dereferenced.json | 7 +- .../dereferenced.json | 7 +- .../dereferenced.json | 8 +- .../dereferenced-no-circular-structures.json | 12 + .../dereferenced-no-circular-structures.json | 15 + .../schema-object/index.js | 336 ++++------ .../__snapshots__/index.js.snap | 32 +- 20 files changed, 666 insertions(+), 725 deletions(-) create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced-no-circular-structures.json create mode 100644 test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced-no-circular-structures.json diff --git a/config/jest/jest.unit.config.js b/config/jest/jest.unit.config.js index eb955f24c..fff76d97e 100644 --- a/config/jest/jest.unit.config.js +++ b/config/jest/jest.unit.config.js @@ -14,4 +14,5 @@ module.exports = { '/__fixtures__/', '/__utils__/', ], + silent: true, }; diff --git a/package-lock.json b/package-lock.json index 0deb0ccba..b02acfc93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,11 @@ "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.22.15", - "@swagger-api/apidom-core": ">=0.99.0 <1.0.0", + "@swagger-api/apidom-core": ">=0.99.1 <1.0.0", "@swagger-api/apidom-error": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-json-pointer": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-reference": ">=0.99.0 <1.0.0", + "@swagger-api/apidom-json-pointer": ">=0.99.1 <1.0.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=0.99.1 <1.0.0", + "@swagger-api/apidom-reference": ">=0.99.1 <1.0.0", "cookie": "~0.6.0", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", diff --git a/package.json b/package.json index c9270c12f..0a81dcf6c 100644 --- a/package.json +++ b/package.json @@ -110,11 +110,11 @@ }, "dependencies": { "@babel/runtime-corejs3": "^7.22.15", - "@swagger-api/apidom-core": ">=0.99.0 <1.0.0", + "@swagger-api/apidom-core": ">=0.99.1 <1.0.0", "@swagger-api/apidom-error": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-json-pointer": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=0.99.0 <1.0.0", - "@swagger-api/apidom-reference": ">=0.99.0 <1.0.0", + "@swagger-api/apidom-json-pointer": ">=0.99.1 <1.0.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=0.99.1 <1.0.0", + "@swagger-api/apidom-reference": ">=0.99.1 <1.0.0", "cookie": "~0.6.0", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", diff --git a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js index 0453bdf44..57a5209bb 100644 --- a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js +++ b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js @@ -1,5 +1,5 @@ /* eslint-disable camelcase */ -import { createNamespace, visit, mergeAllVisitors } from '@swagger-api/apidom-core'; +import { createNamespace, visit, mergeAllVisitors, cloneDeep } from '@swagger-api/apidom-core'; import { ReferenceSet, Reference } from '@swagger-api/apidom-reference/configuration/empty'; import OpenApi3_1DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'; import openApi3_1Namespace, { getNodeType, keyMap } from '@swagger-api/apidom-ns-openapi-3-1'; @@ -14,7 +14,6 @@ const mergeAllVisitorsAsync = mergeAllVisitors[Symbol.for('nodejs.util.promisify const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy.compose({ props: { - useCircularStructures: true, allowMetaPatches: false, parameterMacro: null, modelPropertyMacro: null, @@ -22,7 +21,6 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy ancestors: null, }, init({ - useCircularStructures = this.useCircularStructures, allowMetaPatches = this.allowMetaPatches, parameterMacro = this.parameterMacro, modelPropertyMacro = this.modelPropertyMacro, @@ -30,7 +28,6 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy ancestors = [], } = {}) { this.name = 'openapi-3-1-swagger-client'; - this.useCircularStructures = useCircularStructures; this.allowMetaPatches = allowMetaPatches; this.parameterMacro = parameterMacro; this.modelPropertyMacro = modelPropertyMacro; @@ -41,15 +38,34 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy async dereference(file, options) { const visitors = []; const namespace = createNamespace(openApi3_1Namespace); - const refSet = options.dereference.refSet ?? ReferenceSet(); + const immutableRefSet = options.dereference.refSet ?? ReferenceSet(); + const mutableRefsSet = ReferenceSet(); + let refSet = immutableRefSet; let reference; - if (!refSet.has(file.uri)) { + if (!immutableRefSet.has(file.uri)) { reference = Reference({ uri: file.uri, value: file.parseResult }); - refSet.add(reference); + immutableRefSet.add(reference); } else { // pre-computed refSet was provided as configuration option - reference = refSet.find((ref) => ref.uri === file.uri); + reference = immutableRefSet.find((ref) => ref.uri === file.uri); + } + + /** + * Clone refSet due the dereferencing process being mutable. + * We don't want to mutate the original refSet and the references. + */ + if (options.dereference.immutable) { + immutableRefSet.refs + .map((ref) => + Reference({ + ...ref, + value: cloneDeep(ref.value), + }) + ) + .forEach((ref) => mutableRefsSet.add(ref)); + reference = mutableRefsSet.find((ref) => ref.uri === file.uri); + refSet = mutableRefsSet; } // create main dereference visitor @@ -57,7 +73,6 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy reference, namespace, options, - useCircularStructures: this.useCircularStructures, allowMetaPatches: this.allowMetaPatches, ancestors: this.ancestors, }); @@ -96,13 +111,32 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy }); /** - * Release all memory if this refSet was not provided as a configuration option. + * If immutable option is set, replay refs from the refSet. + */ + if (options.dereference.immutable) { + mutableRefsSet.refs + .filter((ref) => ref.uri.startsWith('immutable://')) + .map((ref) => + Reference({ + ...ref, + uri: ref.uri.replace(/^immutable:\/\//, ''), + }) + ) + .forEach((ref) => immutableRefSet.add(ref)); + reference = immutableRefSet.find((ref) => ref.uri === file.uri); + refSet = immutableRefSet; + } + + /** + * Release all memory if this refSet was not provided as an configuration option. * If provided as configuration option, then provider is responsible for cleanup. */ if (options.dereference.refSet === null) { - refSet.clean(); + immutableRefSet.clean(); } + mutableRefsSet.clean(); + return dereferencedElement; }, }, diff --git a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/all-of.js b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/all-of.js index e82e22660..e26f99574 100644 --- a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/all-of.js +++ b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/all-of.js @@ -1,5 +1,5 @@ -import { isArrayElement, deepmerge, cloneDeep, toValue } from '@swagger-api/apidom-core'; -import { isSchemaElement, SchemaElement } from '@swagger-api/apidom-ns-openapi-3-1'; +import { isArrayElement, deepmerge } from '@swagger-api/apidom-core'; +import { isSchemaElement } from '@swagger-api/apidom-ns-openapi-3-1'; import compose from '../utils/compose.js'; import toPath from '../utils/to-path.js'; @@ -26,11 +26,8 @@ const AllOfVisitor = compose({ // remove allOf keyword if empty if (schemaElement.allOf.isEmpty) { - return new SchemaElement( - schemaElement.content.filter((memberElement) => toValue(memberElement.key) !== 'allOf'), - cloneDeep(schemaElement.meta), - cloneDeep(schemaElement.attributes) - ); + schemaElement.remove('allOf'); + return undefined; } // collect errors if allOf keyword contains anything else than Schema Object @@ -42,37 +39,45 @@ const AllOfVisitor = compose({ return undefined; } - const mergedSchemaElement = deepmerge.all([...schemaElement.allOf.content, schemaElement]); + while (schemaElement.hasKey('allOf')) { + const { allOf } = schemaElement; + schemaElement.remove('allOf'); + const allOfMerged = deepmerge.all([...allOf.content, schemaElement]); - /** - * If there was not an original $$ref value, make sure to remove - * any $$ref value that may exist from the result of `allOf` merges. - */ - if (!schemaElement.hasKey('$$ref')) { - mergedSchemaElement.remove('$$ref'); - } + /** + * If there was not an original $$ref value, make sure to remove + * any $$ref value that may exist from the result of `allOf` merges. + */ + if (!schemaElement.hasKey('$$ref')) { + allOfMerged.remove('$$ref'); + } - /** - * If there was an example keyword in the original definition, - * keep it instead of merging with example from other schema. - */ - if (schemaElement.hasKey('example')) { - const member = mergedSchemaElement.getMember('example'); - member.value = schemaElement.get('example'); - } + /** + * If there was an example keyword in the original schema, + * keep it instead of merging with example from other schema. + */ + if (schemaElement.hasKey('example')) { + const member = allOfMerged.getMember('example'); + if (member) { + member.value = schemaElement.get('example'); + } + } + + /** + * If there was an examples keyword in the original schema, + * keep it instead of merging with examples from other schema. + */ + if (schemaElement.hasKey('examples')) { + const member = allOfMerged.getMember('examples'); + if (member) { + member.value = schemaElement.get('examples'); + } + } - /** - * If there was an examples keyword in the original definition, - * keep it instead of merging with examples from other schema. - */ - if (schemaElement.hasKey('examples')) { - const member = mergedSchemaElement.getMember('examples'); - member.value = schemaElement.get('examples'); + schemaElement.content = allOfMerged.content; } - // remove allOf keyword after the merge - mergedSchemaElement.remove('allOf'); - return mergedSchemaElement; + return undefined; }, }, }, diff --git a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js index 59581b1ac..d96f22776 100644 --- a/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js +++ b/src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js @@ -1,5 +1,6 @@ /* eslint-disable camelcase */ import { + RefElement, isObjectElement, isPrimitiveElement, isStringElement, @@ -14,7 +15,10 @@ import { import { ApiDOMError } from '@swagger-api/apidom-error'; import { isReferenceLikeElement, + isReferenceElement, isBooleanJsonSchemaElement, + isPathItemElement, + isSchemaElement, ReferenceElement, PathItemElement, SchemaElement, @@ -54,16 +58,6 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // initialize element identity manager const identityManager = IdentityManager(); -/** - * Predicate for detecting if element was created by merging referencing - * element with particular element identity with a referenced element. - */ -const wasReferencedBy = (referencingElement) => (element) => - element.meta.hasKey('ref-referencing-element-id') && - element.meta - .get('ref-referencing-element-id') - .equals(toValue(identityManager.identify(referencingElement))); - const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.compose({ props: { useCircularStructures: true, @@ -82,17 +76,12 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c methods: { async ReferenceElement(referencingElement, key, parent, path, ancestors) { try { - const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); - - // skip already identified cycled Path Item Objects - if (includesClasses(['cycle'], referencingElement.$ref)) { + // skip current referencing element as it's already been access + if (this.indirections.includes(referencingElement)) { return false; } - // detect possible cycle in traversal and avoid it - if (ancestorsLineage.includesCycle(referencingElement)) { - return false; - } + const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref)); const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; @@ -116,6 +105,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // possibly non-semantic fragment let referencedElement = jsonPointerEvaluate(jsonPointer, reference.value.result); + referencedElement.id = identityManager.identify(referencedElement); // applying semantics to a fragment if (isPrimitiveElement(referencedElement)) { @@ -138,8 +128,8 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c } // detect direct or indirect reference - if (this.indirections.includes(referencedElement)) { - throw new ApiDOMError('Recursive JSON Pointer detected'); + if (referencingElement === referencedElement) { + throw new ApiDOMError('Recursive Reference Object detected'); } // detect maximum depth of dereferencing @@ -149,110 +139,133 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c ); } - if (!this.useCircularStructures) { - const hasCycles = ancestorsLineage.includes(referencedElement); - if (hasCycles) { - if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { - // make the referencing URL or file system path absolute - const cycledReferenceElement = new ReferenceElement( - { $ref: $refBaseURI }, - cloneDeep(referencingElement.meta), - cloneDeep(referencingElement.attributes) - ); - cycledReferenceElement.get('$ref').classes.push('cycle'); - return cycledReferenceElement; + // detect second deep dive into the same fragment and avoid it + if (ancestorsLineage.includes(referencedElement)) { + reference.refSet.circular = true; + + if (this.options.dereference.circular === 'error') { + throw new ApiDOMError('Circular reference detected'); + } else if (this.options.dereference.circular === 'replace') { + const refElement = new RefElement(referencedElement.id, { + type: 'reference', + uri: reference.uri, + $ref: toValue(referencingElement.$ref), + baseURI: $refBaseURI, + referencingElement, + }); + const replacer = + this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? + this.options.dereference.circularReplacer; + const replacement = replacer(refElement); + + if (isMemberElement(parent)) { + parent.value = replacement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = replacement; // eslint-disable-line no-param-reassign } - // skip processing this reference - return false; + + return !parent ? replacement : false; } } - // append referencing schema to ancestors lineage - directAncestors.add(referencingElement); - - // dive deep into the fragment - const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - ancestors: ancestorsLineage, - allowMetaPatches: this.allowMetaPatches, - useCircularStructures: this.useCircularStructures, - basePath: this.basePath ?? [ - ...toPath([...ancestors, parent, referencingElement]), - '$ref', - ], - }); - referencedElement = await visitAsync(referencedElement, visitor, { - keyMap, - nodeTypeGetter: getNodeType, - }); + /** + * Dive deep into the fragment. + * + * Cases to consider: + * 1. We're crossing document boundary + * 2. Fragment is from non-root document + * 3. Fragment is a Reference Object. We need to follow it to get the eventual value + * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode + */ + const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri; + const shouldDetectCircular = ['error', 'replace'].includes( + this.options.dereference.circular + ); + if ( + (isExternalReference || + isNonRootDocument || + isReferenceElement(referencedElement) || + shouldDetectCircular) && + !ancestorsLineage.includesCycle(referencedElement) + ) { + // append referencing reference to ancestors lineage + directAncestors.add(referencingElement); + + const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + reference, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: ancestorsLineage, + allowMetaPatches: this.allowMetaPatches, + useCircularStructures: this.useCircularStructures, + basePath: this.basePath ?? [ + ...toPath([...ancestors, parent, referencingElement]), + '$ref', + ], + }); + referencedElement = await visitAsync(referencedElement, visitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); - // remove referencing schema from ancestors lineage - directAncestors.delete(referencingElement); + // remove referencing reference from ancestors lineage + directAncestors.delete(referencingElement); + } this.indirections.pop(); - const mergeAndAnnotateReferencedElement = (refedElement) => { - const copy = cloneShallow(refedElement); + const mergedElement = cloneShallow(referencedElement); - // annotate fragment with info about original Reference element - copy.setMetaProperty('ref-fields', { - $ref: toValue(referencingElement.$ref), - description: toValue(referencingElement.description), - summary: toValue(referencingElement.summary), - }); - // annotate fragment with info about origin - copy.setMetaProperty('ref-origin', reference.uri); - // annotate fragment with info about referencing element - copy.setMetaProperty( - 'ref-referencing-element-id', - cloneDeep(identityManager.identify(referencingElement)) - ); + // annotate fragment with info about original Reference element + mergedElement.setMetaProperty('ref-fields', { + $ref: toValue(referencingElement.$ref), + description: toValue(referencingElement.description), + summary: toValue(referencingElement.summary), + }); + // annotate fragment with info about origin + mergedElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + mergedElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)) + ); - // override description and summary (outer has higher priority then inner) - if (isObjectElement(refedElement)) { - if (referencingElement.hasKey('description') && 'description' in refedElement) { - copy.remove('description'); - copy.set('description', referencingElement.get('description')); - } - if (referencingElement.hasKey('summary') && 'summary' in refedElement) { - copy.remove('summary'); - copy.set('summary', referencingElement.get('summary')); - } + // override description and summary (outer has higher priority then inner) + if (isObjectElement(referencedElement)) { + if (referencingElement.hasKey('description') && 'description' in referencedElement) { + mergedElement.remove('description'); + mergedElement.set('description', referencingElement.get('description')); } - - // apply meta patches - if (this.allowMetaPatches && isObjectElement(copy)) { - // apply meta patch only when not already applied - if (!copy.hasKey('$$ref')) { - const baseURI = url.resolve(retrievalURI, $refBaseURI); - copy.set('$$ref', baseURI); - } + if (referencingElement.hasKey('summary') && 'summary' in referencedElement) { + mergedElement.remove('summary'); + mergedElement.set('summary', referencingElement.get('summary')); } + } - return copy; - }; - - // attempting to create cycle - if ( - ancestorsLineage.includes(referencingElement) || - ancestorsLineage.includes(referencedElement) - ) { - const replaceWith = - ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ?? - mergeAndAnnotateReferencedElement(referencedElement); - if (isMemberElement(parent)) { - parent.value = replaceWith; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replaceWith; // eslint-disable-line no-param-reassign + // apply meta patches + if (this.allowMetaPatches && isObjectElement(mergedElement)) { + // apply meta patch only when not already applied + if (!mergedElement.hasKey('$$ref')) { + const baseURI = url.resolve(retrievalURI, $refBaseURI); + mergedElement.set('$$ref', baseURI); } - return false; } - // transclude the element for a fragment - return mergeAndAnnotateReferencedElement(referencedElement); + /** + * Transclude referencing element with merged referenced element. + */ + if (isMemberElement(parent)) { + parent.value = mergedElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = mergedElement; // eslint-disable-line no-param-reassign + } + + /** + * We're at the root of the tree, so we're just replacing the entire tree. + */ + return !parent ? mergedElement : false; } catch (error) { const rootCause = getRootCause(error); const wrappedError = wrapError(rootCause, { @@ -272,23 +285,23 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c async PathItemElement(pathItemElement, key, parent, path, ancestors) { try { - const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); - // ignore PathItemElement without $ref field if (!isStringElement(pathItemElement.$ref)) { return undefined; } - // skip already identified cycled Path Item Objects - if (includesClasses(['cycle'], pathItemElement.$ref)) { + // skip current referencing element as it's already been access + if (this.indirections.includes(pathItemElement)) { return false; } - // detect possible cycle in traversal and avoid it - if (ancestorsLineage.includesCycle(pathItemElement)) { + // skip already identified cycled Path Item Objects + if (includesClasses(['cycle'], pathItemElement.$ref)) { return false; } + const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); + const retrievalURI = this.toBaseURI(toValue(pathItemElement.$ref)); const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; const isExternalReference = !isInternalReference; @@ -311,10 +324,11 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c // possibly non-semantic referenced element let referencedElement = jsonPointerEvaluate(jsonPointer, reference.value.result); + referencedElement.id = identityManager.identify(referencedElement); // applying semantics to a referenced element if (isPrimitiveElement(referencedElement)) { - const cacheKey = `pathItem-${toValue(identityManager.identify(referencedElement))}`; + const cacheKey = `path-item-${toValue(identityManager.identify(referencedElement))}`; if (this.refractCache.has(cacheKey)) { referencedElement = this.refractCache.get(cacheKey); @@ -325,8 +339,8 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c } // detect direct or indirect reference - if (this.indirections.includes(referencedElement)) { - throw new ApiDOMError('Recursive JSON Pointer detected'); + if (pathItemElement === referencedElement) { + throw new ApiDOMError('Recursive Path Item Object reference detected'); } // detect maximum depth of dereferencing @@ -336,54 +350,88 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c ); } - if (!this.useCircularStructures) { - const hasCycles = ancestorsLineage.includes(referencedElement); - if (hasCycles) { - if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { - // make the referencing URL or file system path absolute - const cycledPathItemElement = new PathItemElement( - { $ref: $refBaseURI }, - cloneDeep(pathItemElement.meta), - cloneDeep(pathItemElement.attributes) - ); - cycledPathItemElement.get('$ref').classes.push('cycle'); - return cycledPathItemElement; + // detect second deep dive into the same fragment and avoid it + if (ancestorsLineage.includes(referencedElement)) { + reference.refSet.circular = true; + + if (this.options.dereference.circular === 'error') { + throw new ApiDOMError('Circular reference detected'); + } else if (this.options.dereference.circular === 'replace') { + const refElement = new RefElement(referencedElement.id, { + type: 'path-item', + uri: reference.uri, + $ref: toValue(pathItemElement.$ref), + baseURI: $refBaseURI, + referencingElement: pathItemElement, + }); + const replacer = + this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? + this.options.dereference.circularReplacer; + const replacement = replacer(refElement); + + if (isMemberElement(parent)) { + parent.value = replacement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = replacement; // eslint-disable-line no-param-reassign } - // skip processing this path item and all it's child elements - return false; + + return !parent ? replacement : false; } } - // append referencing schema to ancestors lineage - directAncestors.add(pathItemElement); - - // dive deep into the referenced element - const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - ancestors: ancestorsLineage, - allowMetaPatches: this.allowMetaPatches, - useCircularStructures: this.useCircularStructures, - basePath: this.basePath ?? [...toPath([...ancestors, parent, pathItemElement]), '$ref'], - }); - referencedElement = await visitAsync(referencedElement, visitor, { - keyMap, - nodeTypeGetter: getNodeType, - }); + /** + * Dive deep into the fragment. + * + * Cases to consider: + * 1. We're crossing document boundary + * 2. Fragment is from non-root document + * 3. Fragment is a Path Item Object with $ref field. We need to follow it to get the eventual value + * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode + */ + const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri; + const shouldDetectCircular = ['error', 'replace'].includes( + this.options.dereference.circular + ); + if ( + (isExternalReference || + isNonRootDocument || + (isPathItemElement(referencedElement) && isStringElement(referencedElement.$ref)) || + shouldDetectCircular) && + !ancestorsLineage.includesCycle(referencedElement) + ) { + // append referencing schema to ancestors lineage + directAncestors.add(pathItemElement); + + // dive deep into the referenced element + const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + reference, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + ancestors: ancestorsLineage, + allowMetaPatches: this.allowMetaPatches, + useCircularStructures: this.useCircularStructures, + basePath: this.basePath ?? [...toPath([...ancestors, parent, pathItemElement]), '$ref'], + }); + referencedElement = await visitAsync(referencedElement, visitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); - // remove referencing schema from ancestors lineage - directAncestors.delete(pathItemElement); + // remove referencing schema from ancestors lineage + directAncestors.delete(pathItemElement); + } this.indirections.pop(); - const mergeAndAnnotateReferencedElement = (refedElement) => { - // merge fields from referenced Path Item with referencing one + /** + * Creating a new version of Path Item by merging fields from referenced Path Item with referencing one. + */ + if (isPathItemElement(referencedElement)) { const mergedElement = new PathItemElement( - [...refedElement.content], - cloneDeep(refedElement.meta), - cloneDeep(refedElement.attributes) + [...referencedElement.content], + cloneDeep(referencedElement.meta), + cloneDeep(referencedElement.attributes) ); // existing keywords from referencing PathItemElement overrides ones from referenced element pathItemElement.forEach((value, keyElement, item) => { @@ -413,27 +461,22 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c } } - return mergedElement; - }; + referencedElement = mergedElement; + } - // attempting to create cycle - if ( - ancestorsLineage.includes(pathItemElement) || - ancestorsLineage.includes(referencedElement) - ) { - const replaceWith = - ancestorsLineage.findItem(wasReferencedBy(pathItemElement)) ?? - mergeAndAnnotateReferencedElement(referencedElement); - if (isMemberElement(parent)) { - parent.value = replaceWith; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replaceWith; // eslint-disable-line no-param-reassign - } - return false; + /** + * Transclude referencing element with merged referenced element. + */ + if (isMemberElement(parent)) { + parent.value = referencedElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = referencedElement; // eslint-disable-line no-param-reassign } - // transclude referencing element with merged referenced element - return mergeAndAnnotateReferencedElement(referencedElement); + /** + * We're at the root of the tree, so we're just replacing the entire tree. + */ + return !parent ? referencedElement : undefined; } catch (error) { const rootCause = getRootCause(error); const wrappedError = wrapError(rootCause, { @@ -450,23 +493,18 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c async SchemaElement(referencingElement, key, parent, path, ancestors) { try { - const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); - // skip current referencing schema as $ref keyword was not defined if (!isStringElement(referencingElement.$ref)) { // skip traversing this schema but traverse all it's child schemas return undefined; } - // skip already identified cycled Path Item Objects - if (includesClasses(['cycle'], referencingElement.$ref)) { + // skip current referencing element as it's already been access + if (this.indirections.includes(referencingElement)) { return false; } - // detect possible cycle in traversal and avoid it - if (ancestorsLineage.includesCycle(referencingElement)) { - return false; - } + const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); // compute baseURI using rules around $id and $ref keywords let reference = await this.toReference(url.unsanitize(this.reference.uri)); @@ -476,8 +514,8 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c const file = File({ uri: $refBaseURIStrippedHash }); const isUnknownURI = !this.options.resolve.resolvers.some((r) => r.canRead(file)); const isURL = !isUnknownURI; - const isInternalReference = (uri) => url.stripHash(this.reference.uri) === uri; - const isExternalReference = (uri) => !isInternalReference(uri); + let isInternalReference = url.stripHash(this.reference.uri) === $refBaseURI; + let isExternalReference = !isInternalReference; this.indirections.push(referencingElement); @@ -487,31 +525,46 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c try { if (isUnknownURI || isURL) { // we're dealing with canonical URI or URL with possible fragment + retrievalURI = this.toBaseURI($refBaseURI); const selector = $refBaseURI; - referencedElement = uriEvaluate( - selector, - maybeRefractToSchemaElement(reference.value.result) - ); + const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result); + referencedElement = uriEvaluate(selector, referenceAsSchema); + referencedElement = maybeRefractToSchemaElement(referencedElement); + referencedElement.id = identityManager.identify(referencedElement); + + // ignore resolving internal Schema Objects + if (!this.options.resolve.internal && isInternalReference) { + // skip traversing this schema element but traverse all it's child elements + return undefined; + } + // ignore resolving external Schema Objects + if (!this.options.resolve.external && isExternalReference) { + // skip traversing this schema element but traverse all it's child elements + return undefined; + } } else { // we're assuming here that we're dealing with JSON Pointer here - retrievalURI = this.toBaseURI(toValue($refBaseURI)); + retrievalURI = this.toBaseURI($refBaseURI); + isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; + isExternalReference = !isInternalReference; // ignore resolving internal Schema Objects - if (!this.options.resolve.internal && isInternalReference(retrievalURI)) { + if (!this.options.resolve.internal && isInternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } // ignore resolving external Schema Objects - if (!this.options.resolve.external && isExternalReference(retrievalURI)) { + if (!this.options.resolve.external && isExternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } reference = await this.toReference(url.unsanitize($refBaseURI)); const selector = uriToPointer($refBaseURI); - referencedElement = maybeRefractToSchemaElement( - jsonPointerEvaluate(selector, reference.value.result) - ); + const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result); + referencedElement = jsonPointerEvaluate(selector, referenceAsSchema); + referencedElement = maybeRefractToSchemaElement(referencedElement); + referencedElement.id = identityManager.identify(referencedElement); } } catch (error) { /** @@ -521,45 +574,49 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c if (isURL && error instanceof EvaluationJsonSchemaUriError) { if (isAnchor(uriToAnchor($refBaseURI))) { // we're dealing with JSON Schema $anchor here - retrievalURI = this.toBaseURI(toValue($refBaseURI)); + isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; + isExternalReference = !isInternalReference; // ignore resolving internal Schema Objects - if (!this.options.resolve.internal && isInternalReference(retrievalURI)) { + if (!this.options.resolve.internal && isInternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } // ignore resolving external Schema Objects - if (!this.options.resolve.external && isExternalReference(retrievalURI)) { + if (!this.options.resolve.external && isExternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } reference = await this.toReference(url.unsanitize($refBaseURI)); const selector = uriToAnchor($refBaseURI); - referencedElement = $anchorEvaluate( - selector, - maybeRefractToSchemaElement(reference.value.result) - ); + const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result); + referencedElement = $anchorEvaluate(selector, referenceAsSchema); + referencedElement = maybeRefractToSchemaElement(referencedElement); + referencedElement.id = identityManager.identify(referencedElement); } else { // we're assuming here that we're dealing with JSON Pointer here retrievalURI = this.toBaseURI(toValue($refBaseURI)); + isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; + isExternalReference = !isInternalReference; // ignore resolving internal Schema Objects - if (!this.options.resolve.internal && isInternalReference(retrievalURI)) { + if (!this.options.resolve.internal && isInternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } // ignore resolving external Schema Objects - if (!this.options.resolve.external && isExternalReference(retrievalURI)) { + if (!this.options.resolve.external && isExternalReference) { // skip traversing this schema element but traverse all it's child elements return undefined; } reference = await this.toReference(url.unsanitize($refBaseURI)); const selector = uriToPointer($refBaseURI); - referencedElement = maybeRefractToSchemaElement( - jsonPointerEvaluate(selector, reference.value.result) - ); + const referenceAsSchema = maybeRefractToSchemaElement(reference.value.result); + referencedElement = jsonPointerEvaluate(selector, referenceAsSchema); + referencedElement = maybeRefractToSchemaElement(referencedElement); + referencedElement.id = identityManager.identify(referencedElement); } } else { throw error; @@ -567,7 +624,7 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c } // detect direct or indirect reference - if (this.indirections.includes(referencedElement)) { + if (referencingElement === referencedElement) { throw new ApiDOMError('Recursive Schema Object reference detected'); } @@ -578,50 +635,80 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c ); } - // useCircularStructures option processing - if (!this.useCircularStructures) { - const hasCycles = ancestorsLineage.includes(referencedElement); - if (hasCycles) { - if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { - // make the referencing URL or file system path absolute - const baseURI = url.resolve(retrievalURI, $refBaseURI); - const cycledSchemaElement = new SchemaElement( - { $ref: baseURI }, - cloneDeep(referencingElement.meta), - cloneDeep(referencingElement.attributes) - ); - cycledSchemaElement.get('$ref').classes.push('cycle'); - return cycledSchemaElement; + // detect second deep dive into the same fragment and avoid it + if (ancestorsLineage.includes(referencedElement)) { + reference.refSet.circular = true; + + if (this.options.dereference.circular === 'error') { + throw new ApiDOMError('Circular reference detected'); + } else if (this.options.dereference.circular === 'replace') { + const refElement = new RefElement(referencedElement.id, { + type: 'json-schema', + uri: reference.uri, + $ref: toValue(referencingElement.$ref), + baseURI: url.resolve(retrievalURI, $refBaseURI), + referencingElement, + }); + const replacer = + this.options.dereference.strategyOpts['openapi-3-1']?.circularReplacer ?? + this.options.dereference.circularReplacer; + const replacement = replacer(refElement); + + if (isMemberElement(parent)) { + parent.value = replacement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = replacement; // eslint-disable-line no-param-reassign } - // skip processing this schema and all it's child schemas - return false; + + return !parent ? replacement : false; } } - // append referencing schema to ancestors lineage - directAncestors.add(referencingElement); - - // dive deep into the fragment - const mergeVisitor = OpenApi3_1SwaggerClientDereferenceVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - useCircularStructures: this.useCircularStructures, - allowMetaPatches: this.allowMetaPatches, - ancestors: ancestorsLineage, - basePath: this.basePath ?? [ - ...toPath([...ancestors, parent, referencingElement]), - '$ref', - ], - }); - referencedElement = await visitAsync(referencedElement, mergeVisitor, { - keyMap, - nodeTypeGetter: getNodeType, - }); + /** + * Dive deep into the fragment. + * + * Cases to consider: + * 1. We're crossing document boundary + * 2. Fragment is from non-root document + * 3. Fragment is a Schema Object with $ref field. We need to follow it to get the eventual value + * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode + */ + const isNonRootDocument = url.stripHash(reference.refSet.rootRef.uri) !== reference.uri; + const shouldDetectCircular = ['error', 'replace'].includes( + this.options.dereference.circular + ); + if ( + (isExternalReference || + isNonRootDocument || + (isSchemaElement(referencedElement) && isStringElement(referencedElement.$ref)) || + shouldDetectCircular) && + !ancestorsLineage.includesCycle(referencedElement) + ) { + // append referencing schema to ancestors lineage + directAncestors.add(referencingElement); + + // dive deep into the fragment + const mergeVisitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + reference, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + useCircularStructures: this.useCircularStructures, + allowMetaPatches: this.allowMetaPatches, + ancestors: ancestorsLineage, + basePath: this.basePath ?? [ + ...toPath([...ancestors, parent, referencingElement]), + '$ref', + ], + }); + referencedElement = await visitAsync(referencedElement, mergeVisitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); - // remove referencing schema from ancestors lineage - directAncestors.delete(referencingElement); + // remove referencing schema from ancestors lineage + directAncestors.delete(referencingElement); + } this.indirections.pop(); @@ -638,15 +725,25 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c 'ref-referencing-element-id', cloneDeep(identityManager.identify(referencingElement)) ); - return booleanJsonSchemaElement; + + if (isMemberElement(parent)) { + parent.value = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = booleanJsonSchemaElement; // eslint-disable-line no-param-reassign + } + + return !parent ? booleanJsonSchemaElement : false; } - const mergeAndAnnotateReferencedElement = (refedElement) => { + /** + * Creating a new version of Schema Object by merging fields from referenced Schema Object with referencing one. + */ + if (isSchemaElement(referencedElement)) { // Schema Object - merge keywords from referenced schema with referencing schema const mergedElement = new SchemaElement( - [...refedElement.content], - cloneDeep(refedElement.meta), - cloneDeep(refedElement.attributes) + [...referencedElement.content], + cloneDeep(referencedElement.meta), + cloneDeep(referencedElement.attributes) ); // existing keywords from referencing schema overrides ones from referenced schema referencingElement.forEach((value, keyElement, item) => { @@ -675,27 +772,22 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c } } - return mergedElement; - }; + referencedElement = mergedElement; + } - // attempting to create cycle - if ( - ancestorsLineage.includes(referencingElement) || - ancestorsLineage.includes(referencedElement) - ) { - const replaceWith = - ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ?? - mergeAndAnnotateReferencedElement(referencedElement); - if (isMemberElement(parent)) { - parent.value = replaceWith; // eslint-disable-line no-param-reassign - } else if (Array.isArray(parent)) { - parent[key] = replaceWith; // eslint-disable-line no-param-reassign - } - return false; + /** + * Transclude referencing element with merged referenced element. + */ + if (isMemberElement(parent)) { + parent.value = referencedElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = referencedElement; // eslint-disable-line no-param-reassign } - // transclude referencing element with merged referenced element - return mergeAndAnnotateReferencedElement(referencedElement); + /** + * We're at the root of the tree, so we're just replacing the entire tree. + */ + return !parent ? referencedElement : undefined; } catch (error) { const rootCause = getRootCause(error); const wrappedError = new SchemaRefError( diff --git a/src/resolver/strategies/openapi-3-1-apidom/resolve.js b/src/resolver/strategies/openapi-3-1-apidom/resolve.js index 147d561e5..349cb0770 100644 --- a/src/resolver/strategies/openapi-3-1-apidom/resolve.js +++ b/src/resolver/strategies/openapi-3-1-apidom/resolve.js @@ -1,17 +1,24 @@ /* eslint-disable camelcase */ -import { toValue, transclude, ParseResultElement } from '@swagger-api/apidom-core'; +import { + ParseResultElement, + ObjectElement, + toValue, + transclude, + cloneDeep, +} from '@swagger-api/apidom-core'; import { compile as jsonPointerCompile, evaluate as jsonPointerEvaluate, EvaluationJsonPointerError, InvalidJsonPointerError, } from '@swagger-api/apidom-json-pointer'; -import { OpenApi3_1Element, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; +import { mediaTypes, OpenApi3_1Element } from '@swagger-api/apidom-ns-openapi-3-1'; import { dereferenceApiDOM, url, ReferenceSet, Reference, + options as referenceOptions, } from '@swagger-api/apidom-reference/configuration/empty'; import BinaryParser from '@swagger-api/apidom-reference/parse/parsers/binary'; import OpenApi3_1ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-1'; @@ -26,6 +33,21 @@ import OpenApiJson3_1Parser from '../../apidom/reference/parse/parsers/openapi-j import OpenApiYaml3_1Parser from '../../apidom/reference/parse/parsers/openapi-yaml-3-1/index.js'; import OpenApi3_1SwaggerClientDereferenceStrategy from '../../apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js'; +export const circularReplacer = (refElement) => { + const $refBaseURI = toValue(refElement.meta.get('baseURI')); + const referencingElement = refElement.meta.get('referencingElement'); + + /** + * Removing semantics from the absolutified referencing element by + * using generic ObjectElement to represent the reference. + */ + return new ObjectElement( + { $ref: $refBaseURI }, + cloneDeep(referencingElement.meta), + cloneDeep(referencingElement.attributes) + ); +}; + const resolveOpenAPI31Strategy = async (options) => { const { spec, @@ -123,6 +145,10 @@ const resolveOpenAPI31Strategy = async (options) => { refSet, dereferenceOpts: { errors }, immutable: false, + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, }, }); const transcluded = transclude(fragmentElement, dereferenced, openApiElement); diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/path-item-object/index.js b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/path-item-object/index.js index 8c604ba56..c7eef6068 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/path-item-object/index.js +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/path-item-object/index.js @@ -341,26 +341,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(2); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/direct-internal-circular\/root\.json$/), - $ref: '#/paths/~1path1', - pointer: '/paths/~1path1', - fullPath: ['paths', '/path1', '$ref'], - }); - }); }); describe('given $ref field with with indirect circular internal reference', () => { @@ -375,26 +355,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(3); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/paths/~1path1', - pointer: '/paths/~1path1', - fullPath: ['paths', '/path1', '$ref'], - }); - }); }); describe('given $ref field with with direct circular external reference', () => { @@ -409,26 +369,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/direct-external-circular\/ex\.json$/), - $ref: './root.json#/paths/~1path1', - pointer: '/paths/~1path1', - fullPath: ['paths', '/path1', '$ref'], - }); - }); }); describe('given $ref field with with indirect circular external reference', () => { @@ -443,26 +383,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/indirect-external-circular\/ex2\.json$/), - $ref: './root.json#/paths/~1path1', - pointer: '/paths/~1path1', - fullPath: ['paths', '/path1', '$ref'], - }); - }); }); }); }); diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/direct-external-circular/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/direct-external-circular/dereferenced.json index b473a4b5d..757269c1a 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/direct-external-circular/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/direct-external-circular/dereferenced.json @@ -4,7 +4,7 @@ "components": { "parameters": { "externalRef": { - "$ref": "./root.json#/components/parameters/externalRef", + "$ref": "./ex.json#/externalParameter", "description": "another ref" } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-external-circular/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-external-circular/dereferenced.json index b473a4b5d..b4bf97eee 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-external-circular/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-external-circular/dereferenced.json @@ -4,7 +4,7 @@ "components": { "parameters": { "externalRef": { - "$ref": "./root.json#/components/parameters/externalRef", + "$ref": "./ex1.json#/indirection", "description": "another ref" } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-internal-circular/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-internal-circular/dereferenced.json index 8e877695a..f74e79efe 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-internal-circular/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/__fixtures__/indirect-internal-circular/dereferenced.json @@ -4,19 +4,19 @@ "components": { "parameters": { "userId": { - "$ref": "#/components/parameters/userId", + "$ref": "#/components/parameters/indirection1", "description": "description 1" }, "indirection1": { - "$ref": "#/components/parameters/indirection1", + "$ref": "#/components/parameters/indirection2", "description": "description 1" }, "indirection2": { - "$ref": "#/components/parameters/indirection2", + "$ref": "#/components/parameters/indirection3", "description": "description 1" }, "indirection3": { - "$ref": "#/components/parameters/indirection3", + "$ref": "#/components/parameters/userId", "description": "description 1" } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/index.js b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/index.js index 38af3e449..e723661d0 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/index.js +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/reference-object/index.js @@ -260,7 +260,7 @@ describe('dereference', () => { expect(errors).toHaveLength(1); expect(errors[0]).toMatchObject({ message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ + /^Could not resolve reference: Recursive Reference Object detected/ ), baseDoc: expect.stringMatching(/direct-internal-circular\/root\.json$/), $ref: '#/components/parameters/userId', @@ -282,26 +282,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(4); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/components/parameters/userId', - pointer: '/components/parameters/userId', - fullPath: ['components', 'parameters', 'userId', '$ref'], - }); - }); }); describe('given Reference Objects with direct circular external reference', () => { @@ -316,26 +296,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/direct-external-circular\/ex\.json$/), - $ref: './root.json#/components/parameters/externalRef', - pointer: '/components/parameters/externalRef', - fullPath: ['components', 'parameters', 'externalRef', '$ref'], - }); - }); }); describe('given Reference Objects with indirect circular external reference', () => { @@ -350,26 +310,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive JSON Pointer detected/ - ), - baseDoc: expect.stringMatching(/indirect-external-circular\/ex3\.json$/), - $ref: './root.json#/components/parameters/externalRef', - pointer: '/components/parameters/externalRef', - fullPath: ['components', 'parameters', 'externalRef', '$ref'], - }); - }); }); describe('given Reference Objects with unresolvable reference', () => { @@ -520,15 +460,17 @@ describe('dereference', () => { const errors = []; await dereference(rootFilePath, { - resolve: { maxDepth: 2 }, parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, + dereference: { + dereferenceOpts: { errors }, + maxDepth: 2, + }, }); expect(errors).toHaveLength(1); expect(errors[0]).toMatchObject({ message: expect.stringMatching( - /^Could not resolve reference: Maximum resolution depth/ + /^Could not resolve reference: Maximum dereference depth of/ ), baseDoc: expect.stringMatching(/max-depth\/ex2\.json$/), $ref: './ex3.json#/externalParameter', diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json index d1990bca3..a13a1ef00 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-circular-structures/dereferenced.json @@ -23,13 +23,7 @@ "type": "object", "properties": { "user": { - "$id": "https://swagger.io/schemas/user", - "type": "object", - "properties": { - "profile": { - "$ref": "https://swagger.io/schemas/user-profile" - } - } + "$ref": "https://swagger.io/schemas/user" } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json index 413ab41d6..d5c2360f0 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-pointer-circular-structures/dereferenced.json @@ -20,12 +20,7 @@ "type": "object", "properties": { "user": { - "type": "object", - "properties": { - "profile": { - "$ref": "/home/smartbear/root.json#/components/schemas/UserProfile" - } - } + "$ref": "/home/smartbear/root.json#/components/schemas/User" } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json index 8b5c9b610..79daa8162 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-relative-reference-circular-structures/dereferenced.json @@ -23,12 +23,7 @@ "type": "object", "properties": { "user": { - "type": "object", - "$id": "https://swagger.io/schemas/user", - "properties": { - "profile": { "$ref": "https://swagger.io/schemas/user-profile" } - } - + "$ref": "https://swagger.io/schemas/user" } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json index bd480d701..5bc23ccb6 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-circular-structures/dereferenced.json @@ -23,13 +23,7 @@ "type": "object", "properties": { "user": { - "$id": "urn:uuid:ff564b8a-7a87-4125-8c96-e9f123d6766f", - "type": "object", - "properties": { - "profile": { - "$ref": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" - } - } + "$ref": "urn:uuid:ff564b8a-7a87-4125-8c96-e9f123d6766f" } } } diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced-no-circular-structures.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced-no-circular-structures.json new file mode 100644 index 000000000..2056a7595 --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced-no-circular-structures.json @@ -0,0 +1,12 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "$ref": "/home/smartbear/root.json#/components/schemas/User" + } + } + } + } +] diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced-no-circular-structures.json b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced-no-circular-structures.json new file mode 100644 index 000000000..fdf7deb7e --- /dev/null +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced-no-circular-structures.json @@ -0,0 +1,15 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "$ref": "/home/smartbear/root.json#/components/schemas/User" + }, + "Indirection1": {}, + "Indirection2": {}, + "Indirection3": {} + } + } + } +] diff --git a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js index 6ba9b7226..5f437067c 100644 --- a/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js +++ b/test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js @@ -3,6 +3,7 @@ import { toValue, toJSON } from '@swagger-api/apidom-core'; import { isSchemaElement, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; import { evaluate, escape } from '@swagger-api/apidom-json-pointer'; import { + options as referenceOptions, parse, dereference, dereferenceApiDOM, @@ -14,6 +15,7 @@ import { // eslint-disable-next-line camelcase import OpenApi3_1SwaggerClientDereferenceStrategy from '../../../../../../../../src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js'; import * as jestSetup from '../__utils__/jest.local.setup.js'; +import { circularReplacer } from '../../../../../../../../src/resolver/strategies/openapi-3-1-apidom/resolve.js'; const rootFixturePath = path.join(__dirname, '__fixtures__'); @@ -114,6 +116,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should avoid cycles by skipping transclusion', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, 'cycle-internal-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -123,10 +126,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -136,6 +140,7 @@ describe('dereference', () => { }); test('should avoid cycles by skipping transclusion on single element', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, 'cycle-internal-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -147,10 +152,11 @@ describe('dereference', () => { resolve: { baseURI: '/home/smartbear/root.json' }, parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const [expected] = globalThis.loadJsonFile( @@ -163,6 +169,7 @@ describe('dereference', () => { describe('and using HTTP protocol', () => { test('should make JSON Pointer absolute', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, 'cycle-internal-http-circular-structures' @@ -175,11 +182,10 @@ describe('dereference', () => { await dereference('http://localhost:8123/root.json', { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ - useCircularStructures: false, - }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, }, }) ); @@ -218,6 +224,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should avoid cycles by skipping transclusion', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, 'cycle-internal-advanced-circular-structures' @@ -230,10 +237,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -244,6 +252,7 @@ describe('dereference', () => { describe('and using HTTP protocol', () => { test('should make JSON Pointer absolute', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, 'cycle-internal-advanced-http-circular-structures' @@ -256,11 +265,10 @@ describe('dereference', () => { await dereference('http://localhost:8123/root.json', { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ - useCircularStructures: false, - }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, }, }) ); @@ -303,6 +311,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should avoid cycles by skipping transclusion', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, 'cycle-internal-advanced-circular-structures-2' @@ -315,10 +324,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -329,6 +339,7 @@ describe('dereference', () => { describe('and using HTTP protocol', () => { test('should make JSON Pointer absolute', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, 'cycle-internal-advanced-http-circular-structures-2' @@ -341,11 +352,10 @@ describe('dereference', () => { await dereference('http://localhost:8123/root.json', { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ - useCircularStructures: false, - }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, }, }) ); @@ -384,6 +394,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should avoid cycles by skipping transclusion', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, 'cycle-external-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -394,10 +405,11 @@ describe('dereference', () => { const actual = await dereference('/home/smartbear/root.json', { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -723,6 +735,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, '$id-uri-direct-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -733,10 +746,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -785,6 +799,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$id-uri-enclosing-circular-structures' @@ -798,10 +813,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -851,6 +867,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, '$id-uri-external'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -862,10 +879,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -944,6 +962,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, '$ref-url-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -952,10 +971,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -1006,6 +1026,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$ref-url-relative-reference-circular-structures' @@ -1017,10 +1038,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -1068,6 +1090,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$ref-url-pointer-circular-structures' @@ -1080,10 +1103,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -1191,6 +1215,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$ref-url-resolvable-circular-structures' @@ -1204,10 +1229,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -1309,6 +1335,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join(rootFixturePath, '$ref-urn-circular-structures'); const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { @@ -1317,10 +1344,11 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], }, }); const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); @@ -1452,6 +1480,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$anchor-internal-circular-structures' @@ -1464,9 +1493,10 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, }, }); @@ -1517,6 +1547,7 @@ describe('dereference', () => { describe('and useCircularStructures=false', () => { test('should dereference', async () => { + const useCircularStructures = false; const fixturePath = path.join( rootFixturePath, '$anchor-external-circular-structures' @@ -1531,9 +1562,10 @@ describe('dereference', () => { const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, refSet, }, }); @@ -1756,33 +1788,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(2); - expect(errors.at(0)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/infinite-recursion\/root\.json$/), - $ref: '#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); - expect(errors.at(1)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/infinite-recursion\/root\.json$/), - $ref: '#/components/schemas/UserProfile', - fullPath: ['components', 'schemas', 'UserProfile', '$ref'], - }); - }); }); describe('given Schema Objects with direct circular external reference', () => { @@ -1797,25 +1802,6 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/direct-external-circular\/ex\.json$/), - $ref: './root.json#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); - }); }); describe('given Schema Objects with direct circular internal reference', () => { @@ -1864,63 +1850,32 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-external-circular\/ex3\.json$/), - $ref: './root.json#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); - }); - describe('and useCircularStructures=false', () => { test('should dereference', async () => { - const actual = await dereference(rootFilePath, { + const useCircularStructures = false; + const refSet = await resolve(rootFilePath, { parse: { mediaType: mediaTypes.latest('json') }, - dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], - }, }); - const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - - expect(typeof toJSON(actual)).toBe('string'); - expect(toValue(actual)).toEqual(expected); - }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { + refSet.refs[0].uri = '/home/smartbear/root.json'; + refSet.refs[1].uri = '/home/smartbear/ex1.json'; + refSet.refs[2].uri = '/home/smartbear/ex2.json'; + refSet.refs[3].uri = '/home/smartbear/ex3.json'; + const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - dereferenceOpts: { errors }, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, + refSet, }, }); + const expected = globalThis.loadJsonFile( + path.join(fixturePath, 'dereferenced-no-circular-structures.json') + ); - expect(errors).toHaveLength(1); - expect(errors[0]).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-external-circular\/ex3\.json$/), - $ref: './root.json#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); + expect(typeof toJSON(actual)).toBe('string'); + expect(toValue(actual)).toEqual(expected); }); }); }); @@ -1938,74 +1893,29 @@ describe('dereference', () => { expect(toValue(actual)).toEqual(expected); }); - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { dereferenceOpts: { errors } }, - }); - - expect(errors).toHaveLength(4); - expect(errors.at(0)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); - expect(errors.at(3)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/components/schemas/Indirection3', - fullPath: ['components', 'schemas', 'Indirection3', '$ref'], - }); - }); - describe('and useCircularStructures=false', () => { test('should dereference', async () => { - const actual = await dereference(rootFilePath, { + const useCircularStructures = false; + const refSet = await resolve(rootFilePath, { parse: { mediaType: mediaTypes.latest('json') }, }); - const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - - expect(typeof toJSON(actual)).toBe('string'); - expect(toValue(actual)).toEqual(expected); - }); - - test('should collect error', async () => { - const errors = []; - - await dereference(rootFilePath, { + refSet.refs[0].uri = '/home/smartbear/root.json'; + const actual = await dereference(refSet.refs[0].uri, { parse: { mediaType: mediaTypes.latest('json') }, dereference: { - dereferenceOpts: { errors }, - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], + circular: useCircularStructures ? 'ignore' : 'replace', + circularReplacer: useCircularStructures + ? referenceOptions.dereference.circularReplacer + : circularReplacer, + refSet, }, }); + const expected = globalThis.loadJsonFile( + path.join(fixturePath, 'dereferenced-no-circular-structures.json') + ); - expect(errors).toHaveLength(4); - expect(errors.at(0)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/components/schemas/User', - fullPath: ['components', 'schemas', 'User', '$ref'], - }); - expect(errors.at(3)).toMatchObject({ - message: expect.stringMatching( - /^Could not resolve reference: Recursive Schema Object reference detected/ - ), - baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), - $ref: '#/components/schemas/Indirection3', - fullPath: ['components', 'schemas', 'Indirection3', '$ref'], - }); + expect(typeof toJSON(actual)).toBe('string'); + expect(toValue(actual)).toEqual(expected); }); }); }); diff --git a/test/resolver/strategies/openapi-3-1-apidom/__snapshots__/index.js.snap b/test/resolver/strategies/openapi-3-1-apidom/__snapshots__/index.js.snap index 5c025385e..cb212036c 100644 --- a/test/resolver/strategies/openapi-3-1-apidom/__snapshots__/index.js.snap +++ b/test/resolver/strategies/openapi-3-1-apidom/__snapshots__/index.js.snap @@ -634,12 +634,7 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via URL a "UserProfile": { "properties": { "user": { - "properties": { - "profile": { - "$ref": "https://example.com/circular-structures.json#/components/schemas/UserProfile", - }, - }, - "type": "object", + "$ref": "https://example.com/circular-structures.json#/components/schemas/User", }, }, "type": "object", @@ -668,12 +663,7 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via URL a "profile": { "properties": { "user": { - "properties": { - "profile": { - "properties": [Circular], - "type": "object", - }, - }, + "properties": [Circular], "type": "object", }, }, @@ -2882,7 +2872,23 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via spec }, "Pets": { "items": { - "$ref": "#/components/schemas/Pet", + "properties": { + "id": { + "format": "int64", + "type": "integer", + }, + "name": { + "type": "string", + }, + "tag": { + "type": "string", + }, + }, + "required": [ + "id", + "name", + ], + "type": "object", }, "maxItems": 100, "type": "array",