From e04ead9f2bd09313a21ea806065a9138db971638 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Wed, 4 Jan 2023 13:59:36 +0100 Subject: [PATCH] feat(resolver): add support for OpenAPI 3.1.0 resolution Refs #2744 --- config/webpack/browser.config.babel.js | 2 +- package.json | 8 +- src/resolver/index.js | 7 +- src/resolver/strategies/openapi-3-1.js | 67 +++++ .../openapi-2--3-0.js} | 4 +- .../openapi-3-1/__fixtures__/petstore.json | 179 ++++++++++++ .../openapi-3-1/__snapshots__/index.js.snap | 261 ++++++++++++++++++ test/resolver/strategies/openapi-3-1/index.js | 29 ++ 8 files changed, 549 insertions(+), 8 deletions(-) create mode 100644 src/resolver/strategies/openapi-3-1.js rename test/resolver/{resolver.js => strategies/openapi-2--3-0.js} (99%) create mode 100644 test/resolver/strategies/openapi-3-1/__fixtures__/petstore.json create mode 100644 test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap create mode 100644 test/resolver/strategies/openapi-3-1/index.js diff --git a/config/webpack/browser.config.babel.js b/config/webpack/browser.config.babel.js index aa3cddda9..9e10c78df 100644 --- a/config/webpack/browser.config.babel.js +++ b/config/webpack/browser.config.babel.js @@ -74,7 +74,7 @@ const browserMin = { devtool: 'source-map', performance: { hints: 'error', - maxEntrypointSize: 270000, + maxEntrypointSize: 500000, maxAssetSize: 1300000, }, output: { diff --git a/package.json b/package.json index 4605df2a3..11f550360 100644 --- a/package.json +++ b/package.json @@ -111,10 +111,10 @@ }, "dependencies": { "@babel/runtime-corejs3": "^7.11.2", - "@swagger-api/apidom-core": "^0.62.0", - "@swagger-api/apidom-reference": "^0.62.0", - "@swagger-api/apidom-ns-openapi-3-1": "^0.62.0", - "@swagger-api/apidom-json-pointer": "^0.62.0", + "@swagger-api/apidom-core": "^0.62.1", + "@swagger-api/apidom-reference": "^0.62.1", + "@swagger-api/apidom-ns-openapi-3-1": "^0.62.1", + "@swagger-api/apidom-json-pointer": "^0.62.1", "cookie": "~0.5.0", "cross-fetch": "^3.1.5", "deepmerge": "~4.2.2", diff --git a/src/resolver/index.js b/src/resolver/index.js index 1afec9dd9..7e6399172 100644 --- a/src/resolver/index.js +++ b/src/resolver/index.js @@ -1,7 +1,9 @@ // eslint-disable-next-line camelcase import resolveOpenAPI2_30Strategy from './strategies/openapi-2--3-0.js'; +import resolveOpenAPI31Strategy from './strategies/openapi-3-1.js'; import { makeFetchJSON } from './utils/index.js'; import * as optionsUtil from './utils/options.js'; +import { isOpenAPI31 } from '../helpers/openapi-predicates.js'; const resolve = async (options) => { const { spec, requestInterceptor, responseInterceptor } = options; @@ -11,8 +13,11 @@ const resolve = async (options) => { const retrievedSpec = spec || (await makeFetchJSON(httpClient, { requestInterceptor, responseInterceptor })(retrievalURI)); + const strategyOptions = { ...options, spec: retrievedSpec }; - return resolveOpenAPI2_30Strategy({ ...options, spec: retrievedSpec }); + return isOpenAPI31(retrievedSpec) + ? resolveOpenAPI31Strategy(strategyOptions) + : resolveOpenAPI2_30Strategy(strategyOptions); }; export default resolve; diff --git a/src/resolver/strategies/openapi-3-1.js b/src/resolver/strategies/openapi-3-1.js new file mode 100644 index 000000000..723b298dc --- /dev/null +++ b/src/resolver/strategies/openapi-3-1.js @@ -0,0 +1,67 @@ +/* eslint-disable camelcase */ +import { toValue } from '@swagger-api/apidom-core'; +import { OpenApi3_1Element } from '@swagger-api/apidom-ns-openapi-3-1'; +import { dereferenceApiDOM } 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'; + +import * as optionsUtil from '../utils/options.js'; +import normalizeOpenAPI31 from '../../helpers/normalize/openapi-3-1.js'; +import HttpResolverSwaggerClient from '../../helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js'; +import JsonParser from '../../helpers/apidom/reference/parse/parsers/json/index.js'; +import YamlParser from '../../helpers/apidom/reference/parse/parsers/yaml-1-2/index.js'; +import OpenApiJson3_1Parser from '../../helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js'; +import OpenApiYaml3_1Parser from '../../helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js'; +import OpenApi3_1SwaggerClientDereferenceStrategy from '../../helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js'; + +const resolveOpenAPI31Strategy = async (options) => { + const { + spec, + timeout, + redirects, + requestInterceptor, + responseInterceptor, + allowMetaPatches = false, + useCircularStructures = false, + skipNormalization = false, + } = options; + const openApiElement = OpenApi3_1Element.refract(spec); + const dereferenced = await dereferenceApiDOM(openApiElement, { + resolve: { + baseURI: optionsUtil.retrievalURI(options), + resolvers: [ + HttpResolverSwaggerClient({ + timeout: timeout || 10000, + redirects: redirects || 10, + }), + ], + resolverOpts: { + swaggerHTTPClientConfig: { + requestInterceptor, + responseInterceptor, + }, + }, + strategies: [OpenApi3_1ResolveStrategy()], + }, + parse: { + parsers: [ + OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }), + OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }), + JsonParser({ allowEmpty: false, sourceMap: false }), + YamlParser({ allowEmpty: false, sourceMap: false }), + BinaryParser({ allowEmpty: false, sourceMap: false }), + ], + }, + dereference: { + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ allowMetaPatches, useCircularStructures }), + ], + }, + }); + const normalized = skipNormalization ? dereferenced : normalizeOpenAPI31(dereferenced); + + return { spec: toValue(normalized), errors: [] }; +}; + +export default resolveOpenAPI31Strategy; +/* eslint-enable camelcase */ diff --git a/test/resolver/resolver.js b/test/resolver/strategies/openapi-2--3-0.js similarity index 99% rename from test/resolver/resolver.js rename to test/resolver/strategies/openapi-2--3-0.js index 947b4afce..f42a4df50 100644 --- a/test/resolver/resolver.js +++ b/test/resolver/strategies/openapi-2--3-0.js @@ -3,7 +3,7 @@ import path from 'path'; import fs from 'fs'; import jsYaml from 'js-yaml'; -import Swagger from '../../src/index.js'; +import Swagger from '../../../src/index.js'; describe('resolver', () => { afterEach(() => { @@ -721,7 +721,7 @@ describe('resolver', () => { test('should not throw errors on resvered-keywords in freely-named-fields', () => { // Given const ReservedKeywordSpec = jsYaml.load( - fs.readFileSync(path.resolve(__dirname, '../data/reserved-keywords.yaml'), 'utf8') + fs.readFileSync(path.resolve(__dirname, '../../data/reserved-keywords.yaml'), 'utf8') ); // When diff --git a/test/resolver/strategies/openapi-3-1/__fixtures__/petstore.json b/test/resolver/strategies/openapi-3-1/__fixtures__/petstore.json new file mode 100644 index 000000000..5ce290715 --- /dev/null +++ b/test/resolver/strategies/openapi-3-1/__fixtures__/petstore.json @@ -0,0 +1,179 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "maxItems": 100, + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap b/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap new file mode 100644 index 000000000..adac708da --- /dev/null +++ b/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition should resolve 1`] = ` +{ + "errors": [], + "spec": { + "$$normalized": true, + "components": { + "schemas": { + "Error": { + "properties": { + "code": { + "format": "int32", + "type": "integer", + }, + "message": { + "type": "string", + }, + }, + "required": [ + "code", + "message", + ], + "type": "object", + }, + "Pet": { + "properties": { + "id": { + "format": "int64", + "type": "integer", + }, + "name": { + "type": "string", + }, + "tag": { + "type": "string", + }, + }, + "required": [ + "id", + "name", + ], + "type": "object", + }, + "Pets": { + "items": { + "properties": { + "id": { + "format": "int64", + "type": "integer", + }, + "name": { + "type": "string", + }, + "tag": { + "type": "string", + }, + }, + "required": [ + "id", + "name", + ], + "type": "object", + }, + "maxItems": 100, + "type": "array", + }, + }, + }, + "info": { + "license": { + "name": "MIT", + }, + "title": "Swagger Petstore", + "version": "1.0.0", + }, + "openapi": "3.1.0", + "paths": { + "/pets": { + "get": { + "operationId": "listPets", + "parameters": [ + { + "description": "How many items to return at one time (max 100)", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "properties": { + "id": { + "format": "int64", + "type": "integer", + }, + "name": { + "type": "string", + }, + "tag": { + "type": "string", + }, + }, + "required": [ + "id", + "name", + ], + "type": "object", + }, + "maxItems": 100, + "type": "array", + }, + }, + }, + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string", + }, + }, + }, + }, + "default": { + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "format": "int32", + "type": "integer", + }, + "message": { + "type": "string", + }, + }, + "required": [ + "code", + "message", + ], + "type": "object", + }, + }, + }, + "description": "unexpected error", + }, + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + "summary": "List all pets", + "tags": [ + "pets", + ], + }, + "post": { + "operationId": "createPets", + "responses": { + "201": { + "description": "Null response", + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "https://example.com/petstore.json#/components/schemas/Error", + }, + }, + }, + "description": "unexpected error", + }, + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + "summary": "Create a pet", + "tags": [ + "pets", + ], + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + }, + "/pets/{petId}": { + "get": { + "operationId": "showPetById", + "parameters": [ + { + "description": "The id of the pet to retrieve", + "in": "path", + "name": "petId", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "https://example.com/petstore.json#/components/schemas/Pet", + }, + }, + }, + "description": "Expected response to a valid request", + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "https://example.com/petstore.json#/components/schemas/Error", + }, + }, + }, + "description": "unexpected error", + }, + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + "summary": "Info for a specific pet", + "tags": [ + "pets", + ], + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + }, + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + }, +} +`; diff --git a/test/resolver/strategies/openapi-3-1/index.js b/test/resolver/strategies/openapi-3-1/index.js new file mode 100644 index 000000000..e007e8f47 --- /dev/null +++ b/test/resolver/strategies/openapi-3-1/index.js @@ -0,0 +1,29 @@ +import path from 'node:path'; +import fetchMock from 'fetch-mock'; + +import Swagger from '../../../../src/index.js'; + +const fixturePath = path.join(__dirname, '__fixtures__'); + +describe('resolve', () => { + describe('OpenAPI 3.1.0 strategy', () => { + test('should expose a resolver function', () => { + expect(Swagger.resolve).toBeInstanceOf(Function); + }); + + describe('given OpenAPI 3.1.0 definition', () => { + test('should resolve', async () => { + const url = 'https://example.com/petstore.json'; + const response = new Response(globalThis.loadFile(path.join(fixturePath, 'petstore.json'))); + fetchMock.get(url, response, { repeat: 1 }); + const resolvedSpec = await Swagger.resolve({ + url: 'https://example.com/petstore.json', + }); + + expect(resolvedSpec).toMatchSnapshot(); + + fetchMock.restore(); + }); + }); + }); +});