From 14abf27e87d3de52ee7358b109aa278dcd47b7f0 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 13:36:02 +0200 Subject: [PATCH 01/15] feat(fromOpenApi): add possibility to filter oas --- src/open-api/from-open-api.ts | 21 ++++++++++-- test/oas/from-open-api.test.ts | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 test/oas/from-open-api.test.ts diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index d54e506..c4a3ffd 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -11,6 +11,14 @@ const supportedHttpMethods = Object.keys( http, ) as unknown as SupportedHttpMethods +type PathItemTuple = [ + url: string, + OpenAPIV3.PathItemObject | OpenAPIV2.PathItemObject | undefined, +] +export type MapPathItemFunction = ( + pathItem: PathItemTuple, +) => PathItemTuple | undefined + /** * Generates request handlers from the given OpenAPI V2/V3 document. * @@ -20,6 +28,7 @@ const supportedHttpMethods = Object.keys( */ export async function fromOpenApi( document: string | OpenAPI.Document | OpenAPIV3.Document | OpenAPIV2.Document, + mapPathItem?: MapPathItemFunction, ): Promise> { const specification = await SwaggerParser.dereference(document) const requestHandlers: Array = [] @@ -28,8 +37,16 @@ export async function fromOpenApi( return [] } - const pathItems = Object.entries(specification.paths ?? {}) - for (const item of pathItems) { + const pathItems = Object.entries( + specification.paths ?? {}, + ) as Array + + for (const rowPathItem of pathItems) { + const item = mapPathItem ? mapPathItem(rowPathItem) : rowPathItem + if (!item) { + continue + } + const [url, handlers] = item const pathItem = handlers as | OpenAPIV2.PathItemObject diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts new file mode 100644 index 0000000..890e4a3 --- /dev/null +++ b/test/oas/from-open-api.test.ts @@ -0,0 +1,59 @@ +// @vitest-environment happy-dom +import { + fromOpenApi, + MapPathItemFunction, +} from '../../src/open-api/from-open-api.js' +import { createOpenApiSpec } from '../support/create-open-api-spec.js' +import { InspectedHandler, inspectHandlers } from '../support/inspect.js' + +it('creates handlers based on provided filter', async () => { + const openApiSpec = createOpenApiSpec({ + paths: { + '/numbers': { + get: { + responses: { + 200: { + content: { + 'application/json': { + example: [1, 2, 3], + }, + }, + }, + }, + }, + }, + '/orders': { + get: { + responses: { + 200: { + content: { + 'application/json': { + example: [{ id: 1 }, { id: 2 }, { id: 3 }], + }, + }, + }, + }, + }, + }, + }, + }) + + const mapPathItem: MapPathItemFunction = (pathItemTuple) => + pathItemTuple[0] === '/numbers' ? pathItemTuple : undefined + const handlers = await fromOpenApi(openApiSpec, mapPathItem) + + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([1, 2, 3]), + }, + }, + ]) +}) From e1674fb986ab34dd61b540b0777537ac948cd021 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 14:10:40 +0200 Subject: [PATCH 02/15] refactor(fromOpenApi): make mapper function accept 3 arguments --- src/open-api/from-open-api.ts | 33 ++++++++++++++++----------------- test/oas/from-open-api.test.ts | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index c4a3ffd..7d2c60e 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -11,13 +11,11 @@ const supportedHttpMethods = Object.keys( http, ) as unknown as SupportedHttpMethods -type PathItemTuple = [ +export type MapOperationFunction = ( url: string, - OpenAPIV3.PathItemObject | OpenAPIV2.PathItemObject | undefined, -] -export type MapPathItemFunction = ( - pathItem: PathItemTuple, -) => PathItemTuple | undefined + method: SupportedHttpMethods, + operation: OpenAPIV3.OperationObject, +) => OpenAPIV3.OperationObject | undefined /** * Generates request handlers from the given OpenAPI V2/V3 document. @@ -28,7 +26,7 @@ export type MapPathItemFunction = ( */ export async function fromOpenApi( document: string | OpenAPI.Document | OpenAPIV3.Document | OpenAPIV2.Document, - mapPathItem?: MapPathItemFunction, + mapOperation?: MapOperationFunction, ): Promise> { const specification = await SwaggerParser.dereference(document) const requestHandlers: Array = [] @@ -37,16 +35,9 @@ export async function fromOpenApi( return [] } - const pathItems = Object.entries( - specification.paths ?? {}, - ) as Array - - for (const rowPathItem of pathItems) { - const item = mapPathItem ? mapPathItem(rowPathItem) : rowPathItem - if (!item) { - continue - } + const pathItems = Object.entries(specification.paths ?? {}) + for (const item of pathItems) { const [url, handlers] = item const pathItem = handlers as | OpenAPIV2.PathItemObject @@ -60,7 +51,15 @@ export async function fromOpenApi( continue } - const operation = pathItem[method] as OpenAPIV3.OperationObject + const rowOperation = pathItem[method] as OpenAPIV3.OperationObject + if (!rowOperation) { + continue + } + + const operation = mapOperation + ? mapOperation(url, method, rowOperation) + : rowOperation + if (!operation) { continue } diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 890e4a3..dcc4032 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { fromOpenApi, - MapPathItemFunction, + MapOperationFunction, } from '../../src/open-api/from-open-api.js' import { createOpenApiSpec } from '../support/create-open-api-spec.js' import { InspectedHandler, inspectHandlers } from '../support/inspect.js' @@ -21,6 +21,17 @@ it('creates handlers based on provided filter', async () => { }, }, }, + put: { + responses: { + 200: { + content: { + 'application/json': { + example: [1, 2, 3], + }, + }, + }, + }, + }, }, '/orders': { get: { @@ -38,8 +49,9 @@ it('creates handlers based on provided filter', async () => { }, }) - const mapPathItem: MapPathItemFunction = (pathItemTuple) => - pathItemTuple[0] === '/numbers' ? pathItemTuple : undefined + const mapPathItem: MapOperationFunction = (url, method, operation) => { + return url === '/numbers' && method === 'get' ? operation : undefined + } const handlers = await fromOpenApi(openApiSpec, mapPathItem) expect(await inspectHandlers(handlers)).toEqual([ From ef6531c9cf51fa0d86174605afa3ed95857f5283 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 14:12:11 +0200 Subject: [PATCH 03/15] refactor(fromOpenApi): change variable name to more descriptive one --- test/oas/from-open-api.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index dcc4032..95e72ae 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -49,10 +49,10 @@ it('creates handlers based on provided filter', async () => { }, }) - const mapPathItem: MapOperationFunction = (url, method, operation) => { + const mapOperation: MapOperationFunction = (url, method, operation) => { return url === '/numbers' && method === 'get' ? operation : undefined } - const handlers = await fromOpenApi(openApiSpec, mapPathItem) + const handlers = await fromOpenApi(openApiSpec, mapOperation) expect(await inspectHandlers(handlers)).toEqual([ { From b3b644ae4b527214928f103baeacbf3fb43d14ba Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 14:14:17 +0200 Subject: [PATCH 04/15] style(fromOpenApi): remove unneeded empty line --- src/open-api/from-open-api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 7d2c60e..1f4821f 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -36,7 +36,6 @@ export async function fromOpenApi( } const pathItems = Object.entries(specification.paths ?? {}) - for (const item of pathItems) { const [url, handlers] = item const pathItem = handlers as From c49d3c72f9b64a5094faeef752cca7a097d36210 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 14:54:40 +0200 Subject: [PATCH 05/15] refactor(fromOpenApi): use object-as-argument for mapper function --- src/open-api/from-open-api.ts | 12 ++++++------ test/oas/from-open-api.test.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 1f4821f..381d920 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -11,11 +11,11 @@ const supportedHttpMethods = Object.keys( http, ) as unknown as SupportedHttpMethods -export type MapOperationFunction = ( - url: string, - method: SupportedHttpMethods, - operation: OpenAPIV3.OperationObject, -) => OpenAPIV3.OperationObject | undefined +export type MapOperationFunction = (args: { + url: string + method: SupportedHttpMethods + operation: OpenAPIV3.OperationObject +}) => OpenAPIV3.OperationObject | undefined /** * Generates request handlers from the given OpenAPI V2/V3 document. @@ -56,7 +56,7 @@ export async function fromOpenApi( } const operation = mapOperation - ? mapOperation(url, method, rowOperation) + ? mapOperation({ url, method, operation: rowOperation }) : rowOperation if (!operation) { diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 95e72ae..8808017 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -49,7 +49,7 @@ it('creates handlers based on provided filter', async () => { }, }) - const mapOperation: MapOperationFunction = (url, method, operation) => { + const mapOperation: MapOperationFunction = ({ url, method, operation }) => { return url === '/numbers' && method === 'get' ? operation : undefined } const handlers = await fromOpenApi(openApiSpec, mapOperation) From c642e1ff2a8aabb4ca4acb68af6d2059ac6ac7c0 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:10:43 +0200 Subject: [PATCH 06/15] refactor(fromOpenApi): rename url to path --- src/open-api/from-open-api.ts | 14 +++++++------- src/open-api/utils/normalize-swagger-path.test.ts | 15 +++++++++++++++ ...e-swagger-url.ts => normalize-swagger-path.ts} | 2 +- src/open-api/utils/normalize-swagger-url.test.ts | 15 --------------- test/oas/from-open-api.test.ts | 4 ++-- 5 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 src/open-api/utils/normalize-swagger-path.test.ts rename src/open-api/utils/{normalize-swagger-url.ts => normalize-swagger-path.ts} (74%) delete mode 100644 src/open-api/utils/normalize-swagger-url.test.ts diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 381d920..6519a21 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -1,7 +1,7 @@ import { RequestHandler, HttpHandler, http } from 'msw' import type { OpenAPIV3, OpenAPIV2, OpenAPI } from 'openapi-types' import SwaggerParser from '@apidevtools/swagger-parser' -import { normalizeSwaggerUrl } from './utils/normalize-swagger-url.js' +import { normalizeSwaggerPath } from './utils/normalize-swagger-path.js' import { getServers } from './utils/get-servers.js' import { isAbsoluteUrl, joinPaths } from './utils/url.js' import { createResponseResolver } from './utils/open-api-utils.js' @@ -12,7 +12,7 @@ const supportedHttpMethods = Object.keys( ) as unknown as SupportedHttpMethods export type MapOperationFunction = (args: { - url: string + path: string method: SupportedHttpMethods operation: OpenAPIV3.OperationObject }) => OpenAPIV3.OperationObject | undefined @@ -37,7 +37,7 @@ export async function fromOpenApi( const pathItems = Object.entries(specification.paths ?? {}) for (const item of pathItems) { - const [url, handlers] = item + const [path, handlers] = item const pathItem = handlers as | OpenAPIV2.PathItemObject | OpenAPIV3.PathItemObject @@ -56,7 +56,7 @@ export async function fromOpenApi( } const operation = mapOperation - ? mapOperation({ url, method, operation: rowOperation }) + ? mapOperation({ path, method, operation: rowOperation }) : rowOperation if (!operation) { @@ -66,10 +66,10 @@ export async function fromOpenApi( const serverUrls = getServers(specification) for (const baseUrl of serverUrls) { - const path = normalizeSwaggerUrl(url) + const normalizedPath = normalizeSwaggerPath(path) const requestUrl = isAbsoluteUrl(baseUrl) - ? new URL(path, baseUrl).href - : joinPaths(path, baseUrl) + ? new URL(normalizedPath, baseUrl).href + : joinPaths(normalizedPath, baseUrl) if ( typeof operation.responses === 'undefined' || diff --git a/src/open-api/utils/normalize-swagger-path.test.ts b/src/open-api/utils/normalize-swagger-path.test.ts new file mode 100644 index 0000000..9b2e061 --- /dev/null +++ b/src/open-api/utils/normalize-swagger-path.test.ts @@ -0,0 +1,15 @@ +import { normalizeSwaggerPath } from './normalize-swagger-path.js' + +it('replaces swagger path parameters with colons', () => { + expect(normalizeSwaggerPath('/user/{userId}')).toEqual('/user/:userId') + expect( + normalizeSwaggerPath('https://{subdomain}.example.com/{resource}/recent'), + ).toEqual('https://:subdomain.example.com/:resource/recent') +}) + +it('returns otherwise normal URL as-is', () => { + expect(normalizeSwaggerPath('/user/abc-123')).toEqual('/user/abc-123') + expect( + normalizeSwaggerPath('https://finance.example.com/reports/recent'), + ).toEqual('https://finance.example.com/reports/recent') +}) diff --git a/src/open-api/utils/normalize-swagger-url.ts b/src/open-api/utils/normalize-swagger-path.ts similarity index 74% rename from src/open-api/utils/normalize-swagger-url.ts rename to src/open-api/utils/normalize-swagger-path.ts index b62a530..1bda65b 100644 --- a/src/open-api/utils/normalize-swagger-url.ts +++ b/src/open-api/utils/normalize-swagger-path.ts @@ -1,4 +1,4 @@ -export function normalizeSwaggerUrl(url: string): string { +export function normalizeSwaggerPath(url: string): string { return ( url // Replace OpenAPI style parameters (/pet/{petId}) diff --git a/src/open-api/utils/normalize-swagger-url.test.ts b/src/open-api/utils/normalize-swagger-url.test.ts deleted file mode 100644 index 80b6910..0000000 --- a/src/open-api/utils/normalize-swagger-url.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { normalizeSwaggerUrl } from './normalize-swagger-url.js' - -it('replaces swagger path parameters with colons', () => { - expect(normalizeSwaggerUrl('/user/{userId}')).toEqual('/user/:userId') - expect( - normalizeSwaggerUrl('https://{subdomain}.example.com/{resource}/recent'), - ).toEqual('https://:subdomain.example.com/:resource/recent') -}) - -it('returns otherwise normal URL as-is', () => { - expect(normalizeSwaggerUrl('/user/abc-123')).toEqual('/user/abc-123') - expect( - normalizeSwaggerUrl('https://finance.example.com/reports/recent'), - ).toEqual('https://finance.example.com/reports/recent') -}) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 8808017..4b04705 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -49,8 +49,8 @@ it('creates handlers based on provided filter', async () => { }, }) - const mapOperation: MapOperationFunction = ({ url, method, operation }) => { - return url === '/numbers' && method === 'get' ? operation : undefined + const mapOperation: MapOperationFunction = ({ path, method, operation }) => { + return path === '/numbers' && method === 'get' ? operation : undefined } const handlers = await fromOpenApi(openApiSpec, mapOperation) From b73aa7b296ea27dff904b7181c079b1481b0ede0 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:12:15 +0200 Subject: [PATCH 07/15] refactor(fromOpenApi): rowOperation -> rawOperation --- src/open-api/from-open-api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 6519a21..3e0e565 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -50,14 +50,14 @@ export async function fromOpenApi( continue } - const rowOperation = pathItem[method] as OpenAPIV3.OperationObject - if (!rowOperation) { + const rawOperation = pathItem[method] as OpenAPIV3.OperationObject + if (!rawOperation) { continue } const operation = mapOperation - ? mapOperation({ path, method, operation: rowOperation }) - : rowOperation + ? mapOperation({ path, method, operation: rawOperation }) + : rawOperation if (!operation) { continue From e6a83f2df69a984ce840a9c24571720ce2f5c871 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:18:34 +0200 Subject: [PATCH 08/15] refactor(fromOpenApi): expose open api document to mapper function --- src/open-api/from-open-api.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 3e0e565..6647c79 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -15,6 +15,7 @@ export type MapOperationFunction = (args: { path: string method: SupportedHttpMethods operation: OpenAPIV3.OperationObject + document: OpenAPI.Document }) => OpenAPIV3.OperationObject | undefined /** @@ -56,7 +57,12 @@ export async function fromOpenApi( } const operation = mapOperation - ? mapOperation({ path, method, operation: rawOperation }) + ? mapOperation({ + path, + method, + operation: rawOperation, + document: specification, + }) : rawOperation if (!operation) { From 89f2e80fc589ba5a59aff1b90fc3bdebd9b65203 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:20:07 +0200 Subject: [PATCH 09/15] test(fromOpenApi): inline mapper function --- test/oas/from-open-api.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 4b04705..55694e8 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -1,8 +1,5 @@ // @vitest-environment happy-dom -import { - fromOpenApi, - MapOperationFunction, -} from '../../src/open-api/from-open-api.js' +import { fromOpenApi } from '../../src/open-api/from-open-api.js' import { createOpenApiSpec } from '../support/create-open-api-spec.js' import { InspectedHandler, inspectHandlers } from '../support/inspect.js' @@ -49,10 +46,12 @@ it('creates handlers based on provided filter', async () => { }, }) - const mapOperation: MapOperationFunction = ({ path, method, operation }) => { - return path === '/numbers' && method === 'get' ? operation : undefined - } - const handlers = await fromOpenApi(openApiSpec, mapOperation) + const handlers = await fromOpenApi( + openApiSpec, + ({ path, method, operation }) => { + return path === '/numbers' && method === 'get' ? operation : undefined + }, + ) expect(await inspectHandlers(handlers)).toEqual([ { From e3d239184d940c562768f737ab647c7aa682f9ce Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:32:11 +0200 Subject: [PATCH 10/15] test(fromOpenApi): expect one handler to be generated --- test/oas/from-open-api.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 55694e8..22c7a3a 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -53,7 +53,9 @@ it('creates handlers based on provided filter', async () => { }, ) - expect(await inspectHandlers(handlers)).toEqual([ + const inspectedHandlers = await inspectHandlers(handlers) + expect(inspectHandlers.length).toBe(1) + expect(inspectedHandlers).toEqual([ { handler: { method: 'GET', From 45df8944b7508871660d9e84dfe7381eed3fbca8 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 15:51:32 +0200 Subject: [PATCH 11/15] test(fromOpenApi): add test for when mapper modifies operation --- test/oas/from-open-api.test.ts | 101 +++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 22c7a3a..9409178 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -70,3 +70,104 @@ it('creates handlers based on provided filter', async () => { }, ]) }) + +it('creates handler with modified response', async () => { + const openApiSpec = createOpenApiSpec({ + paths: { + '/numbers': { + description: 'Get numbers', + get: { + responses: { + 200: { + content: { + 'application/json': { + example: [1, 2, 3], + }, + }, + }, + }, + }, + put: { + responses: { + 200: { + content: { + 'application/json': { + example: [1, 2, 3], + }, + }, + }, + }, + }, + }, + '/orders': { + get: { + responses: { + 200: { + content: { + 'application/json': { + example: [{ id: 1 }, { id: 2 }, { id: 3 }], + }, + }, + }, + }, + }, + }, + }, + }) + + const handlers = await fromOpenApi( + openApiSpec, + ({ path, method, operation }) => { + return path === '/numbers' && method === 'get' + ? { + ...operation, + responses: { + 200: { + description: 'Get numbers response', + content: { 'application/json': { example: [10] } }, + }, + }, + } + : operation + }, + ) + + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([10]), + }, + }, + { + handler: { + method: 'PUT', + path: 'http://localhost/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([1, 2, 3]), + }, + }, + { + handler: { + method: 'GET', + path: 'http://localhost/orders', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }]), + }, + }, + ]) +}) From ef583e73a68e6ebbfffb2a91396ec7014dd03b13 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Wed, 17 Jul 2024 16:03:49 +0200 Subject: [PATCH 12/15] feat(fromOpenApi): rename parameter from url to path --- src/open-api/utils/normalize-swagger-path.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/open-api/utils/normalize-swagger-path.ts b/src/open-api/utils/normalize-swagger-path.ts index 1bda65b..29cf021 100644 --- a/src/open-api/utils/normalize-swagger-path.ts +++ b/src/open-api/utils/normalize-swagger-path.ts @@ -1,6 +1,6 @@ -export function normalizeSwaggerPath(url: string): string { +export function normalizeSwaggerPath(path: string): string { return ( - url + path // Replace OpenAPI style parameters (/pet/{petId}) // with the common path parameters (/pet/:petId). .replace(/\{(.+?)\}/g, ':$1') From 898c44385b7694356ea113dd2d7b6f37f15dc066 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Thu, 18 Jul 2024 11:46:08 +0200 Subject: [PATCH 13/15] feat(fromOpenApi): improve typings --- src/open-api/from-open-api.ts | 19 +++++++++++++------ src/open-api/utils/normalize-swagger-path.ts | 2 +- test/oas/from-open-api.test.ts | 6 ++++++ test/oas/oas-json-schema.test.ts | 9 ++++++--- test/oas/oas-response-headers.test.ts | 1 + test/oas/oas-servers.test.ts | 4 ++++ test/support/create-open-api-spec.ts | 10 +++++----- 7 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 6647c79..8117cc8 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -11,8 +11,10 @@ const supportedHttpMethods = Object.keys( http, ) as unknown as SupportedHttpMethods -export type MapOperationFunction = (args: { - path: string +type ExtractPaths = T extends { paths: infer P } ? keyof P : never + +export type MapOperationFunction = (args: { + path: TPaths method: SupportedHttpMethods operation: OpenAPIV3.OperationObject document: OpenAPI.Document @@ -25,9 +27,14 @@ export type MapOperationFunction = (args: { * import specification from './api.oas.json' * await fromOpenApi(specification) */ -export async function fromOpenApi( - document: string | OpenAPI.Document | OpenAPIV3.Document | OpenAPIV2.Document, - mapOperation?: MapOperationFunction, + +export async function fromOpenApi< + T extends string | OpenAPI.Document | OpenAPIV2.Document | OpenAPIV3.Document, +>( + document: T, + mapOperation?: MapOperationFunction< + T extends string ? string : ExtractPaths + >, ): Promise> { const specification = await SwaggerParser.dereference(document) const requestHandlers: Array = [] @@ -38,7 +45,7 @@ export async function fromOpenApi( const pathItems = Object.entries(specification.paths ?? {}) for (const item of pathItems) { - const [path, handlers] = item + const [path, handlers] = item as [ExtractPaths, any] const pathItem = handlers as | OpenAPIV2.PathItemObject | OpenAPIV3.PathItemObject diff --git a/src/open-api/utils/normalize-swagger-path.ts b/src/open-api/utils/normalize-swagger-path.ts index 29cf021..96a8227 100644 --- a/src/open-api/utils/normalize-swagger-path.ts +++ b/src/open-api/utils/normalize-swagger-path.ts @@ -1,4 +1,4 @@ -export function normalizeSwaggerPath(path: string): string { +export function normalizeSwaggerPath(path: T) { return ( path // Replace OpenAPI style parameters (/pet/{petId}) diff --git a/test/oas/from-open-api.test.ts b/test/oas/from-open-api.test.ts index 9409178..d9cd0d1 100644 --- a/test/oas/from-open-api.test.ts +++ b/test/oas/from-open-api.test.ts @@ -10,6 +10,7 @@ it('creates handlers based on provided filter', async () => { get: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -21,6 +22,7 @@ it('creates handlers based on provided filter', async () => { put: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -34,6 +36,7 @@ it('creates handlers based on provided filter', async () => { get: { responses: { 200: { + description: 'Orders response', content: { 'application/json': { example: [{ id: 1 }, { id: 2 }, { id: 3 }], @@ -79,6 +82,7 @@ it('creates handler with modified response', async () => { get: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -90,6 +94,7 @@ it('creates handler with modified response', async () => { put: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -103,6 +108,7 @@ it('creates handler with modified response', async () => { get: { responses: { 200: { + description: 'Orders response', content: { 'application/json': { example: [{ id: 1 }, { id: 2 }, { id: 3 }], diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index 83d818e..3d7ac92 100644 --- a/test/oas/oas-json-schema.test.ts +++ b/test/oas/oas-json-schema.test.ts @@ -3,6 +3,7 @@ import { fromOpenApi } from '../../src/open-api/from-open-api.js' import { withHandlers } from '../support/with-handlers.js' import { createOpenApiSpec } from '../support/create-open-api-spec.js' import { InspectedHandler, inspectHandlers } from '../support/inspect.js' +import { OpenAPI } from 'openapi-types' const ID_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ @@ -15,6 +16,7 @@ it('supports JSON Schema object', async () => { get: { responses: { '200': { + description: 'Cart response', content: { 'application/json': { schema: { @@ -79,10 +81,10 @@ it('normalizes path parameters', async () => { createOpenApiSpec({ paths: { '/pet/{petId}': { - get: { responses: { 200: {} } }, + get: { responses: { 200: { description: '' } } }, }, '/pet/{petId}/{foodId}': { - get: { responses: { 200: {} } }, + get: { responses: { 200: { description: '' } } }, }, }, }), @@ -114,7 +116,7 @@ it('treats operations without "responses" as not implemented (501)', async () => get: { responses: null }, }, }, - }), + } as unknown as OpenAPI.Document), ) expect(await inspectHandlers(handlers)).toEqual([ { @@ -203,6 +205,7 @@ it('respects the "Accept" request header', async () => { get: { responses: { 200: { + description: 'User response', content: { 'application/json': { example: { id: 'user-1' }, diff --git a/test/oas/oas-response-headers.test.ts b/test/oas/oas-response-headers.test.ts index 1acac38..63ed649 100644 --- a/test/oas/oas-response-headers.test.ts +++ b/test/oas/oas-response-headers.test.ts @@ -11,6 +11,7 @@ it('supports response headers', async () => { get: { responses: { 200: { + description: 'User response', headers: { 'X-Rate-Limit-Remaining': { schema: { diff --git a/test/oas/oas-servers.test.ts b/test/oas/oas-servers.test.ts index 5651d9b..d799684 100644 --- a/test/oas/oas-servers.test.ts +++ b/test/oas/oas-servers.test.ts @@ -12,6 +12,7 @@ it('supports absolute server url', async () => { get: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -50,6 +51,7 @@ it('supports relative server url', async () => { post: { responses: { 200: { + description: 'Token response', content: { 'plain/text': { example: 'abc-123', @@ -87,6 +89,7 @@ it('supports multiple server urls', async () => { get: { responses: { 200: { + description: 'Numbers response', content: { 'application/json': { example: [1, 2, 3], @@ -136,6 +139,7 @@ it('supports the "basePath" url', async () => { get: { responses: { 200: { + description: 'Strings response', content: { 'application/json': { example: ['a', 'b', 'c'], diff --git a/test/support/create-open-api-spec.ts b/test/support/create-open-api-spec.ts index 2b61427..45e33f1 100644 --- a/test/support/create-open-api-spec.ts +++ b/test/support/create-open-api-spec.ts @@ -1,10 +1,10 @@ import { OpenAPI } from 'openapi-types' -export function createOpenApiSpec( - document: Partial, -): OpenAPI.Document { +export function createOpenApiSpec>( + document: T, +) { return Object.assign( - {} as OpenAPI.Document, + {}, { openapi: '3.0.0', info: { @@ -14,5 +14,5 @@ export function createOpenApiSpec( paths: {}, }, document, - ) + ) satisfies T } From a214ae2cc4805fa67a223e3dcae2d7426cba0ec4 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Thu, 18 Jul 2024 11:52:07 +0200 Subject: [PATCH 14/15] refactor(fromOpenApi): make generic singular --- src/open-api/from-open-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 8117cc8..9c67583 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -13,8 +13,8 @@ const supportedHttpMethods = Object.keys( type ExtractPaths = T extends { paths: infer P } ? keyof P : never -export type MapOperationFunction = (args: { - path: TPaths +export type MapOperationFunction = (args: { + path: TPath method: SupportedHttpMethods operation: OpenAPIV3.OperationObject document: OpenAPI.Document From 053d58b8e41c8bd943d63745b6b50d4debdad5f0 Mon Sep 17 00:00:00 2001 From: Jaunius Eitmantis Date: Thu, 18 Jul 2024 12:00:29 +0200 Subject: [PATCH 15/15] refactor(fromOpenApi): extract common type to a separate --- src/open-api/from-open-api.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/open-api/from-open-api.ts b/src/open-api/from-open-api.ts index 9c67583..05172ea 100644 --- a/src/open-api/from-open-api.ts +++ b/src/open-api/from-open-api.ts @@ -11,13 +11,19 @@ const supportedHttpMethods = Object.keys( http, ) as unknown as SupportedHttpMethods +type OpenApiDocument = + | string + | OpenAPI.Document + | OpenAPIV2.Document + | OpenAPIV3.Document + type ExtractPaths = T extends { paths: infer P } ? keyof P : never export type MapOperationFunction = (args: { path: TPath method: SupportedHttpMethods operation: OpenAPIV3.OperationObject - document: OpenAPI.Document + document: OpenApiDocument }) => OpenAPIV3.OperationObject | undefined /** @@ -28,9 +34,7 @@ export type MapOperationFunction = (args: { * await fromOpenApi(specification) */ -export async function fromOpenApi< - T extends string | OpenAPI.Document | OpenAPIV2.Document | OpenAPIV3.Document, ->( +export async function fromOpenApi( document: T, mapOperation?: MapOperationFunction< T extends string ? string : ExtractPaths