diff --git a/src/resolver/strategies/openapi-3-1.js b/src/resolver/strategies/openapi-3-1.js index 4a4e3aab3d..8f8fbdda9c 100644 --- a/src/resolver/strategies/openapi-3-1.js +++ b/src/resolver/strategies/openapi-3-1.js @@ -1,7 +1,16 @@ /* eslint-disable camelcase */ -import { toValue } from '@swagger-api/apidom-core'; -import { OpenApi3_1Element } from '@swagger-api/apidom-ns-openapi-3-1'; -import { dereferenceApiDOM, url } from '@swagger-api/apidom-reference/configuration/empty'; +import { toValue, transclude, ParseResultElement } from '@swagger-api/apidom-core'; +import { + compile as jsonPointerCompile, + evaluate as jsonPointerEvaluate, +} from '@swagger-api/apidom-json-pointer'; +import { OpenApi3_1Element, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; +import { + dereferenceApiDOM, + url, + ReferenceSet, + Reference, +} 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'; @@ -21,13 +30,32 @@ const resolveOpenAPI31Strategy = async (options) => { redirects, requestInterceptor, responseInterceptor, + pathDiscriminator = [], allowMetaPatches = false, useCircularStructures = false, skipNormalization = false, } = options; - const baseURI = optionsUtil.retrievalURI(options) ?? url.cwd(); + // determining BaseURI + const defaultBaseURI = 'https://smartbear.com/'; + const retrievalURI = optionsUtil.retrievalURI(options) ?? url.cwd(); + const baseURI = url.isHttpUrl(retrievalURI) ? retrievalURI : defaultBaseURI; + + // prepare spec for dereferencing const openApiElement = OpenApi3_1Element.refract(spec); - const dereferenced = await dereferenceApiDOM(openApiElement, { + openApiElement.classes.push('result'); + const openApiParseResultElement = new ParseResultElement([openApiElement]); + + // prepare fragment for dereferencing + const jsonPointer = jsonPointerCompile(pathDiscriminator); + const jsonPointerURI = jsonPointer === '' ? '' : `#${jsonPointer}`; + const fragmentElement = jsonPointerEvaluate(jsonPointer, openApiElement); + + // prepare reference set for dereferencing + const openApiElementReference = Reference({ uri: baseURI, value: openApiParseResultElement }); + const refSet = ReferenceSet({ refs: [openApiElementReference] }); + if (jsonPointer !== '') refSet.rootRef = null; // reset root reference as we want fragment to become the root reference + + const dereferenced = await dereferenceApiDOM(fragmentElement, { resolve: { /** * swagger-client only supports resolving HTTP(S) URLs or spec objects. @@ -35,7 +63,7 @@ const resolveOpenAPI31Strategy = async (options) => { * and baseURI was not provided as part of resolver options, * then below baseURI check will make sure that constant HTTPS URL is used as baseURI. */ - baseURI: url.isHttpUrl(baseURI) ? baseURI : 'https://smartbear.com/', + baseURI: `${baseURI}${jsonPointerURI}`, resolvers: [ HttpResolverSwaggerClient({ timeout: timeout || 10000, @@ -51,6 +79,7 @@ const resolveOpenAPI31Strategy = async (options) => { strategies: [OpenApi3_1ResolveStrategy()], }, parse: { + mediaType: mediaTypes.latest(), parsers: [ OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }), OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }), @@ -63,9 +92,12 @@ const resolveOpenAPI31Strategy = async (options) => { strategies: [ OpenApi3_1SwaggerClientDereferenceStrategy({ allowMetaPatches, useCircularStructures }), ], + refSet, }, }); - const normalized = skipNormalization ? dereferenced : normalizeOpenAPI31(dereferenced); + + const transcluded = transclude(fragmentElement, dereferenced, openApiElement); + const normalized = skipNormalization ? transcluded : normalizeOpenAPI31(transcluded); return { spec: toValue(normalized), errors: [] }; }; diff --git a/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap b/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap index 3872b42ff5..8ada9a4ec7 100644 --- a/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap +++ b/test/resolver/strategies/openapi-3-1/__snapshots__/index.js.snap @@ -1594,6 +1594,581 @@ exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via spec } `; +exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via spec option and pathDiscriminator is empty list should resolve entire spec 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": { + "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": "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": { + "properties": { + "id": { + "format": "int64", + "type": "integer", + }, + "name": { + "type": "string", + }, + "tag": { + "type": "string", + }, + }, + "required": [ + "id", + "name", + ], + "type": "object", + }, + }, + }, + "description": "Expected response to a valid request", + }, + "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": "Info for a specific pet", + "tags": [ + "pets", + ], + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + }, + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1", + }, + ], + }, +} +`; + +exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via spec option and pathDiscriminator=[paths, /pets] should resolve within the pathDiscriminator 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": { + "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": "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": "#/components/schemas/Pet", + }, + }, + }, + "description": "Expected response to a valid request", + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/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", + }, + ], + }, +} +`; + exports[`resolve OpenAPI 3.1.0 strategy given OpenAPI 3.1.0 definition via spec option and skipNormalization=false should resolve and normalize 1`] = ` { "errors": [], diff --git a/test/resolver/strategies/openapi-3-1/index.js b/test/resolver/strategies/openapi-3-1/index.js index 8b2c9953df..aaea4ae0b1 100644 --- a/test/resolver/strategies/openapi-3-1/index.js +++ b/test/resolver/strategies/openapi-3-1/index.js @@ -1,5 +1,6 @@ import path from 'node:path'; import fetchMock from 'fetch-mock'; +import { EvaluationJsonPointerError } from '@swagger-api/apidom-json-pointer'; import SwaggerClient from '../../../../src/index.js'; @@ -156,6 +157,43 @@ describe('resolve', () => { expect(resolvedSpec).toMatchSnapshot(); }); }); + + describe('and pathDiscriminator is empty list', () => { + test('should resolve entire spec', async () => { + const spec = globalThis.loadJsonFile(path.join(fixturePath, 'petstore.json')); + const resolvedSpec = await SwaggerClient.resolve({ + spec, + pathDiscriminator: [], + }); + + expect(resolvedSpec).toMatchSnapshot(); + }); + }); + + describe('and pathDiscriminator=[paths, /pets]', () => { + test('should resolve within the pathDiscriminator', async () => { + const spec = globalThis.loadJsonFile(path.join(fixturePath, 'petstore.json')); + const resolvedSpec = await SwaggerClient.resolve({ + spec, + pathDiscriminator: ['paths', '/pets'], + }); + + expect(resolvedSpec).toMatchSnapshot(); + }); + }); + + describe('and pathDiscriminator compiles into invalid JSON Pointer', () => { + test('should throw error', async () => { + const spec = globalThis.loadJsonFile(path.join(fixturePath, 'petstore.json')); + const resolveThunk = () => + SwaggerClient.resolve({ + spec, + pathDiscriminator: ['path', 'to', 'nothing'], + }); + + await expect(resolveThunk()).rejects.toThrow(EvaluationJsonPointerError); + }); + }); }); }); });