From 3a0b0824fac05f99fc72b451f2369bffacf39343 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Fri, 8 Mar 2024 09:40:52 +0100 Subject: [PATCH] fix(reference): base OpenAPI 3.0.x resolver strategy on dereference Refs #3452 --- .../resolve/strategies/openapi-3-0/index.ts | 7 +- .../resolve/strategies/openapi-3-0/visitor.ts | 290 +----------------- .../openapi-3-0/link-object/index.ts | 20 +- .../fixtures/unresolvable-path-item/ex.json | 1 + .../fixtures/unresolvable-path-item/root.json | 2 +- 5 files changed, 19 insertions(+), 301 deletions(-) create mode 100644 packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/ex.json diff --git a/packages/apidom-reference/src/resolve/strategies/openapi-3-0/index.ts b/packages/apidom-reference/src/resolve/strategies/openapi-3-0/index.ts index 940a17e726..38c24cf5e4 100644 --- a/packages/apidom-reference/src/resolve/strategies/openapi-3-0/index.ts +++ b/packages/apidom-reference/src/resolve/strategies/openapi-3-0/index.ts @@ -16,6 +16,7 @@ import { import ReferenceSet from '../../../ReferenceSet'; import Reference from '../../../Reference'; import OpenApi3_0ResolveVisitor from './visitor'; +import { merge as mergeOptions } from '../../../options/util'; // @ts-ignore const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; @@ -23,7 +24,7 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; // eslint-disable-next-line @typescript-eslint/naming-convention const OpenApi3_0ResolveStrategy: stampit.Stamp = stampit(ResolveStrategy, { init() { - this.name = 'asyncapi-2'; + this.name = 'openapi-3-0'; }, methods: { canResolve(file: IFile) { @@ -39,7 +40,8 @@ const OpenApi3_0ResolveStrategy: stampit.Stamp = stampit(Resol async resolve(file: IFile, options: IReferenceOptions) { const namespace = createNamespace(openapi3_0Namespace); const reference = Reference({ uri: file.uri, value: file.parseResult }); - const visitor = OpenApi3_0ResolveVisitor({ reference, namespace, options }); + const mergedOptions = mergeOptions(options, { resolve: { internal: false } }); + const visitor = OpenApi3_0ResolveVisitor({ reference, namespace, options: mergedOptions }); const refSet = ReferenceSet(); refSet.add(reference); @@ -47,7 +49,6 @@ const OpenApi3_0ResolveStrategy: stampit.Stamp = stampit(Resol keyMap, nodeTypeGetter: getNodeType, }); - await visitor.crawl(); return refSet; }, diff --git a/packages/apidom-reference/src/resolve/strategies/openapi-3-0/visitor.ts b/packages/apidom-reference/src/resolve/strategies/openapi-3-0/visitor.ts index 21edbf560f..0b30dd45b6 100644 --- a/packages/apidom-reference/src/resolve/strategies/openapi-3-0/visitor.ts +++ b/packages/apidom-reference/src/resolve/strategies/openapi-3-0/visitor.ts @@ -1,294 +1,8 @@ import stampit from 'stampit'; -import { propEq, values, has, pipe } from 'ramda'; -import { allP } from 'ramda-adjunct'; -import { isPrimitiveElement, isStringElement, visit, toValue } from '@swagger-api/apidom-core'; -import { ApiDOMError } from '@swagger-api/apidom-error'; -import { evaluate, uriToPointer } from '@swagger-api/apidom-json-pointer'; -import { - getNodeType, - isReferenceElement, - isReferenceLikeElement, - isPathItemElement, - keyMap, - ReferenceElement, - PathItemElement, - LinkElement, - ExampleElement, -} from '@swagger-api/apidom-ns-openapi-3-0'; -import { Reference as IReference } from '../../../types'; -import MaximumDereferenceDepthError from '../../../errors/MaximumDereferenceDepthError'; -import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError'; -import * as url from '../../../util/url'; -import parse from '../../../parse'; -import Reference from '../../../Reference'; - -// @ts-ignore -const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; +import OpenApi3_0DereferenceVisitor from '../../../dereference/strategies/openapi-3-0/visitor'; // eslint-disable-next-line @typescript-eslint/naming-convention -const OpenApi3_0ResolveVisitor = stampit({ - props: { - indirections: [], - namespace: null, - reference: null, - crawledElements: null, - crawlingMap: null, - options: null, - }, - init({ reference, namespace, indirections = [], options }) { - this.indirections = indirections; - this.namespace = namespace; - this.reference = reference; - this.crawledElements = []; - this.crawlingMap = {}; - this.options = options; - }, - methods: { - toBaseURI(uri: string): string { - return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri))); - }, - - async toReference(uri: string): Promise { - // detect maximum depth of resolution - if (this.reference.depth >= this.options.resolve.maxDepth) { - throw new MaximumResolveDepthError( - `Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, - ); - } - - const baseURI = this.toBaseURI(uri); - const { refSet } = this.reference; - - // we've already processed this Reference in past - if (refSet.has(baseURI)) { - return refSet.find(propEq(baseURI, 'uri')); - } - - const parseResult = await parse(url.unsanitize(baseURI), { - ...this.options, - parse: { ...this.options.parse, mediaType: 'text/plain' }, - }); - - // register new Reference with ReferenceSet - const reference = Reference({ - uri: baseURI, - value: parseResult, - depth: this.reference.depth + 1, - }); - - refSet.add(reference); - - return reference; - }, - - ReferenceElement(referenceElement: ReferenceElement) { - const uri = toValue(referenceElement.$ref); - const retrievalURI = this.toBaseURI(uri); - - // ignore resolving external Reference Objects - if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) { - return false; - } - - if (!has(retrievalURI, this.crawlingMap)) { - this.crawlingMap[retrievalURI] = this.toReference(uri); - } - this.crawledElements.push(referenceElement); - - return undefined; - }, - - PathItemElement(pathItemElement: PathItemElement) { - // ignore PathItemElement without $ref field - if (!isStringElement(pathItemElement.$ref)) { - return undefined; - } - - const uri = toValue(pathItemElement.$ref); - const retrievalURI = this.toBaseURI(uri); - - // ignore resolving external Path Item Objects - if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) { - return undefined; - } - - if (!has(retrievalURI, this.crawlingMap)) { - this.crawlingMap[retrievalURI] = this.toReference(uri); - } - this.crawledElements.push(pathItemElement); - - return undefined; - }, - - LinkElement(linkElement: LinkElement) { - // ignore LinkElement without operationRef or operationId field - if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) { - return undefined; - } - - // operationRef and operationId are mutually exclusive - if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) { - throw new ApiDOMError('LinkElement operationRef and operationId are mutually exclusive.'); - } - - if (isStringElement(linkElement.operationRef)) { - const uri = toValue(linkElement.operationRef); - const retrievalURI = this.toBaseURI(uri); - - // ignore resolving LinkElement.operationRef - if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) { - return undefined; - } - - if (!has(retrievalURI, this.crawlingMap)) { - this.crawlingMap[retrievalURI] = this.toReference(uri); - } - } - - return undefined; - }, - - ExampleElement(exampleElement: ExampleElement) { - // ignore ExampleElement without externalValue field - if (!isStringElement(exampleElement.externalValue)) { - return undefined; - } - - // value and externalValue fields are mutually exclusive - if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) { - throw new ApiDOMError( - 'ExampleElement value and externalValue fields are mutually exclusive.', - ); - } - - const uri = toValue(exampleElement.externalValue); - const retrievalURI = this.toBaseURI(uri); - - // ignore resolving ExampleElement externalValue - if (!this.options.resolve.external && url.stripHash(this.reference.uri) !== retrievalURI) { - return undefined; - } - - if (!has(retrievalURI, this.crawlingMap)) { - this.crawlingMap[retrievalURI] = this.toReference(uri); - } - - return undefined; - }, - - async crawlReferenceElement(referenceElement: ReferenceElement) { - // @ts-ignore - const reference = await this.toReference(toValue(referenceElement.$ref)); - - this.indirections.push(referenceElement); - - const jsonPointer = uriToPointer(toValue(referenceElement.$ref)); - - // possibly non-semantic fragment - let fragment = evaluate(jsonPointer, reference.value.result); - - // applying semantics to a fragment - if (isPrimitiveElement(fragment)) { - const referencedElementType = toValue(referenceElement.meta.get('referenced-element')); - - if (isReferenceLikeElement(fragment)) { - // handling indirect references - fragment = ReferenceElement.refract(fragment); - fragment.setMetaProperty('referenced-element', referencedElementType); - } else { - // handling direct references - const ElementClass = this.namespace.getElementClass(referencedElementType); - fragment = ElementClass.refract(fragment); - } - } - - // detect direct or circular reference - if (this.indirections.includes(fragment)) { - throw new ApiDOMError('Recursive Reference Object detected'); - } - - // detect maximum depth of dereferencing - if (this.indirections.length > this.options.dereference.maxDepth) { - throw new MaximumDereferenceDepthError( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, - ); - } - - // dive deep into the fragment - const visitor = OpenApi3_0ResolveVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - }); - await visitAsync(fragment, visitor, { keyMap, nodeTypeGetter: getNodeType }); - await visitor.crawl(); - - this.indirections.pop(); - }, - - async crawlPathItemElement(pathItemElement: PathItemElement) { - // @ts-ignore - const reference = await this.toReference(toValue(pathItemElement.$ref)); - - this.indirections.push(pathItemElement); - - const jsonPointer = uriToPointer(toValue(pathItemElement.$ref)); - - // possibly non-semantic fragment - let referencedElement = evaluate(jsonPointer, reference.value.result); - - // applying semantics to a fragment - if (isPrimitiveElement(referencedElement)) { - referencedElement = PathItemElement.refract(referencedElement); - } - - // detect direct or indirect reference - if (this.indirections.includes(referencedElement)) { - throw new ApiDOMError('Recursive Path Item Object reference detected'); - } - - // detect maximum depth of dereferencing - if (this.indirections.length > this.options.dereference.maxDepth) { - throw new MaximumDereferenceDepthError( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`, - ); - } - - // dive deep into the fragment - const visitor: any = OpenApi3_0ResolveVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - }); - await visitAsync(referencedElement, visitor, { keyMap, nodeTypeGetter: getNodeType }); - await visitor.crawl(); - - this.indirections.pop(); - }, - - async crawl() { - /** - * Synchronize all parallel resolutions in this place. - * After synchronization happened we can be sure that refSet - * contains resolved Reference objects. - */ - await pipe(values, allP)(this.crawlingMap); - this.crawlingMap = null; - - /* eslint-disable no-await-in-loop */ - for (const element of this.crawledElements) { - if (isReferenceElement(element)) { - await this.crawlReferenceElement(element); - } else if (isPathItemElement(element)) { - await this.crawlPathItemElement(element); - } - } - /* eslint-enabled */ - }, - }, -}); +const OpenApi3_0ResolveVisitor = stampit(OpenApi3_0DereferenceVisitor); export default OpenApi3_0ResolveVisitor; diff --git a/packages/apidom-reference/test/resolve/strategies/openapi-3-0/link-object/index.ts b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/link-object/index.ts index 54b08319ad..7d676fe9a7 100644 --- a/packages/apidom-reference/test/resolve/strategies/openapi-3-0/link-object/index.ts +++ b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/link-object/index.ts @@ -43,14 +43,16 @@ describe('resolve', function () { context('and with invalid JSON Pointer', function () { const fixturePath = path.join(rootFixturePath, 'operation-ref-invalid-pointer'); - specify('should resolve', async function () { - // external resolution of Link Object is not concerned with validity of JSON Pointer (if defined) - const rootFilePath = path.join(fixturePath, 'root.json'); - const refSet = await resolve(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); - - assert.strictEqual(refSet.size, 2); + specify('should throw error', async function () { + try { + const rootFilePath = path.join(fixturePath, 'root.json'); + await resolve(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + assert.fail('should throw ResolverError'); + } catch (e) { + assert.instanceOf(e, ResolverError); + } }); }); @@ -85,7 +87,7 @@ describe('resolve', function () { } catch (error: any) { assert.strictEqual( error.cause.cause.message, - 'LinkElement operationRef and operationId are mutually exclusive.', + 'LinkElement operationRef and operationId fields are mutually exclusive.', ); assert.instanceOf(error, ResolverError); } diff --git a/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/ex.json b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/ex.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/ex.json @@ -0,0 +1 @@ +{} diff --git a/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/root.json b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/root.json index 11763d29ef..24b564d2bb 100644 --- a/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/root.json +++ b/packages/apidom-reference/test/resolve/strategies/openapi-3-0/path-item-object/fixtures/unresolvable-path-item/root.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "paths": { "/path1": { - "$ref": "#/paths/invalid-pointer" + "$ref": "./ex.json#/paths/invalid-pointer" }, "/path2": { "summary": "path item summary",