From 9193f6ec0a8f409057d8c4a9d6424fe5faa28289 Mon Sep 17 00:00:00 2001 From: Jordan Shatford Date: Thu, 21 Mar 2024 15:33:04 +1100 Subject: [PATCH 1/4] feat: common getRef function for both v2 and v3 parsing --- src/openApi/common/interfaces/OpenApi.ts | 4 +++ .../parser/__tests__}/getRef.spec.ts | 36 +++++++++++++++++-- src/openApi/{v3 => common}/parser/getRef.ts | 9 ++--- .../v2/parser/getOperationParameter.ts | 2 +- .../v2/parser/getOperationParameters.ts | 2 +- src/openApi/v2/parser/getOperationResponse.ts | 2 +- .../v2/parser/getOperationResponses.ts | 2 +- src/openApi/v2/parser/getRef.spec.ts | 35 ------------------ src/openApi/v2/parser/getRef.ts | 33 ----------------- .../getRequiredPropertiesFromComposition.ts | 2 +- .../v3/parser/getOperationParameter.ts | 2 +- .../v3/parser/getOperationParameters.ts | 2 +- src/openApi/v3/parser/getOperationResponse.ts | 2 +- .../v3/parser/getOperationResponses.ts | 2 +- .../getRequiredPropertiesFromComposition.ts | 2 +- src/openApi/v3/parser/operation.ts | 2 +- src/utils/getOpenApiSpec.ts | 5 ++- 17 files changed, 56 insertions(+), 88 deletions(-) create mode 100644 src/openApi/common/interfaces/OpenApi.ts rename src/openApi/{v3/parser => common/parser/__tests__}/getRef.spec.ts (63%) rename src/openApi/{v3 => common}/parser/getRef.ts (73%) delete mode 100644 src/openApi/v2/parser/getRef.spec.ts delete mode 100644 src/openApi/v2/parser/getRef.ts diff --git a/src/openApi/common/interfaces/OpenApi.ts b/src/openApi/common/interfaces/OpenApi.ts new file mode 100644 index 000000000..41a9fe08d --- /dev/null +++ b/src/openApi/common/interfaces/OpenApi.ts @@ -0,0 +1,4 @@ +import type { OpenApi as OpenApiV2 } from '../../v2/interfaces/OpenApi'; +import type { OpenApi as OpenApiV3 } from '../../v3/interfaces/OpenApi'; + +export type OpenApi = OpenApiV2 | OpenApiV3; diff --git a/src/openApi/v3/parser/getRef.spec.ts b/src/openApi/common/parser/__tests__/getRef.spec.ts similarity index 63% rename from src/openApi/v3/parser/getRef.spec.ts rename to src/openApi/common/parser/__tests__/getRef.spec.ts index c76152be6..c0243c95f 100644 --- a/src/openApi/v3/parser/getRef.spec.ts +++ b/src/openApi/common/parser/__tests__/getRef.spec.ts @@ -1,8 +1,40 @@ import { describe, expect, it } from 'vitest'; -import { getRef } from './getRef'; +import { getRef } from '../getRef'; -describe('getRef', () => { +describe('getRef (v2)', () => { + it('should produce correct result', () => { + expect( + getRef( + { + swagger: '2.0', + info: { + title: 'dummy', + version: '1.0', + }, + host: 'localhost:8080', + basePath: '/api', + schemes: ['http', 'https'], + paths: {}, + definitions: { + Example: { + description: 'This is an Example model ', + type: 'integer', + }, + }, + }, + { + $ref: '#/definitions/Example', + } + ) + ).toEqual({ + description: 'This is an Example model ', + type: 'integer', + }); + }); +}); + +describe('getRef (v3)', () => { it('should produce correct result', () => { expect( getRef( diff --git a/src/openApi/v3/parser/getRef.ts b/src/openApi/common/parser/getRef.ts similarity index 73% rename from src/openApi/v3/parser/getRef.ts rename to src/openApi/common/parser/getRef.ts index 896212d3a..437f87747 100644 --- a/src/openApi/v3/parser/getRef.ts +++ b/src/openApi/common/parser/getRef.ts @@ -1,10 +1,11 @@ -import type { OpenApi } from '../interfaces/OpenApi'; -import type { OpenApiReference } from '../interfaces/OpenApiReference'; +import type { OpenApiReference as OpenApiReferenceV2 } from '../../v2/interfaces/OpenApiReference'; +import type { OpenApiReference as OpenApiReferenceV3 } from '../../v3/interfaces/OpenApiReference'; +import { OpenApi } from '../interfaces/OpenApi'; const ESCAPED_REF_SLASH = /~1/g; const ESCAPED_REF_TILDE = /~0/g; -export const getRef = (openApi: OpenApi, item: T & OpenApiReference): T => { +export function getRef(openApi: OpenApi, item: T & (OpenApiReferenceV2 | OpenApiReferenceV3)): T { if (item.$ref) { // Fetch the paths to the definitions, this converts: // "#/components/schemas/Form" to ["components", "schemas", "Form"] @@ -30,4 +31,4 @@ export const getRef = (openApi: OpenApi, item: T & OpenApiReference): T => { return result as T; } return item as T; -}; +} diff --git a/src/openApi/v2/parser/getOperationParameter.ts b/src/openApi/v2/parser/getOperationParameter.ts index d43b17838..2401e7695 100644 --- a/src/openApi/v2/parser/getOperationParameter.ts +++ b/src/openApi/v2/parser/getOperationParameter.ts @@ -2,13 +2,13 @@ import type { OperationParameter } from '../../../types/client'; import { getEnums } from '../../../utils/getEnums'; import { getPattern } from '../../../utils/getPattern'; import { getType } from '../../../utils/type'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModel } from './getModel'; import { getOperationParameterDefault } from './getOperationParameterDefault'; import { getOperationParameterName } from './getOperationParameterName'; -import { getRef } from './getRef'; export const getOperationParameter = (openApi: OpenApi, parameter: OpenApiParameter): OperationParameter => { const operationParameter: OperationParameter = { diff --git a/src/openApi/v2/parser/getOperationParameters.ts b/src/openApi/v2/parser/getOperationParameters.ts index 9cc1f59ec..975781e23 100644 --- a/src/openApi/v2/parser/getOperationParameters.ts +++ b/src/openApi/v2/parser/getOperationParameters.ts @@ -1,8 +1,8 @@ import type { OperationParameters } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; -import { getRef } from './getRef'; export const getOperationParameters = (openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters => { const operationParameters: OperationParameters = { diff --git a/src/openApi/v2/parser/getOperationResponse.ts b/src/openApi/v2/parser/getOperationResponse.ts index d046b17dd..eb9bf29c4 100644 --- a/src/openApi/v2/parser/getOperationResponse.ts +++ b/src/openApi/v2/parser/getOperationResponse.ts @@ -1,11 +1,11 @@ import type { OperationResponse } from '../../../types/client'; import { getPattern } from '../../../utils/getPattern'; import { getType } from '../../../utils/type'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModel } from './getModel'; -import { getRef } from './getRef'; export const getOperationResponse = ( openApi: OpenApi, diff --git a/src/openApi/v2/parser/getOperationResponses.ts b/src/openApi/v2/parser/getOperationResponses.ts index cf0cf13f9..7c6abddcc 100644 --- a/src/openApi/v2/parser/getOperationResponses.ts +++ b/src/openApi/v2/parser/getOperationResponses.ts @@ -1,10 +1,10 @@ import type { OperationResponse } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiResponses } from '../interfaces/OpenApiResponses'; import { getOperationResponse } from './getOperationResponse'; import { getOperationResponseCode } from './getOperationResponseCode'; -import { getRef } from './getRef'; export const getOperationResponses = (openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] => { const operationResponses: OperationResponse[] = []; diff --git a/src/openApi/v2/parser/getRef.spec.ts b/src/openApi/v2/parser/getRef.spec.ts deleted file mode 100644 index 93eb2425b..000000000 --- a/src/openApi/v2/parser/getRef.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { getRef } from './getRef'; - -describe('getRef', () => { - it('should produce correct result', () => { - expect( - getRef( - { - swagger: '2.0', - info: { - title: 'dummy', - version: '1.0', - }, - host: 'localhost:8080', - basePath: '/api', - schemes: ['http', 'https'], - paths: {}, - definitions: { - Example: { - description: 'This is an Example model ', - type: 'integer', - }, - }, - }, - { - $ref: '#/definitions/Example', - } - ) - ).toEqual({ - description: 'This is an Example model ', - type: 'integer', - }); - }); -}); diff --git a/src/openApi/v2/parser/getRef.ts b/src/openApi/v2/parser/getRef.ts deleted file mode 100644 index cab57f17f..000000000 --- a/src/openApi/v2/parser/getRef.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { OpenApi } from '../interfaces/OpenApi'; -import type { OpenApiReference } from '../interfaces/OpenApiReference'; - -const ESCAPED_REF_SLASH = /~1/g; -const ESCAPED_REF_TILDE = /~0/g; - -export const getRef = (openApi: OpenApi, item: T & OpenApiReference): T => { - if (item.$ref) { - // Fetch the paths to the definitions, this converts: - // "#/definitions/Form" to ["definitions", "Form"] - const paths = item.$ref - .replace(/^#/g, '') - .split('/') - .filter(item => item); - - // Try to find the reference by walking down the path, - // if we cannot find it, then we throw an error. - let result = openApi; - paths.forEach(path => { - const decodedPath = decodeURIComponent( - path.replace(ESCAPED_REF_SLASH, '/').replace(ESCAPED_REF_TILDE, '~') - ); - if (result.hasOwnProperty(decodedPath)) { - // @ts-ignore - result = result[decodedPath]; - } else { - throw new Error(`Could not find reference: "${item.$ref}"`); - } - }); - return result as T; - } - return item as T; -}; diff --git a/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts b/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts index e8bc73935..d8ce87b67 100644 --- a/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts +++ b/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts @@ -1,8 +1,8 @@ import type { Model } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import type { getModel } from './getModel'; -import { getRef } from './getRef'; // Fix for circular dependency export type GetModelFn = typeof getModel; diff --git a/src/openApi/v3/parser/getOperationParameter.ts b/src/openApi/v3/parser/getOperationParameter.ts index 7479595d3..aa24294db 100644 --- a/src/openApi/v3/parser/getOperationParameter.ts +++ b/src/openApi/v3/parser/getOperationParameter.ts @@ -1,13 +1,13 @@ import type { OperationParameter } from '../../../types/client'; import { getPattern } from '../../../utils/getPattern'; import { getType } from '../../../utils/type'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModel } from './getModel'; import { getModelDefault } from './getModelDefault'; import { getOperationParameterName } from './getOperationParameterName'; -import { getRef } from './getRef'; export const getOperationParameter = (openApi: OpenApi, parameter: OpenApiParameter): OperationParameter => { const operationParameter: OperationParameter = { diff --git a/src/openApi/v3/parser/getOperationParameters.ts b/src/openApi/v3/parser/getOperationParameters.ts index 7c650f8dd..babb072d0 100644 --- a/src/openApi/v3/parser/getOperationParameters.ts +++ b/src/openApi/v3/parser/getOperationParameters.ts @@ -1,8 +1,8 @@ import type { OperationParameters } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; -import { getRef } from './getRef'; const allowedIn = ['cookie', 'formData', 'header', 'path', 'query'] as const; diff --git a/src/openApi/v3/parser/getOperationResponse.ts b/src/openApi/v3/parser/getOperationResponse.ts index ccd708ba1..b46912d9e 100644 --- a/src/openApi/v3/parser/getOperationResponse.ts +++ b/src/openApi/v3/parser/getOperationResponse.ts @@ -1,12 +1,12 @@ import type { OperationResponse } from '../../../types/client'; import { getPattern } from '../../../utils/getPattern'; import { getType } from '../../../utils/type'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getContent } from './getContent'; import { getModel } from './getModel'; -import { getRef } from './getRef'; export const getOperationResponse = ( openApi: OpenApi, diff --git a/src/openApi/v3/parser/getOperationResponses.ts b/src/openApi/v3/parser/getOperationResponses.ts index cf0cf13f9..7c6abddcc 100644 --- a/src/openApi/v3/parser/getOperationResponses.ts +++ b/src/openApi/v3/parser/getOperationResponses.ts @@ -1,10 +1,10 @@ import type { OperationResponse } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiResponses } from '../interfaces/OpenApiResponses'; import { getOperationResponse } from './getOperationResponse'; import { getOperationResponseCode } from './getOperationResponseCode'; -import { getRef } from './getRef'; export const getOperationResponses = (openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] => { const operationResponses: OperationResponse[] = []; diff --git a/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts b/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts index e8bc73935..d8ce87b67 100644 --- a/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts +++ b/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts @@ -1,8 +1,8 @@ import type { Model } from '../../../types/client'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import type { getModel } from './getModel'; -import { getRef } from './getRef'; // Fix for circular dependency export type GetModelFn = typeof getModel; diff --git a/src/openApi/v3/parser/operation.ts b/src/openApi/v3/parser/operation.ts index 6cd1f99ad..24c1ed0ab 100644 --- a/src/openApi/v3/parser/operation.ts +++ b/src/openApi/v3/parser/operation.ts @@ -1,6 +1,7 @@ import type { Operation, OperationParameter, OperationParameters } from '../../../types/client'; import type { Config } from '../../../types/config'; import { getOperationName } from '../../../utils/operation'; +import { getRef } from '../../common/parser/getRef'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody'; @@ -10,7 +11,6 @@ import { getOperationRequestBody } from './getOperationRequestBody'; import { getOperationResponseHeader } from './getOperationResponseHeader'; import { getOperationResponses } from './getOperationResponses'; import { getOperationResults } from './getOperationResults'; -import { getRef } from './getRef'; import { getServiceName } from './service'; // add global path parameters, skip duplicate names diff --git a/src/utils/getOpenApiSpec.ts b/src/utils/getOpenApiSpec.ts index a27727ec5..8ae1ad2f3 100644 --- a/src/utils/getOpenApiSpec.ts +++ b/src/utils/getOpenApiSpec.ts @@ -3,8 +3,7 @@ import path from 'node:path'; import $RefParser from '@apidevtools/json-schema-ref-parser'; -import type { OpenApi as OpenApiV2 } from '../openApi/v2/interfaces/OpenApi'; -import type { OpenApi as OpenApiV3 } from '../openApi/v3/interfaces/OpenApi'; +import type { OpenApi } from '../openApi/common/interfaces/OpenApi'; /** * Load and parse te open api spec. If the file extension is ".yml" or ".yaml" @@ -14,6 +13,6 @@ import type { OpenApi as OpenApiV3 } from '../openApi/v3/interfaces/OpenApi'; */ export const getOpenApiSpec = async (location: string) => { const absolutePathOrUrl = existsSync(location) ? path.resolve(location) : location; - const schema = (await $RefParser.bundle(absolutePathOrUrl, absolutePathOrUrl, {})) as OpenApiV2 | OpenApiV3; + const schema = (await $RefParser.bundle(absolutePathOrUrl, absolutePathOrUrl, {})) as OpenApi; return schema; }; From 6fa5f395c098871236f983c1b915b123e5277d92 Mon Sep 17 00:00:00 2001 From: Jordan Shatford Date: Thu, 21 Mar 2024 15:56:17 +1100 Subject: [PATCH 2/4] feat: move getServiceVersion to common parsing --- src/openApi/common/parser/__tests__/service.spec.ts | 13 +++++++++++++ .../parser/service.ts} | 4 +++- src/openApi/v2/index.ts | 2 +- src/openApi/v2/parser/getServiceVersion.spec.ts | 11 ----------- src/openApi/v3/index.ts | 2 +- src/openApi/v3/parser/__tests__/service.spec.ts | 10 +--------- src/openApi/v3/parser/service.ts | 7 ------- 7 files changed, 19 insertions(+), 30 deletions(-) create mode 100644 src/openApi/common/parser/__tests__/service.spec.ts rename src/openApi/{v2/parser/getServiceVersion.ts => common/parser/service.ts} (56%) delete mode 100644 src/openApi/v2/parser/getServiceVersion.spec.ts diff --git a/src/openApi/common/parser/__tests__/service.spec.ts b/src/openApi/common/parser/__tests__/service.spec.ts new file mode 100644 index 000000000..4bc2eac1e --- /dev/null +++ b/src/openApi/common/parser/__tests__/service.spec.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { getServiceVersion } from '../service'; + +describe('getServiceVersion', () => { + it.each([ + { input: '1.0', expected: '1.0' }, + { input: 'v1.2', expected: '1.2' }, + { input: 'V2.4', expected: '2.4' }, + ])('should get $expected when version is $input', ({ input, expected }) => { + expect(getServiceVersion(input)).toEqual(expected); + }); +}); diff --git a/src/openApi/v2/parser/getServiceVersion.ts b/src/openApi/common/parser/service.ts similarity index 56% rename from src/openApi/v2/parser/getServiceVersion.ts rename to src/openApi/common/parser/service.ts index 729a21530..7d860d6db 100644 --- a/src/openApi/v2/parser/getServiceVersion.ts +++ b/src/openApi/common/parser/service.ts @@ -3,4 +3,6 @@ * This basically removes any "v" prefix from the version string. * @param version */ -export const getServiceVersion = (version = '1.0'): string => String(version).replace(/^v/gi, ''); +export function getServiceVersion(version = '1.0'): string { + return String(version).replace(/^v/gi, ''); +} diff --git a/src/openApi/v2/index.ts b/src/openApi/v2/index.ts index 941ed8870..f1ee4a1b3 100644 --- a/src/openApi/v2/index.ts +++ b/src/openApi/v2/index.ts @@ -1,10 +1,10 @@ import type { Client } from '../../types/client'; import type { Config } from '../../types/config'; +import { getServiceVersion } from '../common/parser/service'; import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; import { getServer } from './parser/getServer'; import { getServices } from './parser/getServices'; -import { getServiceVersion } from './parser/getServiceVersion'; /** * Parse the OpenAPI specification to a Client model that contains diff --git a/src/openApi/v2/parser/getServiceVersion.spec.ts b/src/openApi/v2/parser/getServiceVersion.spec.ts deleted file mode 100644 index 1d47f5280..000000000 --- a/src/openApi/v2/parser/getServiceVersion.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { getServiceVersion } from './getServiceVersion'; - -describe('getServiceVersion', () => { - it('should produce correct result', () => { - expect(getServiceVersion('1.0')).toEqual('1.0'); - expect(getServiceVersion('v1.0')).toEqual('1.0'); - expect(getServiceVersion('V1.0')).toEqual('1.0'); - }); -}); diff --git a/src/openApi/v3/index.ts b/src/openApi/v3/index.ts index f19e49e15..a7ec3a3c5 100644 --- a/src/openApi/v3/index.ts +++ b/src/openApi/v3/index.ts @@ -1,10 +1,10 @@ import type { Client } from '../../types/client'; import type { Config } from '../../types/config'; +import { getServiceVersion } from '../common/parser/service'; import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; import { getServer } from './parser/getServer'; import { getServices } from './parser/getServices'; -import { getServiceVersion } from './parser/service'; /** * Parse the OpenAPI specification to a Client model that contains diff --git a/src/openApi/v3/parser/__tests__/service.spec.ts b/src/openApi/v3/parser/__tests__/service.spec.ts index a8d116ee6..4f7966db9 100644 --- a/src/openApi/v3/parser/__tests__/service.spec.ts +++ b/src/openApi/v3/parser/__tests__/service.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getServiceName, getServiceVersion } from '../service'; +import { getServiceName } from '../service'; describe('getServiceName', () => { it('should produce correct result', () => { @@ -14,11 +14,3 @@ describe('getServiceName', () => { expect(getServiceName('non-ascii-æøåÆØÅöôêÊ字符串')).toEqual('NonAsciiÆøåÆøÅöôêÊ字符串'); }); }); - -describe('getServiceVersion', () => { - it('should produce correct result', () => { - expect(getServiceVersion('1.0')).toEqual('1.0'); - expect(getServiceVersion('v1.0')).toEqual('1.0'); - expect(getServiceVersion('V1.0')).toEqual('1.0'); - }); -}); diff --git a/src/openApi/v3/parser/service.ts b/src/openApi/v3/parser/service.ts index e10a52420..878609344 100644 --- a/src/openApi/v3/parser/service.ts +++ b/src/openApi/v3/parser/service.ts @@ -12,10 +12,3 @@ export const getServiceName = (value: string): string => { const clean = sanitizeServiceName(value).trim(); return camelCase(clean, { pascalCase: true }); }; - -/** - * Convert the service version to 'normal' version. - * This basically removes any "v" prefix from the version string. - * @param version - */ -export const getServiceVersion = (version = '1.0'): string => String(version).replace(/^v/gi, ''); From c32eb39582f211a7a4dc5059931f13067ed54cde Mon Sep 17 00:00:00 2001 From: Jordan Shatford Date: Thu, 21 Mar 2024 16:06:54 +1100 Subject: [PATCH 3/4] feat: handle all version detection in openapi parse function --- src/index.spec.ts | 102 +--------------------------- src/index.ts | 15 +--- src/openApi/__tests__/index.spec.ts | 101 +++++++++++++++++++++++++++ src/openApi/index.ts | 20 ++++++ 4 files changed, 125 insertions(+), 113 deletions(-) create mode 100644 src/openApi/__tests__/index.spec.ts create mode 100644 src/openApi/index.ts diff --git a/src/index.spec.ts b/src/index.spec.ts index e3d84aa45..c943fdc42 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,8 +1,6 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { describe, it } from 'vitest'; -import { createClient, parseOpenApiSpecification } from './index'; -import * as parseV2 from './openApi/v2'; -import * as parseV3 from './openApi/v3'; +import { createClient } from './index'; describe('index', () => { it('parses v2 without issues', async () => { @@ -37,99 +35,3 @@ describe('index', () => { }); }); }); - -describe('parseOpenApiSpecification', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - const options: Parameters[1] = { - client: 'fetch', - enums: true, - exportCore: true, - exportModels: true, - exportSchemas: true, - exportServices: true, - format: true, - input: '', - lint: false, - operationId: true, - output: '', - postfixModels: '', - postfixServices: '', - serviceResponse: 'body', - useDateType: false, - useOptions: true, - write: false, - }; - - it('uses v2 parser', () => { - const spy = vi.spyOn(parseV2, 'parse'); - - const spec: Parameters[0] = { - info: { - title: 'dummy', - version: '1.0', - }, - paths: {}, - swagger: '2', - }; - parseOpenApiSpecification(spec, options); - expect(spy).toHaveBeenCalledWith(spec, options); - - const spec2: Parameters[0] = { - info: { - title: 'dummy', - version: '1.0', - }, - paths: {}, - swagger: '2.0', - }; - parseOpenApiSpecification(spec2, options); - expect(spy).toHaveBeenCalledWith(spec2, options); - }); - - it('uses v3 parser', () => { - const spy = vi.spyOn(parseV3, 'parse'); - - const spec: Parameters[0] = { - info: { - title: 'dummy', - version: '1.0', - }, - openapi: '3', - paths: {}, - }; - parseOpenApiSpecification(spec, options); - expect(spy).toHaveBeenCalledWith(spec, options); - - const spec2: Parameters[0] = { - info: { - title: 'dummy', - version: '1.0', - }, - openapi: '3.0', - paths: {}, - }; - parseOpenApiSpecification(spec2, options); - expect(spy).toHaveBeenCalledWith(spec2, options); - - const spec3: Parameters[0] = { - info: { - title: 'dummy', - version: '1.0', - }, - openapi: '3.1.0', - paths: {}, - }; - parseOpenApiSpecification(spec3, options); - expect(spy).toHaveBeenCalledWith(spec3, options); - }); - - it('throws on unknown version', () => { - // @ts-ignore - expect(() => parseOpenApiSpecification({ foo: 'bar' }, options)).toThrow( - `Unsupported Open API specification: ${JSON.stringify({ foo: 'bar' }, null, 2)}` - ); - }); -}); diff --git a/src/index.ts b/src/index.ts index f8454bf61..1ba7078ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,7 @@ import { pathToFileURL } from 'node:url'; import { sync } from 'cross-spawn'; -import { parse as parseV2 } from './openApi/v2'; -import { parse as parseV3 } from './openApi/v3'; +import { parse } from './openApi'; import type { Client } from './types/client'; import type { Config, UserConfig } from './types/config'; import { getOpenApiSpec } from './utils/getOpenApiSpec'; @@ -19,16 +18,6 @@ type Dependencies = Record; // add support for `openapi-ts.config.ts` const configFiles = ['openapi-ts.config.js']; -export const parseOpenApiSpecification = (openApi: Awaited>, config: Config) => { - if ('openapi' in openApi) { - return parseV3(openApi, config); - } - if ('swagger' in openApi) { - return parseV2(openApi, config); - } - throw new Error(`Unsupported Open API specification: ${JSON.stringify(openApi, null, 2)}`); -}; - const processOutput = (config: Config, dependencies: Dependencies) => { if (config.format) { if (dependencies.prettier) { @@ -170,7 +159,7 @@ export async function createClient(userConfig: UserConfig): Promise { ? await getOpenApiSpec(config.input) : (config.input as unknown as Awaited>); - const client = postProcessClient(parseOpenApiSpecification(openApi, config)); + const client = postProcessClient(parse(openApi, config)); const templates = registerHandlebarTemplates(config, client); if (config.write) { diff --git a/src/openApi/__tests__/index.spec.ts b/src/openApi/__tests__/index.spec.ts new file mode 100644 index 000000000..d324d7210 --- /dev/null +++ b/src/openApi/__tests__/index.spec.ts @@ -0,0 +1,101 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { parse } from '..'; +import * as parseV2 from '../v2'; +import * as parseV3 from '../v3'; + +describe('parse', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const options: Parameters[1] = { + client: 'fetch', + enums: true, + exportCore: true, + exportModels: true, + exportSchemas: true, + exportServices: true, + format: true, + input: '', + lint: false, + operationId: true, + output: '', + postfixModels: '', + postfixServices: '', + serviceResponse: 'body', + useDateType: false, + useOptions: true, + write: false, + }; + + it('uses v2 parser', () => { + const spy = vi.spyOn(parseV2, 'parse'); + + const spec: Parameters[0] = { + info: { + title: 'dummy', + version: '1.0', + }, + paths: {}, + swagger: '2', + }; + parse(spec, options); + expect(spy).toHaveBeenCalledWith(spec, options); + + const spec2: Parameters[0] = { + info: { + title: 'dummy', + version: '1.0', + }, + paths: {}, + swagger: '2.0', + }; + parse(spec2, options); + expect(spy).toHaveBeenCalledWith(spec2, options); + }); + + it('uses v3 parser', () => { + const spy = vi.spyOn(parseV3, 'parse'); + + const spec: Parameters[0] = { + info: { + title: 'dummy', + version: '1.0', + }, + openapi: '3', + paths: {}, + }; + parse(spec, options); + expect(spy).toHaveBeenCalledWith(spec, options); + + const spec2: Parameters[0] = { + info: { + title: 'dummy', + version: '1.0', + }, + openapi: '3.0', + paths: {}, + }; + parse(spec2, options); + expect(spy).toHaveBeenCalledWith(spec2, options); + + const spec3: Parameters[0] = { + info: { + title: 'dummy', + version: '1.0', + }, + openapi: '3.1.0', + paths: {}, + }; + parse(spec3, options); + expect(spy).toHaveBeenCalledWith(spec3, options); + }); + + it('throws on unknown version', () => { + // @ts-ignore + expect(() => parse({ foo: 'bar' }, options)).toThrow( + `Unsupported Open API specification: ${JSON.stringify({ foo: 'bar' }, null, 2)}` + ); + }); +}); diff --git a/src/openApi/index.ts b/src/openApi/index.ts new file mode 100644 index 000000000..be006f1c7 --- /dev/null +++ b/src/openApi/index.ts @@ -0,0 +1,20 @@ +import type { Client } from '../types/client'; +import type { Config } from '../types/config'; +import { OpenApi } from './common/interfaces/OpenApi'; +import { parse as parseV2 } from './v2/index'; +import { parse as parseV3 } from './v3/index'; + +/** + * Parse the OpenAPI specification to a Client model that contains + * all the models, services and schema's we should output. + * @param openApi The OpenAPI spec that we have loaded from disk. + * @param options {@link Config} passed to the `createClient()` method + */ +export function parse(openApi: OpenApi, config: Config): Client { + if ('openapi' in openApi) { + return parseV3(openApi, config); + } else if ('swagger' in openApi) { + return parseV2(openApi, config); + } + throw new Error(`Unsupported Open API specification: ${JSON.stringify(openApi, null, 2)}`); +} From 34b3ddb280f52e6f4651d0d0a2cd2a7347b3188b Mon Sep 17 00:00:00 2001 From: Jordan Shatford Date: Thu, 21 Mar 2024 16:49:07 +1100 Subject: [PATCH 4/4] feat(parser): move sort by required to common parsing --- .../common/parser/__tests__/sort.spec.ts | 34 +++++++++++++++++++ src/openApi/common/parser/sort.ts | 13 +++++++ src/openApi/v2/parser/getOperation.ts | 13 ++----- src/openApi/v3/parser/operation.ts | 12 ++----- 4 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 src/openApi/common/parser/__tests__/sort.spec.ts create mode 100644 src/openApi/common/parser/sort.ts diff --git a/src/openApi/common/parser/__tests__/sort.spec.ts b/src/openApi/common/parser/__tests__/sort.spec.ts new file mode 100644 index 000000000..840da7ef8 --- /dev/null +++ b/src/openApi/common/parser/__tests__/sort.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { toSortedByRequired } from '../sort'; + +describe('sort', () => { + it.each([ + { + input: [ + { id: 'test', isRequired: false }, + { id: 'test2', isRequired: true }, + { id: 'test3', isRequired: true }, + ], + expected: [ + { id: 'test2', isRequired: true }, + { id: 'test3', isRequired: true }, + { id: 'test', isRequired: false }, + ], + }, + { + input: [ + { id: 'test', isRequired: false }, + { id: 'test2', isRequired: false }, + { id: 'test3', isRequired: true, default: 'something' }, + ], + expected: [ + { id: 'test', isRequired: false }, + { id: 'test2', isRequired: false }, + { id: 'test3', isRequired: true, default: 'something' }, + ], + }, + ])('should sort $input by required to produce $expected', ({ input, expected }) => { + expect(toSortedByRequired(input)).toEqual(expected); + }); +}); diff --git a/src/openApi/common/parser/sort.ts b/src/openApi/common/parser/sort.ts new file mode 100644 index 000000000..66c591980 --- /dev/null +++ b/src/openApi/common/parser/sort.ts @@ -0,0 +1,13 @@ +/** + * Sort list of values and ensure that required parameters are first so that we do not generate + * invalid types. Optional parameters cannot be positioned after required ones. + */ +export function toSortedByRequired(values: T[]): T[] { + return values.sort((a, b) => { + const aNeedsValue = a.isRequired && a.default === undefined; + const bNeedsValue = b.isRequired && b.default === undefined; + if (aNeedsValue && !bNeedsValue) return -1; + if (bNeedsValue && !aNeedsValue) return 1; + return 0; + }); +} diff --git a/src/openApi/v2/parser/getOperation.ts b/src/openApi/v2/parser/getOperation.ts index 7c5b1dbad..aea6064c2 100644 --- a/src/openApi/v2/parser/getOperation.ts +++ b/src/openApi/v2/parser/getOperation.ts @@ -1,6 +1,7 @@ -import type { Operation, OperationParameter, OperationParameters } from '../../../types/client'; +import type { Operation, OperationParameters } from '../../../types/client'; import type { Config } from '../../../types/config'; import { getOperationName } from '../../../utils/operation'; +import { toSortedByRequired } from '../../common/parser/sort'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import { getOperationErrors } from './getOperationErrors'; @@ -10,14 +11,6 @@ import { getOperationResponses } from './getOperationResponses'; import { getOperationResults } from './getOperationResults'; import { getServiceName } from './getServiceName'; -const sortByRequired = (a: OperationParameter, b: OperationParameter): number => { - const aNeedsValue = a.isRequired && a.default === undefined; - const bNeedsValue = b.isRequired && b.default === undefined; - if (aNeedsValue && !bNeedsValue) return -1; - if (bNeedsValue && !aNeedsValue) return 1; - return 0; -}; - export const getOperation = ( openApi: OpenApi, url: string, @@ -79,7 +72,7 @@ export const getOperation = ( }); } - operation.parameters = operation.parameters.sort(sortByRequired); + operation.parameters = toSortedByRequired(operation.parameters); return operation; }; diff --git a/src/openApi/v3/parser/operation.ts b/src/openApi/v3/parser/operation.ts index 24c1ed0ab..97f8ce3d2 100644 --- a/src/openApi/v3/parser/operation.ts +++ b/src/openApi/v3/parser/operation.ts @@ -2,6 +2,7 @@ import type { Operation, OperationParameter, OperationParameters } from '../../. import type { Config } from '../../../types/config'; import { getOperationName } from '../../../utils/operation'; import { getRef } from '../../common/parser/getRef'; +import { toSortedByRequired } from '../../common/parser/sort'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody'; @@ -109,15 +110,8 @@ export const getOperation = ( operation.parametersPath = mergeParameters(operation.parametersPath, pathParams.parametersPath); operation.parametersQuery = mergeParameters(operation.parametersQuery, pathParams.parametersQuery); - // place required parameters first so we don't generate invalid types since - // optional parameters cannot be positioned before required ones - operation.parameters = operation.parameters.sort((a: OperationParameter, b: OperationParameter): number => { - const aNeedsValue = a.isRequired && a.default === undefined; - const bNeedsValue = b.isRequired && b.default === undefined; - if (aNeedsValue && !bNeedsValue) return -1; - if (bNeedsValue && !aNeedsValue) return 1; - return 0; - }); + // Sort by required + operation.parameters = toSortedByRequired(operation.parameters); return operation; };