From 41e6bf1b0d02e039d201bc95d915abecc7d59565 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Thu, 29 Dec 2022 16:35:04 +0100 Subject: [PATCH] feat: add initial OpenAPI 3.1.0 dereference strategy Refs #2717 --- .eslintrc | 6 +- config/jest/jest.unit.config.js | 2 + .../openapi-3-1-swagger-client/index.js | 64 +++++++++++++++++++ .../openapi-3-1-swagger-client/visitor.js | 7 ++ .../__utils__/jest.local.setup.js | 33 ++++++++++ .../components-callbacks/dereferenced.json | 19 ++++++ .../components-callbacks/root.yaml | 9 +++ .../operation-object/dereferenced.json | 27 ++++++++ .../__fixtures__/operation-object/root.yaml | 13 ++++ .../callback-object/index.js | 52 +++++++++++++++ .../sample-api.json | 0 .../parse/parsers/openapi-json-3-1/index.js | 10 +-- .../sample-api.yaml | 0 .../parse/parsers/openapi-yaml-3-1/index.js | 10 +-- .../empty-openapi-3-1-api.json | 0 .../sample-openapi-3-1-api.json | 0 .../unknown-extension.ext | 0 .../resolvers/http-swagger-client/index.js | 2 +- test/jest.setup.js | 4 ++ 19 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js create mode 100644 src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/__utils__/jest.local.setup.js create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/dereferenced.json create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/root.yaml create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/dereferenced.json create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/root.yaml create mode 100644 test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/index.js rename test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/{fixtures => __fixtures__}/sample-api.json (100%) rename test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/{fixtures => __fixtures__}/sample-api.yaml (100%) rename test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/{fixtures => __fixtures__}/empty-openapi-3-1-api.json (100%) rename test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/{fixtures => __fixtures__}/sample-openapi-3-1-api.json (100%) rename test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/{fixtures => __fixtures__}/unknown-extension.ext (100%) diff --git a/.eslintrc b/.eslintrc index 5b558deba..d6eee28a1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,7 +37,11 @@ "import/no-unresolved": [ 2, { - "ignore": ["^@swagger-api/apidom-reference/configuration/empty$"] + "ignore": [ + "^@swagger-api/apidom-reference/configuration/empty$", + "^@swagger-api/apidom-reference/dereference/strategies/openapi-3-1$", + "^@swagger-api/apidom-reference/resolve/resolvers/file$" + ] } ], "prettier/prettier": "error", diff --git a/config/jest/jest.unit.config.js b/config/jest/jest.unit.config.js index 540015b1d..1ddaf7e16 100644 --- a/config/jest/jest.unit.config.js +++ b/config/jest/jest.unit.config.js @@ -15,5 +15,7 @@ module.exports = { '/test/jest.setup.js', '/test/specmap/data/', '/test/build-artifacts/', + '/__fixtures__/', + '/__utils__/', ], }; diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js new file mode 100644 index 000000000..ba2816801 --- /dev/null +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js @@ -0,0 +1,64 @@ +/* eslint-disable camelcase */ +import { createNamespace, visit } 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'; + +import OpenApi3_1SwaggerClientDereferenceVisitor from './visitor.js'; + +const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; + +const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy.compose({ + props: { + useCircularStructures: true, + allowMetaPatches: false, + }, + init({ + useCircularStructures = this.useCircularStructures, + allowMetaPatches = this.allowMetaPatches, + } = {}) { + this.name = 'openapi-3-1-swagger-client'; + this.useCircularStructures = useCircularStructures; + this.allowMetaPatches = allowMetaPatches; + }, + methods: { + async dereference(file, options) { + const namespace = createNamespace(openApi3_1Namespace); + const refSet = options.dereference.refSet ?? ReferenceSet(); + let reference; + + if (!refSet.has(file.uri)) { + reference = Reference({ uri: file.uri, value: file.parseResult }); + refSet.add(reference); + } else { + // pre-computed refSet was provided as configuration option + reference = refSet.find((ref) => ref.uri === file.uri); + } + + const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + reference, + namespace, + options, + useCircularStructures: this.useCircularStructures, + allowMetaPatches: this.allowMetaPatches, + }); + const dereferencedElement = await visitAsync(refSet.rootRef.value, visitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); + + /** + * Release all memory if this refSet was not provided as a configuration option. + * If provided as configuration option, then provider is responsible for cleanup. + */ + if (options.dereference.refSet === null) { + refSet.clean(); + } + + return dereferencedElement; + }, + }, +}); + +export default OpenApi3_1SwaggerClientDereferenceStrategy; +/* eslint-enable camelcase */ diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js new file mode 100644 index 000000000..b5f1dae42 --- /dev/null +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js @@ -0,0 +1,7 @@ +/* eslint-disable camelcase */ +import { OpenApi3_1DereferenceVisitor } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'; + +const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.compose({}); + +export default OpenApi3_1SwaggerClientDereferenceVisitor; +/* eslint-enable camelcase */ diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/__utils__/jest.local.setup.js b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/__utils__/jest.local.setup.js new file mode 100644 index 000000000..ae36f4ef6 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/__utils__/jest.local.setup.js @@ -0,0 +1,33 @@ +/* eslint-disable camelcase */ +import { options } from '@swagger-api/apidom-reference/configuration/empty'; +import FileResolver from '@swagger-api/apidom-reference/resolve/resolvers/file'; + +import JsonParser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/json/index.js'; +import YamlParser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/yaml-1-2/index.js'; +import OpenApiJson3_1Parser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js'; +import OpenApiYaml3_1Parser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js'; +import HttpResolverSwaggerClient from '../../../../../../../../src/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js'; +import OpenApi3_1SwaggerClientDereferenceStrategy from '../../../../../../../../src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js'; + +export const beforeAll = () => { + // configure custom parser plugins globally + options.parse.parsers = [ + OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }), + OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }), + JsonParser({ allowEmpty: false, sourceMap: false }), + YamlParser({ allowEmpty: false, sourceMap: false }), + ]; + + // configure custom resolver plugins globally + options.resolve.resolvers = [FileResolver({ fileAllowList: ['*'] }), HttpResolverSwaggerClient()]; + + // configure custom dereference strategy globally + options.dereference.strategies = [OpenApi3_1SwaggerClientDereferenceStrategy()]; +}; + +export const afterAll = () => { + options.parse.parsers = []; + options.resolve.resolvers = []; + options.dereference.strategies = []; +}; +/* eslint-enable camelcase */ diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/dereferenced.json new file mode 100644 index 000000000..4c753eb15 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/dereferenced.json @@ -0,0 +1,19 @@ +[ + { + "openapi": "3.1.0", + "components": { + "callbacks": { + "callback1": { + "{$method}": { + "description": "description of callback2" + } + }, + "callback2": { + "{$method}": { + "description": "description of callback2" + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/root.yaml b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/root.yaml new file mode 100644 index 000000000..93fff754f --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/components-callbacks/root.yaml @@ -0,0 +1,9 @@ +--- +openapi: 3.1.0 +components: + callbacks: + callback1: + "$ref": "#/components/callbacks/callback2" + callback2: + "{$method}": + description: description of callback2 diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/dereferenced.json new file mode 100644 index 000000000..9bdc71aca --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/dereferenced.json @@ -0,0 +1,27 @@ +[ + { + "openapi": "3.1.0", + "paths": { + "/path": { + "get": { + "callbacks": { + "callback": { + "{$method}": { + "description": "description of callback2" + } + } + } + } + } + }, + "components": { + "callbacks": { + "callback": { + "{$method}": { + "description": "description of callback2" + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/root.yaml b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/root.yaml new file mode 100644 index 000000000..0c9162d0f --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/__fixtures__/operation-object/root.yaml @@ -0,0 +1,13 @@ +--- +openapi: 3.1.0 +paths: + "/path": + get: + callbacks: + callback: + "$ref": "#/components/callbacks/callback" +components: + callbacks: + callback: + "{$method}": + description: description of callback2 diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/index.js b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/index.js new file mode 100644 index 000000000..4153655a2 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/callback-object/index.js @@ -0,0 +1,52 @@ +import path from 'node:path'; +import { toValue } from '@swagger-api/apidom-core'; +import { mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; +import { dereference } from '@swagger-api/apidom-reference/configuration/empty'; + +import * as jestSetup from '../__utils__/jest.local.setup.js'; + +const rootFixturePath = path.join(__dirname, '__fixtures__'); + +describe('dereference', () => { + beforeAll(() => { + jestSetup.beforeAll(); + }); + + afterAll(() => { + jestSetup.afterAll(); + }); + + describe('strategies', () => { + describe('openapi-3-1swagger-client', () => { + describe('Callback Object', () => { + describe('given in components/callbacks field', () => { + const fixturePath = path.join(rootFixturePath, 'components-callbacks'); + + test('should dereference', async () => { + const rootFilePath = path.join(fixturePath, 'root.yaml'); + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('yaml') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + }); + + describe('given in Operation Object', () => { + const fixturePath = path.join(rootFixturePath, 'operation-object'); + + test('should dereference', async () => { + const rootFilePath = path.join(fixturePath, 'root.yaml'); + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('yaml') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + }); + }); + }); + }); +}); diff --git a/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/fixtures/sample-api.json b/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/__fixtures__/sample-api.json similarity index 100% rename from test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/fixtures/sample-api.json rename to test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/__fixtures__/sample-api.json diff --git a/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js b/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js index e8abdf190..155377f4b 100644 --- a/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js +++ b/test/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js @@ -67,7 +67,7 @@ describe('OpenApiJson3_1Parser', () => { describe('given file with supported extension', () => { describe('and file data is buffer and can be detected as OpenAPI 3.1.0', () => { test('should return true', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.json'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.json'); const file = File({ uri: '/path/to/open-api.json', data: fs.readFileSync(url), @@ -80,7 +80,7 @@ describe('OpenApiJson3_1Parser', () => { describe('and file data is string and can be detected as OpenAPI 3.1.0', () => { test('should return true', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.json'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.json'); const file = File({ uri: '/path/to/open-api.json', data: fs.readFileSync(url).toString(), @@ -96,7 +96,7 @@ describe('OpenApiJson3_1Parser', () => { describe('parse', () => { describe('given OpenApi 3.1.x JSON data', () => { test('should return parse result', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.json'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.json'); const data = fs.readFileSync(url).toString(); const file = File({ url, @@ -112,7 +112,7 @@ describe('OpenApiJson3_1Parser', () => { describe('given OpenApi 3.1.x JSON data as buffer', () => { test('should return parse result', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.json'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.json'); const data = fs.readFileSync(url); const file = File({ url, @@ -174,7 +174,7 @@ describe('OpenApiJson3_1Parser', () => { describe('given sourceMap disabled', () => { test('should not decorate ApiDOM with source maps', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.json'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.json'); const data = fs.readFileSync(url).toString(); const file = File({ url, diff --git a/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/fixtures/sample-api.yaml b/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/__fixtures__/sample-api.yaml similarity index 100% rename from test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/fixtures/sample-api.yaml rename to test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/__fixtures__/sample-api.yaml diff --git a/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js b/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js index 98b9a2b64..0ac4638c7 100644 --- a/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js +++ b/test/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js @@ -98,7 +98,7 @@ describe('OpenApiYaml3_1Parser', () => { describe('given file with supported extension', () => { describe('and file data is buffer and can be detected as OpenAPI 3.1.0', () => { test('should return true', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.yaml'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml'); const file = File({ uri: '/path/to/open-api.yaml', data: fs.readFileSync(url), @@ -111,7 +111,7 @@ describe('OpenApiYaml3_1Parser', () => { describe('and file data is string and can be detected as OpenAPI 3.1.0', () => { test('should return true', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.yaml'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml'); const file = File({ uri: '/path/to/open-api.yaml', data: fs.readFileSync(url).toString(), @@ -127,7 +127,7 @@ describe('OpenApiYaml3_1Parser', () => { describe('parse', () => { describe('given OpenApi 3.1.x YAML data', () => { test('should return parse result', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.yaml'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml'); const data = fs.readFileSync(url).toString(); const file = File({ url, @@ -143,7 +143,7 @@ describe('OpenApiYaml3_1Parser', () => { describe('given OpenApi 3.1.x YAML data as buffer', () => { test('should return parse result', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.yaml'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml'); const data = fs.readFileSync(url); const file = File({ url, @@ -205,7 +205,7 @@ describe('OpenApiYaml3_1Parser', () => { describe('given sourceMap disabled', () => { test('should not decorate ApiDOM with source maps', async () => { - const url = path.join(__dirname, 'fixtures', 'sample-api.yaml'); + const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml'); const data = fs.readFileSync(url).toString(); const file = File({ url, diff --git a/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/empty-openapi-3-1-api.json b/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/empty-openapi-3-1-api.json similarity index 100% rename from test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/empty-openapi-3-1-api.json rename to test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/empty-openapi-3-1-api.json diff --git a/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/sample-openapi-3-1-api.json b/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/sample-openapi-3-1-api.json similarity index 100% rename from test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/sample-openapi-3-1-api.json rename to test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/sample-openapi-3-1-api.json diff --git a/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/unknown-extension.ext b/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/unknown-extension.ext similarity index 100% rename from test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/fixtures/unknown-extension.ext rename to test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/__fixtures__/unknown-extension.ext diff --git a/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js b/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js index f3aca7d54..96537553b 100644 --- a/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js +++ b/test/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js @@ -79,7 +79,7 @@ describe('HttpResolverSwaggerClient', () => { test('should throw on timeout', async () => { resolver = HttpResolverSwaggerClient({ timeout: 1 }); const url = 'http://localhost:8123/local-file.txt'; - const cwd = path.join(__dirname, 'fixtures'); + const cwd = path.join(__dirname, '__fixtures__'); const server = globalThis.createHTTPServer({ port: 8123, cwd }); expect.assertions(3); diff --git a/test/jest.setup.js b/test/jest.setup.js index 82fb59825..30ded100e 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -15,6 +15,10 @@ fetchMock.config.Headers = Headers; // provide AbortController for older Node.js versions globalThis.AbortController = globalThis.AbortController ?? AbortController; +// helpers for reading local files +globalThis.loadFile = (uri) => fs.readFileSync(uri).toString(); +globalThis.loadJsonFile = (uri) => JSON.parse(globalThis.loadFile(uri)); + // helper for providing HTTP server instance for testing globalThis.createHTTPServer = ({ port = 8123, cwd = process.cwd() } = {}) => { const server = http.createServer((req, res) => {