diff --git a/apidom/packages/apidom-reference/src/resolve/strategies/openapi-3-1/visitor.ts b/apidom/packages/apidom-reference/src/resolve/strategies/openapi-3-1/visitor.ts index f8db7d4b73..925e8118a1 100644 --- a/apidom/packages/apidom-reference/src/resolve/strategies/openapi-3-1/visitor.ts +++ b/apidom/packages/apidom-reference/src/resolve/strategies/openapi-3-1/visitor.ts @@ -9,12 +9,14 @@ import { keyMap, ReferenceElement, PathItemElement, + LinkElement, SchemaElement, isReferenceElementExternal, isSchemaElementExternal, isSchemaElement, isPathItemElement, isPathItemElementExternal, + isLinkElementExternal, } from 'apidom-ns-openapi-3-1'; import { Reference as IReference } from '../../../types'; @@ -134,6 +136,34 @@ const OpenApi3_1ResolveVisitor = stampit({ return undefined; }, + LinkElement(linkElement: LinkElement) { + // ignore LinkElement without operationRef or operationId field + if (!isStringElement(linkElement.operationRef) && !isStringElement(linkElement.operationId)) { + return undefined; + } + + // ignore resolving external Path Item Elements + if (!this.options.resolve.external && isLinkElementExternal(linkElement)) { + return undefined; + } + + // operationRef and operationId are mutually exclusive + if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) { + throw new Error('LinkElement operationRef and operationId are mutually exclusive.'); + } + + if (isLinkElementExternal(linkElement)) { + const uri = linkElement.operationRef.toValue(); + const baseURI = this.toBaseURI(uri); + + if (!has(baseURI, this.crawlingMap)) { + this.crawlingMap[baseURI] = this.toReference(uri); + } + } + + return undefined; + }, + SchemaElement(schemaElement: SchemaElement) { /** * Skip traversal for already visited schemas and all their child schemas. diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/ex.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/ex.json new file mode 100644 index 0000000000..3d6eb5ad85 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/ex.json @@ -0,0 +1,5 @@ +{ + "operation": { + "description": "description of operation" + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/root.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/root.json new file mode 100644 index 0000000000..76e6167168 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-external/root.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.1.0", + "components": { + "links": { + "link1": { + "operationRef": "./ex.json#/operation" + } + } + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-id-both-defined/root.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-id-both-defined/root.json new file mode 100644 index 0000000000..c490b812bc --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-id-both-defined/root.json @@ -0,0 +1,11 @@ +{ + "openapi": "3.1.0", + "components": { + "links": { + "link1": { + "operationRef": "./ex.json", + "operationId": "op1" + } + } + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/ex.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/ex.json new file mode 100644 index 0000000000..3d6eb5ad85 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/ex.json @@ -0,0 +1,5 @@ +{ + "operation": { + "description": "description of operation" + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/root.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/root.json new file mode 100644 index 0000000000..76e6167168 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-ignore-external/root.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.1.0", + "components": { + "links": { + "link1": { + "operationRef": "./ex.json#/operation" + } + } + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/ex.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/ex.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/ex.json @@ -0,0 +1 @@ +{} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/root.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/root.json new file mode 100644 index 0000000000..e64e97c8cb --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-invalid-pointer/root.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.1.0", + "components": { + "links": { + "link1": { + "operationRef": "./ex.json#invalid-pointer" + } + } + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-unresolvable/root.json b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-unresolvable/root.json new file mode 100644 index 0000000000..e76d0955e4 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/fixtures/operation-ref-unresolvable/root.json @@ -0,0 +1,10 @@ +{ + "openapi": "3.1.0", + "components": { + "links": { + "link1": { + "operationRef": "./ex.json" + } + } + } +} diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/index.ts b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/index.ts new file mode 100644 index 0000000000..8f869d96f5 --- /dev/null +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/link-object/index.ts @@ -0,0 +1,96 @@ +import path from 'path'; +import { assert } from 'chai'; + +import { resolve } from '../../../../../src'; +import { ResolverError } from '../../../../../src/util/errors'; + +const rootFixturePath = path.join(__dirname, 'fixtures'); + +describe('resolve', function () { + context('strategies', function () { + context('openapi-3-1', function () { + context('Link Object', function () { + context('given operationRef field', function () { + context('and with external JSON Pointer', function () { + const fixturePath = path.join(rootFixturePath, 'operation-ref-external'); + const rootFilePath = path.join(fixturePath, 'root.json'); + + specify('should resolve', async function () { + const refSet = await resolve(rootFilePath, { + parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, + }); + + assert.strictEqual(refSet.size, 2); + }); + }); + }); + + context('with external resolution disabled', function () { + const fixturePath = path.join(rootFixturePath, 'operation-ref-ignore-external'); + + specify('should not resolve', async function () { + const rootFilePath = path.join(fixturePath, 'root.json'); + const refSet = await resolve(rootFilePath, { + parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, + resolve: { external: false }, + }); + + assert.strictEqual(refSet.size, 1); + }); + }); + + 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: 'application/vnd.oai.openapi+json;version=3.1.0' }, + }); + + assert.strictEqual(refSet.size, 2); + }); + }); + + context('and with unresolvable URI', function () { + const fixturePath = path.join(rootFixturePath, 'operation-ref-unresolvable'); + + specify('should throw error', async function () { + const rootFilePath = path.join(fixturePath, 'root.json'); + + try { + await resolve(rootFilePath, { + parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, + }); + assert.fail('should throw ResolverError'); + } catch (e) { + assert.instanceOf(e, ResolverError); + } + }); + }); + + context('given both operationRef and operationId fields are defined', function () { + const fixturePath = path.join(rootFixturePath, 'operation-ref-id-both-defined'); + + specify('should throw error', async function () { + const rootFilePath = path.join(fixturePath, 'root.json'); + + try { + await resolve(rootFilePath, { + parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, + }); + assert.fail('should throw ResolverError'); + } catch (e) { + assert.strictEqual( + e.cause.cause.message, + 'LinkElement operationRef and operationId are mutually exclusive.', + ); + assert.instanceOf(e, ResolverError); + } + }); + }); + }); + }); + }); +}); diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/path-item-object/index.ts b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/path-item-object/index.ts index e1144ae75c..efad40b5bc 100644 --- a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/path-item-object/index.ts +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/path-item-object/index.ts @@ -27,7 +27,7 @@ describe('resolve', function () { context('given $ref field pointing internally and externally', function () { const fixturePath = path.join(rootFixturePath, 'internal-external'); - specify('should dereference', async function () { + specify('should resolve', async function () { const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, @@ -40,7 +40,7 @@ describe('resolve', function () { context('given external resolution disabled', function () { const fixturePath = path.join(rootFixturePath, 'ignore-external'); - specify('should dereference', async function () { + specify('should resolve', async function () { const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, @@ -54,7 +54,7 @@ describe('resolve', function () { context('given $ref field pointing to external indirections', function () { const fixturePath = path.join(rootFixturePath, 'external-indirections'); - specify('should dereference', async function () { + specify('should resolve', async function () { const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' }, diff --git a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/schema-object/index.ts b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/schema-object/index.ts index 578c580205..1db8852330 100644 --- a/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/schema-object/index.ts +++ b/apidom/packages/apidom-reference/test/resolve/strategies/openapi-3-1/schema-object/index.ts @@ -270,7 +270,7 @@ describe('resolve', function () { function () { const fixturePath = path.join(rootFixturePath, '$anchor-external'); - specify('should dereference', async function () { + specify('should resolve', async function () { const rootFilePath = path.join(fixturePath, 'root.json'); const refSet = await resolve(rootFilePath, { parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },