diff --git a/packages/openapi-ts/src/generate/__tests__/class.spec.ts b/packages/openapi-ts/src/generate/__tests__/class.spec.ts index 7bc005e31..6dc6c81b3 100644 --- a/packages/openapi-ts/src/generate/__tests__/class.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/class.spec.ts @@ -35,7 +35,6 @@ describe('generateClientClass', () => { const client: Parameters[2] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, diff --git a/packages/openapi-ts/src/generate/__tests__/core.spec.ts b/packages/openapi-ts/src/generate/__tests__/core.spec.ts index 1c1c704f8..3ebf059e3 100644 --- a/packages/openapi-ts/src/generate/__tests__/core.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/core.spec.ts @@ -18,7 +18,6 @@ describe('generateCore', () => { it('writes to filesystem', async () => { const client: Parameters[1] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, @@ -79,7 +78,6 @@ describe('generateCore', () => { it('uses client server value for base', async () => { const client: Parameters[1] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, @@ -122,7 +120,6 @@ describe('generateCore', () => { it('uses custom value for base', async () => { const client: Parameters[1] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, diff --git a/packages/openapi-ts/src/generate/__tests__/output.spec.ts b/packages/openapi-ts/src/generate/__tests__/output.spec.ts index de0868456..bde837308 100644 --- a/packages/openapi-ts/src/generate/__tests__/output.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/output.spec.ts @@ -35,7 +35,6 @@ describe('generateOutput', () => { const client: Parameters[1] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, diff --git a/packages/openapi-ts/src/generate/__tests__/services.spec.ts b/packages/openapi-ts/src/generate/__tests__/services.spec.ts index 55f0b41ad..99f1cbdf6 100644 --- a/packages/openapi-ts/src/generate/__tests__/services.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/services.spec.ts @@ -38,7 +38,6 @@ describe('generateServices', () => { const client: Parameters[0]['client'] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [ { @@ -119,7 +118,6 @@ describe('methodNameBuilder', () => { const client: Parameters[0]['client'] = { models: [], - operationIds: new Map(), server: 'http://localhost:8080', services: [ { diff --git a/packages/openapi-ts/src/generate/__tests__/types.spec.ts b/packages/openapi-ts/src/generate/__tests__/types.spec.ts index 8c3d0c149..c083060e6 100644 --- a/packages/openapi-ts/src/generate/__tests__/types.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/types.spec.ts @@ -60,7 +60,6 @@ describe('generateTypes', () => { type: 'User', }, ], - operationIds: new Map(), server: 'http://localhost:8080', services: [], types: {}, diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 6b224e41c..f152406e7 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -11,6 +11,12 @@ import type { ClientConfig, Config, UserConfig } from './types/config'; import { getConfig, isStandaloneClient, setConfig } from './utils/config'; import { getOpenApiSpec } from './utils/getOpenApiSpec'; import { registerHandlebarTemplates } from './utils/handlebars'; +import { + operationFilterFn, + operationNameFn, + operationParameterFilterFn, + operationParameterNameFn, +} from './utils/parse'; import { Performance } from './utils/performance'; import { postProcessClient } from './utils/postprocess'; @@ -319,7 +325,19 @@ export async function createClient(userConfig: UserConfig): Promise { >); Performance.start('parser'); - const parsed = parse(openApi); + const parsed = parse({ + config: { + filterFn: { + operation: operationFilterFn, + operationParameter: operationParameterFilterFn, + }, + nameFn: { + operation: operationNameFn, + operationParameter: operationParameterNameFn, + }, + }, + openApi, + }); const client = postProcessClient(parsed); Performance.end('parser'); diff --git a/packages/openapi-ts/src/openApi/__tests__/index.spec.ts b/packages/openapi-ts/src/openApi/__tests__/index.spec.ts index 7fa6f4a9c..5776d80d7 100644 --- a/packages/openapi-ts/src/openApi/__tests__/index.spec.ts +++ b/packages/openapi-ts/src/openApi/__tests__/index.spec.ts @@ -1,19 +1,16 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { Config } from '../../types/config'; -import { parse } from '..'; +import { type OpenApi, parse } from '..'; +import type { Config } from '../common/interfaces/config'; import * as parseV2 from '../v2'; import * as parseV3 from '../v3'; -vi.mock('../../utils/config', () => { - const config: Partial = { - services: {}, - types: {}, - }; - return { - getConfig: () => config, - }; -}); +const config: Config = { + nameFn: { + operation: () => 'operation', + operationParameter: () => 'operationParameter', + }, +}; describe('parse', () => { afterEach(() => { @@ -23,7 +20,7 @@ describe('parse', () => { it('uses v2 parser', () => { const spy = vi.spyOn(parseV2, 'parse'); - const spec: Parameters[0] = { + const spec: OpenApi = { info: { title: 'dummy', version: '1.0', @@ -31,10 +28,10 @@ describe('parse', () => { paths: {}, swagger: '2', }; - parse(spec); + parse({ config, openApi: spec }); expect(spy).toHaveBeenCalledWith(spec); - const spec2: Parameters[0] = { + const spec2: OpenApi = { info: { title: 'dummy', version: '1.0', @@ -42,14 +39,14 @@ describe('parse', () => { paths: {}, swagger: '2.0', }; - parse(spec2); + parse({ config, openApi: spec2 }); expect(spy).toHaveBeenCalledWith(spec2); }); it('uses v3 parser', () => { const spy = vi.spyOn(parseV3, 'parse'); - const spec: Parameters[0] = { + const spec: OpenApi = { info: { title: 'dummy', version: '1.0', @@ -57,10 +54,10 @@ describe('parse', () => { openapi: '3', paths: {}, }; - parse(spec); + parse({ config, openApi: spec }); expect(spy).toHaveBeenCalledWith(spec); - const spec2: Parameters[0] = { + const spec2: OpenApi = { info: { title: 'dummy', version: '1.0', @@ -68,10 +65,10 @@ describe('parse', () => { openapi: '3.0', paths: {}, }; - parse(spec2); + parse({ config, openApi: spec2 }); expect(spy).toHaveBeenCalledWith(spec2); - const spec3: Parameters[0] = { + const spec3: OpenApi = { info: { title: 'dummy', version: '1.0', @@ -79,13 +76,13 @@ describe('parse', () => { openapi: '3.1.0', paths: {}, }; - parse(spec3); + parse({ config, openApi: spec3 }); expect(spy).toHaveBeenCalledWith(spec3); }); it('throws on unknown version', () => { // @ts-ignore - expect(() => parse({ foo: 'bar' })).toThrow( + expect(() => parse({ config, openApi: { foo: 'bar' } })).toThrow( `Unsupported Open API specification: ${JSON.stringify({ foo: 'bar' }, null, 2)}`, ); }); diff --git a/packages/openapi-ts/src/openApi/common/interfaces/config.ts b/packages/openapi-ts/src/openApi/common/interfaces/config.ts new file mode 100644 index 000000000..bd29668ff --- /dev/null +++ b/packages/openapi-ts/src/openApi/common/interfaces/config.ts @@ -0,0 +1,13 @@ +import type { Operation, OperationParameter } from './client'; + +export interface Config { + debug?: boolean; + filterFn?: { + operation?: (operation: Operation) => boolean; + operationParameter?: (parameter: OperationParameter) => boolean; + }; + nameFn: { + operation: (operation: Omit) => string; + operationParameter: (parameter: Omit) => string; + }; +} diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts index 79e40262a..ca9a7e24a 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts @@ -1,284 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { setConfig } from '../../../../utils/config'; -import { getOperationName, parseResponseStatusCode } from '../operation'; - -describe('getOperationName', () => { - const optionsCommon: Parameters[0] = { - client: { - name: 'legacy/fetch', - }, - configFile: '', - debug: false, - dryRun: true, - experimental_parser: false, - exportCore: false, - input: '', - output: { - path: '', - }, - plugins: [], - schemas: { - export: false, - }, - services: { - export: false, - operationId: true, - response: 'body', - }, - types: { - export: false, - }, - useOptions: false, - }; - - const options1: Parameters[0] = { - ...optionsCommon, - services: { - export: false, - operationId: true, - response: 'body', - }, - }; - - const options2: Parameters[0] = { - ...optionsCommon, - services: { - export: false, - operationId: false, - response: 'body', - }, - }; - - const options3: Parameters[0] = { - ...optionsCommon, - client: { - name: '@hey-api/client-fetch', - }, - services: { - export: false, - operationId: true, - response: 'body', - }, - }; - - const options4: Parameters[0] = { - ...optionsCommon, - client: { - name: '@hey-api/client-fetch', - }, - services: { - export: false, - operationId: false, - response: 'body', - }, - }; - - it.each([ - { - expected: 'getAllUsers', - method: 'GET', - operationId: 'GetAllUsers', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'getApiUsers', - method: 'GET', - operationId: undefined, - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'postApiUsers', - method: 'POST', - operationId: undefined, - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'getAllUsers', - method: 'GET', - operationId: 'GetAllUsers', - options: options1, - url: '/api/v1/users', - }, - { - expected: 'getApiV1Users', - method: 'GET', - operationId: undefined, - options: options1, - url: '/api/v1/users', - }, - { - expected: 'postApiV1Users', - method: 'POST', - operationId: undefined, - options: options1, - url: '/api/v1/users', - }, - { - expected: 'getApiV1UsersById', - method: 'GET', - operationId: undefined, - options: options1, - url: '/api/v1/users/{id}', - }, - { - expected: 'postApiV1UsersById', - method: 'POST', - operationId: undefined, - options: options1, - url: '/api/v1/users/{id}', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'fooBar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'FooBar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'Foo Bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'foo bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'foo-bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'foo_bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: '@foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: '$foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: '_foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: '-foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: '123.foo.bar', - options: options1, - url: '/api/v{api-version}/users', - }, - { - expected: 'getApiV1Users', - method: 'GET', - operationId: 'GetAllUsers', - options: options2, - url: '/api/v1/users', - }, - { - expected: 'getApiUsers', - method: 'GET', - operationId: 'fooBar', - options: options2, - url: '/api/v{api-version}/users', - }, - { - expected: 'getApiUsersByUserIdLocationByLocationId', - method: 'GET', - operationId: 'fooBar', - options: options2, - url: '/api/v{api-version}/users/{userId}/location/{locationId}', - }, - { - expected: 'getAllUsers', - method: 'GET', - operationId: 'GetAllUsers', - options: options3, - url: '/api/v1/users', - }, - { - expected: 'fooBar', - method: 'GET', - operationId: 'fooBar', - options: options3, - url: '/api/v{api-version}/users', - }, - { - expected: 'getApiV1Users', - method: 'GET', - operationId: 'GetAllUsers', - options: options4, - url: '/api/v1/users', - }, - { - expected: 'getApiVbyApiVersionUsers', - method: 'GET', - operationId: 'fooBar', - options: options4, - url: '/api/v{api-version}/users', - }, - { - expected: 'getApiVbyApiVersionUsersByUserIdLocationByLocationId', - method: 'GET', - operationId: 'fooBar', - options: options4, - url: '/api/v{api-version}/users/{userId}/location/{locationId}', - }, - ])( - 'getOperationName($url, $method, { operationId: $useOperationId }, $operationId) -> $expected', - ({ url, method, options, operationId, expected }) => { - setConfig(options); - expect(getOperationName(url, method, operationId)).toBe(expected); - }, - ); -}); +import { parseResponseStatusCode } from '../operation'; describe('parseResponseStatusCode', () => { it.each([ diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.spec.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.spec.ts index e9de4b3b9..f0bc7a526 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.spec.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.spec.ts @@ -1,8 +1,7 @@ -import { describe, expect, it, type MockedFunction, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { Config } from '../../../../types/config'; -import { isStandaloneClient } from '../../../../utils/config'; -import { getMappedType, getType, transformTypeKeyName } from '../type'; +import { getMappedType, getType } from '../type'; vi.mock('../../../../utils/config', () => { const config: Partial = { @@ -143,51 +142,3 @@ describe('getType', () => { expect(type.isNullable).toEqual(true); }); }); - -describe('transformTypeKeyName', () => { - describe('legacy client', () => { - it.each([ - { expected: '', input: '' }, - { expected: 'foobar', input: 'foobar' }, - { expected: 'fooBar', input: 'fooBar' }, - { expected: 'fooBar', input: 'foo_bar' }, - { expected: 'fooBar', input: 'foo-bar' }, - { expected: 'fooBar', input: 'foo.bar' }, - { expected: 'fooBar', input: '@foo.bar' }, - { expected: 'fooBar', input: '$foo.bar' }, - { expected: 'fooBar', input: '123.foo.bar' }, - { expected: 'fooBar', input: 'Foo-Bar' }, - { expected: 'fooBar', input: 'FOO-BAR' }, - { expected: 'fooBar', input: 'foo[bar]' }, - { expected: 'fooBarArray', input: 'foo.bar[]' }, - ])('$input -> $expected', ({ input, expected }) => { - (isStandaloneClient as MockedFunction).mockImplementationOnce( - () => false, - ); - expect(transformTypeKeyName(input)).toBe(expected); - }); - }); - - describe('standalone client', () => { - it.each([ - { expected: '', input: '' }, - { expected: 'foobar', input: 'foobar' }, - { expected: 'fooBar', input: 'fooBar' }, - { expected: 'fooBar', input: 'foo_bar' }, - { expected: 'fooBar', input: 'foo-bar' }, - { expected: 'fooBar', input: 'foo.bar' }, - { expected: 'fooBar', input: '@foo.bar' }, - { expected: 'fooBar', input: '$foo.bar' }, - { expected: 'fooBar', input: '123.foo.bar' }, - { expected: 'fooBar', input: 'Foo-Bar' }, - { expected: 'fooBar', input: 'FOO-BAR' }, - { expected: 'fooBar', input: 'foo[bar]' }, - { expected: 'fooBarArray', input: 'foo.bar[]' }, - ])('$input -> $input', ({ input }) => { - (isStandaloneClient as MockedFunction).mockImplementationOnce( - () => true, - ); - expect(transformTypeKeyName(input)).toBe(input); - }); - }); -}); diff --git a/packages/openapi-ts/src/openApi/common/parser/operation.ts b/packages/openapi-ts/src/openApi/common/parser/operation.ts index 523608aec..d634c2e8c 100644 --- a/packages/openapi-ts/src/openApi/common/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/common/parser/operation.ts @@ -1,49 +1,7 @@ -import { camelCase } from '../../../utils/camelCase'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import type { OperationParameter, OperationResponse, } from '../interfaces/client'; -import { sanitizeNamespaceIdentifier } from './sanitize'; - -/** - * Convert the input value to a correct operation (method) class name. - * This will use the operation ID - if available - and otherwise fallback - * on a generated name from the URL - */ -export const getOperationName = ( - url: string, - method: string, - operationId?: string, -): string => { - const config = getConfig(); - - if (config.services.operationId && operationId) { - return camelCase({ - input: sanitizeNamespaceIdentifier(operationId), - }); - } - - let urlWithoutPlaceholders = url; - - // legacy clients ignore the "api-version" param since we do not want to - // add it as the first/default parameter for each of the service calls - if (!isStandaloneClient(config)) { - urlWithoutPlaceholders = urlWithoutPlaceholders.replace( - /[^/]*?{api-version}.*?\//g, - '', - ); - } - - urlWithoutPlaceholders = urlWithoutPlaceholders - .replace(/{(.*?)}/g, 'by-$1') - // replace slashes with hyphens for camelcase method at the end - .replace(/\//g, '-'); - - return camelCase({ - input: `${method}-${urlWithoutPlaceholders}`, - }); -}; export const getOperationResponseHeader = ( operationResponses: OperationResponse[], diff --git a/packages/openapi-ts/src/openApi/common/parser/type.ts b/packages/openapi-ts/src/openApi/common/parser/type.ts index 98a1c90c8..1165285f6 100644 --- a/packages/openapi-ts/src/openApi/common/parser/type.ts +++ b/packages/openapi-ts/src/openApi/common/parser/type.ts @@ -1,14 +1,8 @@ -import { camelCase } from '../../../utils/camelCase'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import { refParametersPartial } from '../../../utils/const'; -import { reservedWordsRegExp } from '../../../utils/reservedWords'; import { transformTypeName } from '../../../utils/transform'; import { isDefinitionTypeNullable } from '../../v3/parser/inferType'; import type { Type } from '../interfaces/Type'; -import { - ensureValidTypeScriptJavaScriptIdentifier, - sanitizeOperationParameterName, -} from './sanitize'; +import { ensureValidTypeScriptJavaScriptIdentifier } from './sanitize'; import { stripNamespace } from './stripNamespace'; /** @@ -172,21 +166,3 @@ export const getType = ({ return result; }; - -/** - * Replaces any invalid characters from a parameter name. - * For example: 'filter.someProperty' becomes 'filterSomeProperty'. - */ -export const transformTypeKeyName = (value: string): string => { - const config = getConfig(); - - // do not transform anything for standalone clients - if (isStandaloneClient(config)) { - return value; - } - - const name = camelCase({ - input: sanitizeOperationParameterName(value), - }).replace(reservedWordsRegExp, '_$1'); - return name; -}; diff --git a/packages/openapi-ts/src/openApi/config.ts b/packages/openapi-ts/src/openApi/config.ts new file mode 100644 index 000000000..b557a1a53 --- /dev/null +++ b/packages/openapi-ts/src/openApi/config.ts @@ -0,0 +1,10 @@ +import type { Config } from './common/interfaces/config'; + +let _config: Config; + +export const getConfig = () => _config; + +export const setConfig = (config: Config) => { + _config = config; + return getConfig(); +}; diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts index 15b3a78ef..06d27ff76 100644 --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -1,23 +1,34 @@ import type { Client } from '../types/client'; +import type { Config } from './common/interfaces/config'; import { OpenApi } from './common/interfaces/OpenApi'; +import { setConfig } from './config'; import { parse as parseV2 } from './v2/index'; import { parse as parseV3 } from './v3/index'; -export { +export type { Enum, Model, Operation, OperationParameter, Service, } from './common/interfaces/client'; -export { OpenApi } from './common/interfaces/OpenApi'; +export type { Config } from './common/interfaces/config'; +export type { OpenApi } from './common/interfaces/OpenApi'; /** * 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. */ -export function parse(openApi: OpenApi): Client { +export function parse({ + openApi, + config, +}: { + config: Config; + openApi: OpenApi; +}): Client { + setConfig(config); + if ('openapi' in openApi) { return parseV3(openApi); } diff --git a/packages/openapi-ts/src/openApi/v2/index.ts b/packages/openapi-ts/src/openApi/v2/index.ts index 53111b07c..98d4bc613 100644 --- a/packages/openapi-ts/src/openApi/v2/index.ts +++ b/packages/openapi-ts/src/openApi/v2/index.ts @@ -14,11 +14,10 @@ export const parse = (openApi: OpenApi): Client => { const version = getServiceVersion(openApi.info.version); const server = getServer(openApi); const { models, types } = getModels(openApi); - const { operationIds, services } = getServices({ openApi, types }); + const services = getServices({ openApi, types }); return { models, - operationIds, server, services, types, diff --git a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts index 9241eefc3..e5e4dca42 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts @@ -1,33 +1,18 @@ import { describe, expect, it } from 'vitest'; -import { setConfig } from '../../../../utils/config'; +import { setConfig } from '../../../config'; import { getServices } from '../getServices'; describe('getServices', () => { it('should create an unnamed service if tags are empty', () => { setConfig({ - client: { - name: 'legacy/fetch', + nameFn: { + operation: () => 'operation', + operationParameter: () => 'operationParameter', }, - configFile: '', - debug: false, - dryRun: true, - experimental_parser: false, - exportCore: true, - input: '', - output: { - path: '', - }, - plugins: [], - schemas: {}, - services: { - operationId: false, - }, - types: {}, - useOptions: true, }); - const { services } = getServices({ + const services = getServices({ openApi: { info: { title: 'x', diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts index 883d8ee9d..9c1aa9cf6 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts @@ -1,31 +1,28 @@ import type { Client } from '../../../types/client'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import type { OperationParameter } from '../../common/interfaces/client'; import { getDefault } from '../../common/parser/getDefault'; import { getEnums } from '../../common/parser/getEnums'; import { getPattern } from '../../common/parser/getPattern'; import { getRef } from '../../common/parser/getRef'; -import { getType, transformTypeKeyName } from '../../common/parser/type'; +import { getType } from '../../common/parser/type'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModel } from './getModel'; export const getOperationParameter = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - debug, openApi, parameter, types, }: { - debug?: boolean; openApi: OpenApi; parameter: OpenApiParameter; types: Client['types']; }): OperationParameter => { const config = getConfig(); - let operationParameter: OperationParameter = { + const operationParameterWithoutName: Omit = { $refs: [], base: 'unknown', description: parameter.description || null, @@ -50,9 +47,6 @@ export const getOperationParameter = ({ minLength: parameter.minLength, minimum: parameter.minimum, multipleOf: parameter.multipleOf, - name: isStandaloneClient(config) - ? parameter.name - : transformTypeKeyName(parameter.name), pattern: getPattern(parameter.pattern), prop: parameter.name, properties: [], @@ -60,6 +54,10 @@ export const getOperationParameter = ({ type: 'unknown', uniqueItems: parameter.uniqueItems, }; + let operationParameter = { + ...operationParameterWithoutName, + name: config.nameFn.operationParameter(operationParameterWithoutName), + }; if (parameter.$ref) { const model = getType({ type: parameter.$ref }); diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts index 9d1d4dba3..429843a36 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts @@ -1,7 +1,7 @@ import type { Client } from '../../../types/client'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import type { OperationParameters } from '../../common/interfaces/client'; import { getRef } from '../../common/parser/getRef'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; @@ -9,20 +9,16 @@ import { getOperationParameter } from './getOperationParameter'; const allowedIn = ['body', 'formData', 'header', 'path', 'query'] as const; export const getOperationParameters = ({ - debug, openApi, parameters, types, }: { - debug?: boolean; openApi: OpenApi; parameters: OpenApiParameter[]; types: Client['types']; }): OperationParameters => { const config = getConfig(); - const isStandalone = isStandaloneClient(config); - const operationParameters: OperationParameters = { $refs: [], imports: [], @@ -41,18 +37,15 @@ export const getOperationParameters = ({ parameterOrReference, ); const parameter = getOperationParameter({ - debug, openApi, parameter: parameterDef, types, }); - // legacy clients ignore the "api-version" param since we do not want to - // add it as the first/default parameter for each of the service calls - if ( - !allowedIn.includes(parameterDef.in) || - (!isStandalone && parameter.prop === 'api-version') - ) { + const skip = + config.filterFn?.operationParameter && + !config.filterFn?.operationParameter(parameter); + if (!allowedIn.includes(parameterDef.in) || skip) { return; } diff --git a/packages/openapi-ts/src/openApi/v2/parser/getServices.ts b/packages/openapi-ts/src/openApi/v2/parser/getServices.ts index 67002cd2d..c8458193c 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getServices.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getServices.ts @@ -1,11 +1,11 @@ import type { Client } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; import { unique } from '../../../utils/unique'; import type { Operation, Service } from '../../common/interfaces/client'; import { allowedServiceMethods, getNewService, } from '../../common/parser/service'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; @@ -16,13 +16,9 @@ export const getServices = ({ }: { openApi: OpenApi; types: Client['types']; -}): Pick => { +}): Service[] => { const config = getConfig(); - const regexp = config.services.filter - ? new RegExp(config.services.filter) - : undefined; - const operationIds = new Map(); const services = new Map(); @@ -38,9 +34,8 @@ export const getServices = ({ const method = key as Lowercase; const operationKey = `${method.toUpperCase()} ${url}`; - const shouldProcess = !regexp || regexp.test(operationKey); - if (shouldProcess && allowedServiceMethods.includes(method)) { + if (allowedServiceMethods.includes(method)) { const op = path[method]!; if (op.operationId) { @@ -53,10 +48,7 @@ export const getServices = ({ } } - const tags = - op.tags?.length && (config.services.asClass || config.name) - ? op.tags.filter(unique) - : ['Default']; + const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; tags.forEach((tag) => { const operation = getOperation({ method, @@ -67,19 +59,21 @@ export const getServices = ({ types, url, }); - const service = - services.get(operation.service) || getNewService(operation); - service.$refs = [...service.$refs, ...operation.$refs]; - service.imports = [...service.imports, ...operation.imports]; - service.operations = [...service.operations, operation]; - services.set(operation.service, service); + if ( + !config.filterFn?.operation || + config.filterFn?.operation(operation) + ) { + const service = + services.get(operation.service) || getNewService(operation); + service.$refs = [...service.$refs, ...operation.$refs]; + service.imports = [...service.imports, ...operation.imports]; + service.operations = [...service.operations, operation]; + services.set(operation.service, service); + } }); } } } - return { - operationIds, - services: Array.from(services.values()), - }; + return Array.from(services.values()); }; diff --git a/packages/openapi-ts/src/openApi/v2/parser/operation.ts b/packages/openapi-ts/src/openApi/v2/parser/operation.ts index df404f8ad..5c4e15f8d 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/operation.ts @@ -3,12 +3,10 @@ import type { Operation, OperationParameters, } from '../../common/interfaces/client'; -import { - getOperationName, - getOperationResponseHeader, -} from '../../common/parser/operation'; +import { getOperationResponseHeader } from '../../common/parser/operation'; import { getServiceName } from '../../common/parser/service'; import { toSortedByRequired } from '../../common/parser/sort'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import { getOperationParameters } from './getOperationParameters'; @@ -31,17 +29,16 @@ export const getOperation = ({ types: Client['types']; url: string; }): Operation => { + const config = getConfig(); const service = getServiceName(tag); - const name = getOperationName(url, method, op.operationId); - const operation: Operation = { + const operationWithoutName: Omit = { $refs: [], deprecated: op.deprecated === true, description: op.description || null, id: op.operationId || null, imports: [], method: method.toUpperCase() as Operation['method'], - name, parameters: [...pathParams.parameters], parametersBody: pathParams.parametersBody, parametersCookie: [...pathParams.parametersCookie], @@ -55,6 +52,10 @@ export const getOperation = ({ service, summary: op.summary || null, }; + const operation = { + ...operationWithoutName, + name: config.nameFn.operation(operationWithoutName), + }; if (op.parameters) { const parameters = getOperationParameters({ diff --git a/packages/openapi-ts/src/openApi/v3/index.ts b/packages/openapi-ts/src/openApi/v3/index.ts index 53111b07c..98d4bc613 100644 --- a/packages/openapi-ts/src/openApi/v3/index.ts +++ b/packages/openapi-ts/src/openApi/v3/index.ts @@ -14,11 +14,10 @@ export const parse = (openApi: OpenApi): Client => { const version = getServiceVersion(openApi.info.version); const server = getServer(openApi); const { models, types } = getModels(openApi); - const { operationIds, services } = getServices({ openApi, types }); + const services = getServices({ openApi, types }); return { models, - operationIds, server, services, types, diff --git a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts index f643a7f54..941c88ad2 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts @@ -1,33 +1,18 @@ import { describe, expect, it } from 'vitest'; -import { setConfig } from '../../../../utils/config'; +import { setConfig } from '../../../config'; import { getServices } from '../getServices'; describe('getServices', () => { it('should create a unnamed service if tags are empty', () => { setConfig({ - client: { - name: 'legacy/fetch', + nameFn: { + operation: () => 'operation', + operationParameter: () => 'operationParameter', }, - configFile: '', - debug: false, - dryRun: true, - experimental_parser: false, - exportCore: true, - input: '', - output: { - path: '', - }, - plugins: [], - schemas: {}, - services: { - operationId: true, - }, - types: {}, - useOptions: true, }); - const { services } = getServices({ + const services = getServices({ openApi: { info: { title: 'x', diff --git a/packages/openapi-ts/src/openApi/v3/parser/getModels.ts b/packages/openapi-ts/src/openApi/v3/parser/getModels.ts index 433e2818b..745ce788a 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getModels.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getModels.ts @@ -1,6 +1,6 @@ import type { Client } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; import { getParametersMeta, getSchemasMeta } from '../../../utils/meta'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import { getModel } from './getModel'; import { getParameterSchema } from './parameter'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts index 242ac8885..f7ceabb65 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts @@ -1,12 +1,12 @@ import type { Client } from '../../../types/client'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import { refParametersPartial } from '../../../utils/const'; import { enumMeta } from '../../../utils/enum'; import type { OperationParameter } from '../../common/interfaces/client'; import { getDefault } from '../../common/parser/getDefault'; import { getPattern } from '../../common/parser/getPattern'; import { getRef } from '../../common/parser/getRef'; -import { getType, transformTypeKeyName } from '../../common/parser/type'; +import { getType } from '../../common/parser/type'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; @@ -15,20 +15,17 @@ import { isDefinitionNullable } from './inferType'; import { getParameterSchema } from './parameter'; export const getOperationParameter = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - debug, openApi, parameter, types, }: { - debug?: boolean; openApi: OpenApi; parameter: OpenApiParameter; types: Client['types']; }): OperationParameter => { const config = getConfig(); - let operationParameter: OperationParameter = { + const operationParameterWithoutName: Omit = { $refs: [], base: 'unknown', deprecated: parameter.deprecated === true, @@ -44,14 +41,15 @@ export const getOperationParameter = ({ isRequired: parameter.required === true, link: null, mediaType: null, - name: isStandaloneClient(config) - ? parameter.name - : transformTypeKeyName(parameter.name), prop: parameter.name, properties: [], template: null, type: 'unknown', }; + let operationParameter = { + ...operationParameterWithoutName, + name: config.nameFn.operationParameter(operationParameterWithoutName), + }; if (parameter.$ref) { const model = getType({ type: parameter.$ref }); diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts index 8bae01a41..e637d7922 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts @@ -1,7 +1,7 @@ import type { Client } from '../../../types/client'; -import { getConfig, isStandaloneClient } from '../../../utils/config'; import type { OperationParameters } from '../../common/interfaces/client'; import { getRef } from '../../common/parser/getRef'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; @@ -9,20 +9,16 @@ import { getOperationParameter } from './getOperationParameter'; const allowedIn = ['cookie', 'formData', 'header', 'path', 'query'] as const; export const getOperationParameters = ({ - debug, openApi, parameters, types, }: { - debug?: boolean; openApi: OpenApi; parameters: OpenApiParameter[]; types: Client['types']; }): OperationParameters => { const config = getConfig(); - const isStandalone = isStandaloneClient(config); - const operationParameters: OperationParameters = { $refs: [], imports: [], @@ -41,18 +37,15 @@ export const getOperationParameters = ({ parameterOrReference, ); const parameter = getOperationParameter({ - debug, openApi, parameter: parameterDef, types, }); - // legacy clients ignore the "api-version" param since we do not want to - // add it as the first/default parameter for each of the service calls - if ( - !allowedIn.includes(parameterDef.in) || - (!isStandalone && parameter.prop === 'api-version') - ) { + const skip = + config.filterFn?.operationParameter && + !config.filterFn?.operationParameter(parameter); + if (!allowedIn.includes(parameterDef.in) || skip) { return; } diff --git a/packages/openapi-ts/src/openApi/v3/parser/getServices.ts b/packages/openapi-ts/src/openApi/v3/parser/getServices.ts index 67002cd2d..c8458193c 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getServices.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getServices.ts @@ -1,11 +1,11 @@ import type { Client } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; import { unique } from '../../../utils/unique'; import type { Operation, Service } from '../../common/interfaces/client'; import { allowedServiceMethods, getNewService, } from '../../common/parser/service'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; @@ -16,13 +16,9 @@ export const getServices = ({ }: { openApi: OpenApi; types: Client['types']; -}): Pick => { +}): Service[] => { const config = getConfig(); - const regexp = config.services.filter - ? new RegExp(config.services.filter) - : undefined; - const operationIds = new Map(); const services = new Map(); @@ -38,9 +34,8 @@ export const getServices = ({ const method = key as Lowercase; const operationKey = `${method.toUpperCase()} ${url}`; - const shouldProcess = !regexp || regexp.test(operationKey); - if (shouldProcess && allowedServiceMethods.includes(method)) { + if (allowedServiceMethods.includes(method)) { const op = path[method]!; if (op.operationId) { @@ -53,10 +48,7 @@ export const getServices = ({ } } - const tags = - op.tags?.length && (config.services.asClass || config.name) - ? op.tags.filter(unique) - : ['Default']; + const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; tags.forEach((tag) => { const operation = getOperation({ method, @@ -67,19 +59,21 @@ export const getServices = ({ types, url, }); - const service = - services.get(operation.service) || getNewService(operation); - service.$refs = [...service.$refs, ...operation.$refs]; - service.imports = [...service.imports, ...operation.imports]; - service.operations = [...service.operations, operation]; - services.set(operation.service, service); + if ( + !config.filterFn?.operation || + config.filterFn?.operation(operation) + ) { + const service = + services.get(operation.service) || getNewService(operation); + service.$refs = [...service.$refs, ...operation.$refs]; + service.imports = [...service.imports, ...operation.imports]; + service.operations = [...service.operations, operation]; + services.set(operation.service, service); + } }); } } } - return { - operationIds, - services: Array.from(services.values()), - }; + return Array.from(services.values()); }; diff --git a/packages/openapi-ts/src/openApi/v3/parser/operation.ts b/packages/openapi-ts/src/openApi/v3/parser/operation.ts index e4c8c7c73..d3b903750 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/operation.ts @@ -5,12 +5,10 @@ import type { OperationParameters, } from '../../common/interfaces/client'; import { getRef } from '../../common/parser/getRef'; -import { - getOperationName, - getOperationResponseHeader, -} from '../../common/parser/operation'; +import { getOperationResponseHeader } from '../../common/parser/operation'; import { getServiceName } from '../../common/parser/service'; import { toSortedByRequired } from '../../common/parser/sort'; +import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody'; @@ -58,17 +56,16 @@ export const getOperation = ({ types: Client['types']; url: string; }): Operation => { + const config = getConfig(); const service = getServiceName(tag); - const name = getOperationName(url, method, op.operationId); - const operation: Operation = { + const operationWithoutName: Omit = { $refs: [], deprecated: Boolean(op.deprecated), description: op.description || null, id: op.operationId || null, imports: [], method: method.toUpperCase() as Operation['method'], - name, parameters: [], parametersBody: pathParams.parametersBody, parametersCookie: [], @@ -82,6 +79,10 @@ export const getOperation = ({ service, summary: op.summary || null, }; + const operation = { + ...operationWithoutName, + name: config.nameFn.operation(operationWithoutName), + }; if (op.parameters) { const parameters = getOperationParameters({ diff --git a/packages/openapi-ts/src/types/client.ts b/packages/openapi-ts/src/types/client.ts index 24f0e42db..29fffdfa3 100644 --- a/packages/openapi-ts/src/types/client.ts +++ b/packages/openapi-ts/src/types/client.ts @@ -3,12 +3,6 @@ import type { ModelMeta } from '../openApi/common/interfaces/client'; export interface Client { models: Model[]; - /** - * Map of unique operation IDs where operation IDs are keys. The values - * are endpoints in the `${method} ${path}` format. This is used to detect - * duplicate operation IDs in the specification. - */ - operationIds: Map; server: string; services: Service[]; /** diff --git a/packages/openapi-ts/src/utils/__tests__/parse.spec.ts b/packages/openapi-ts/src/utils/__tests__/parse.spec.ts new file mode 100644 index 000000000..02cb74ac8 --- /dev/null +++ b/packages/openapi-ts/src/utils/__tests__/parse.spec.ts @@ -0,0 +1,284 @@ +import { describe, expect, it } from 'vitest'; + +import type { Operation } from '../../openApi'; +import { setConfig } from '../config'; +import { operationNameFn } from '../parse'; + +describe('operationNameFn', () => { + const optionsCommon: Parameters[0] = { + client: { + name: 'legacy/fetch', + }, + configFile: '', + debug: false, + dryRun: true, + experimental_parser: false, + exportCore: false, + input: '', + output: { + path: '', + }, + plugins: [], + schemas: { + export: false, + }, + services: { + export: false, + operationId: true, + response: 'body', + }, + types: { + export: false, + }, + useOptions: false, + }; + + const options1: Parameters[0] = { + ...optionsCommon, + services: { + export: false, + operationId: true, + response: 'body', + }, + }; + + const options2: Parameters[0] = { + ...optionsCommon, + services: { + export: false, + operationId: false, + response: 'body', + }, + }; + + const options3: Parameters[0] = { + ...optionsCommon, + client: { + name: '@hey-api/client-fetch', + }, + services: { + export: false, + operationId: true, + response: 'body', + }, + }; + + const options4: Parameters[0] = { + ...optionsCommon, + client: { + name: '@hey-api/client-fetch', + }, + services: { + export: false, + operationId: false, + response: 'body', + }, + }; + + it.each([ + { + expected: 'getAllUsers', + method: 'GET', + operationId: 'GetAllUsers', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'getApiUsers', + method: 'GET', + operationId: undefined, + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'postApiUsers', + method: 'POST', + operationId: undefined, + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'getAllUsers', + method: 'GET', + operationId: 'GetAllUsers', + options: options1, + url: '/api/v1/users', + }, + { + expected: 'getApiV1Users', + method: 'GET', + operationId: undefined, + options: options1, + url: '/api/v1/users', + }, + { + expected: 'postApiV1Users', + method: 'POST', + operationId: undefined, + options: options1, + url: '/api/v1/users', + }, + { + expected: 'getApiV1UsersById', + method: 'GET', + operationId: undefined, + options: options1, + url: '/api/v1/users/{id}', + }, + { + expected: 'postApiV1UsersById', + method: 'POST', + operationId: undefined, + options: options1, + url: '/api/v1/users/{id}', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'fooBar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'FooBar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'Foo Bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'foo bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'foo-bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'foo_bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: '@foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: '$foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: '_foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: '-foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: '123.foo.bar', + options: options1, + url: '/api/v{api-version}/users', + }, + { + expected: 'getApiV1Users', + method: 'GET', + operationId: 'GetAllUsers', + options: options2, + url: '/api/v1/users', + }, + { + expected: 'getApiUsers', + method: 'GET', + operationId: 'fooBar', + options: options2, + url: '/api/v{api-version}/users', + }, + { + expected: 'getApiUsersByUserIdLocationByLocationId', + method: 'GET', + operationId: 'fooBar', + options: options2, + url: '/api/v{api-version}/users/{userId}/location/{locationId}', + }, + { + expected: 'getAllUsers', + method: 'GET', + operationId: 'GetAllUsers', + options: options3, + url: '/api/v1/users', + }, + { + expected: 'fooBar', + method: 'GET', + operationId: 'fooBar', + options: options3, + url: '/api/v{api-version}/users', + }, + { + expected: 'getApiV1Users', + method: 'GET', + operationId: 'GetAllUsers', + options: options4, + url: '/api/v1/users', + }, + { + expected: 'getApiVbyApiVersionUsers', + method: 'GET', + operationId: 'fooBar', + options: options4, + url: '/api/v{api-version}/users', + }, + { + expected: 'getApiVbyApiVersionUsersByUserIdLocationByLocationId', + method: 'GET', + operationId: 'fooBar', + options: options4, + url: '/api/v{api-version}/users/{userId}/location/{locationId}', + }, + ])( + 'getOperationName($url, $method, { operationId: $useOperationId }, $operationId) -> $expected', + ({ url, method, options, operationId, expected }) => { + setConfig(options); + expect( + operationNameFn({ id: operationId, method, path: url } as Operation), + ).toBe(expected); + }, + ); +}); diff --git a/packages/openapi-ts/src/utils/__tests__/type.spec.ts b/packages/openapi-ts/src/utils/__tests__/type.spec.ts new file mode 100644 index 000000000..62990d43e --- /dev/null +++ b/packages/openapi-ts/src/utils/__tests__/type.spec.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, type MockedFunction, vi } from 'vitest'; + +import type { Config } from '../../types/config'; +import { isStandaloneClient } from '../config'; +import { transformTypeKeyName } from '../type'; + +vi.mock('../config', () => { + const config: Partial = { + types: {}, + }; + return { + getConfig: () => config, + isStandaloneClient: vi.fn().mockReturnValue(false), + }; +}); + +describe('transformTypeKeyName', () => { + describe('legacy client', () => { + it.each([ + { expected: '', input: '' }, + { expected: 'foobar', input: 'foobar' }, + { expected: 'fooBar', input: 'fooBar' }, + { expected: 'fooBar', input: 'foo_bar' }, + { expected: 'fooBar', input: 'foo-bar' }, + { expected: 'fooBar', input: 'foo.bar' }, + { expected: 'fooBar', input: '@foo.bar' }, + { expected: 'fooBar', input: '$foo.bar' }, + { expected: 'fooBar', input: '123.foo.bar' }, + { expected: 'fooBar', input: 'Foo-Bar' }, + { expected: 'fooBar', input: 'FOO-BAR' }, + { expected: 'fooBar', input: 'foo[bar]' }, + { expected: 'fooBarArray', input: 'foo.bar[]' }, + ])('$input -> $expected', ({ input, expected }) => { + (isStandaloneClient as MockedFunction).mockImplementationOnce( + () => false, + ); + expect(transformTypeKeyName(input)).toBe(expected); + }); + }); + + describe('standalone client', () => { + it.each([ + { expected: '', input: '' }, + { expected: 'foobar', input: 'foobar' }, + { expected: 'fooBar', input: 'fooBar' }, + { expected: 'fooBar', input: 'foo_bar' }, + { expected: 'fooBar', input: 'foo-bar' }, + { expected: 'fooBar', input: 'foo.bar' }, + { expected: 'fooBar', input: '@foo.bar' }, + { expected: 'fooBar', input: '$foo.bar' }, + { expected: 'fooBar', input: '123.foo.bar' }, + { expected: 'fooBar', input: 'Foo-Bar' }, + { expected: 'fooBar', input: 'FOO-BAR' }, + { expected: 'fooBar', input: 'foo[bar]' }, + { expected: 'fooBarArray', input: 'foo.bar[]' }, + ])('$input -> $input', ({ input }) => { + (isStandaloneClient as MockedFunction).mockImplementationOnce( + () => true, + ); + expect(transformTypeKeyName(input)).toBe(input); + }); + }); +}); diff --git a/packages/openapi-ts/src/utils/parse.ts b/packages/openapi-ts/src/utils/parse.ts new file mode 100644 index 000000000..2fc624861 --- /dev/null +++ b/packages/openapi-ts/src/utils/parse.ts @@ -0,0 +1,73 @@ +import type { + Operation, + OperationParameter, +} from '../openApi/common/interfaces/client'; +import { sanitizeNamespaceIdentifier } from '../openApi/common/parser/sanitize'; +import { camelCase } from './camelCase'; +import { getConfig, isStandaloneClient } from './config'; +import { transformTypeKeyName } from './type'; + +export const operationFilterFn = (operation: Operation): boolean => { + const config = getConfig(); + + const regexp = config.services.filter + ? new RegExp(config.services.filter) + : undefined; + const operationKey = `${operation.method} ${operation.path}`; + return !regexp || regexp.test(operationKey); +}; + +export const operationParameterFilterFn = ( + parameter: OperationParameter, +): boolean => { + const config = getConfig(); + + // legacy clients ignore the "api-version" param since we do not want to + // add it as the first/default parameter for each of the service calls + return isStandaloneClient(config) || parameter.prop !== 'api-version'; +}; + +/** + * Convert the input value to a correct operation (method) class name. + * This will use the operation ID - if available - and otherwise fallback + * on a generated name from the URL + */ +export const operationNameFn = (operation: Omit): string => { + const config = getConfig(); + + if (config.services.operationId && operation.id) { + return camelCase({ + input: sanitizeNamespaceIdentifier(operation.id), + }); + } + + let urlWithoutPlaceholders = operation.path; + + // legacy clients ignore the "api-version" param since we do not want to + // add it as the first/default parameter for each of the service calls + if (!isStandaloneClient(config)) { + urlWithoutPlaceholders = urlWithoutPlaceholders.replace( + /[^/]*?{api-version}.*?\//g, + '', + ); + } + + urlWithoutPlaceholders = urlWithoutPlaceholders + .replace(/{(.*?)}/g, 'by-$1') + // replace slashes with hyphens for camelcase method at the end + .replace(/\//g, '-'); + + return camelCase({ + input: `${operation.method}-${urlWithoutPlaceholders}`, + }); +}; + +export const operationParameterNameFn = ( + parameter: Omit, +): string => { + const config = getConfig(); + + return isStandaloneClient(config) + ? parameter.prop + : transformTypeKeyName(parameter.prop); +}; diff --git a/packages/openapi-ts/src/utils/type.ts b/packages/openapi-ts/src/utils/type.ts index 74fc159e1..af1c52986 100644 --- a/packages/openapi-ts/src/utils/type.ts +++ b/packages/openapi-ts/src/utils/type.ts @@ -1,12 +1,14 @@ import { compiler, type Property, type TypeNode } from '../compiler'; import type { Model } from '../openApi'; -import { transformTypeKeyName } from '../openApi/common/parser/type'; +import { sanitizeOperationParameterName } from '../openApi/common/parser/sanitize'; import type { Client } from '../types/client'; +import { camelCase } from './camelCase'; import { getConfig, isStandaloneClient } from './config'; import { refSchemasPartial } from './const'; import { enumValue } from './enum'; import { escapeComment, escapeName, unescapeName } from './escape'; import { getSchemasMeta } from './meta'; +import { reservedWordsRegExp } from './reservedWords'; import { unique } from './unique'; const base = (model: Model) => { @@ -318,3 +320,21 @@ export const unsetUniqueTypeName = ({ }; return result; }; + +/** + * Replaces any invalid characters from a parameter name. + * For example: 'filter.someProperty' becomes 'filterSomeProperty'. + */ +export const transformTypeKeyName = (value: string): string => { + const config = getConfig(); + + // do not transform anything for standalone clients + if (isStandaloneClient(config)) { + return value; + } + + const name = camelCase({ + input: sanitizeOperationParameterName(value), + }).replace(reservedWordsRegExp, '_$1'); + return name; +}; -- 2.51.2 From 28c79bbacc57675dac2ecedcf967decd7579152a Mon Sep 17 00:00:00 2001 From: Jacob Cohen Date: Fri, 6 Sep 2024 17:25:18 -0400 Subject: [PATCH 2/3] refactor: seperate openapi-ts client from parser client --- .../src/generate/__tests__/services.spec.ts | 2 +- packages/openapi-ts/src/generate/services.ts | 9 +- .../src/openApi/common/interfaces/client.ts | 18 ++-- .../common/parser/__tests__/service.spec.ts | 20 +--- .../src/openApi/common/parser/service.ts | 21 ---- packages/openapi-ts/src/openApi/index.ts | 7 +- packages/openapi-ts/src/openApi/v2/index.ts | 10 +- .../v2/parser/__tests__/getServices.spec.ts | 44 --------- .../{getServices.ts => getOperations.ts} | 53 ++++------ .../src/openApi/v2/parser/operation.ts | 6 +- packages/openapi-ts/src/openApi/v3/index.ts | 10 +- .../v3/parser/__tests__/getServices.spec.ts | 44 --------- .../{getServices.ts => getOperations.ts} | 53 ++++------ .../src/openApi/v3/parser/operation.ts | 6 +- .../plugins/@tanstack/query-core/plugin.ts | 8 +- packages/openapi-ts/src/types/client.ts | 31 +++--- packages/openapi-ts/src/types/config.ts | 2 +- .../src/utils/__tests__/postprocess.spec.ts | 97 +++++++++++++++++++ .../src/utils/__tests__/sort.spec.ts | 2 +- packages/openapi-ts/src/utils/postprocess.ts | 57 ++++++++++- 20 files changed, 251 insertions(+), 249 deletions(-) delete mode 100644 packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts rename packages/openapi-ts/src/openApi/v2/parser/{getServices.ts => getOperations.ts} (50%) delete mode 100644 packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts rename packages/openapi-ts/src/openApi/v3/parser/{getServices.ts => getOperations.ts} (50%) create mode 100644 packages/openapi-ts/src/utils/__tests__/postprocess.spec.ts diff --git a/packages/openapi-ts/src/generate/__tests__/services.spec.ts b/packages/openapi-ts/src/generate/__tests__/services.spec.ts index 99f1cbdf6..6a92eca90 100644 --- a/packages/openapi-ts/src/generate/__tests__/services.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/services.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import { TypeScriptFile } from '../../compiler'; -import type { Operation } from '../../openApi'; +import type { Operation } from '../../types/client'; import type { Files } from '../../types/utils'; import { setConfig } from '../../utils/config'; import { generateServices } from '../services'; diff --git a/packages/openapi-ts/src/generate/services.ts b/packages/openapi-ts/src/generate/services.ts index 7044866f3..cada63da6 100644 --- a/packages/openapi-ts/src/generate/services.ts +++ b/packages/openapi-ts/src/generate/services.ts @@ -7,9 +7,14 @@ import { TypeScriptFile, } from '../compiler'; import type { FunctionTypeParameter, ObjectValue } from '../compiler/types'; -import type { Model, Operation, OperationParameter, Service } from '../openApi'; import { isOperationParameterRequired } from '../openApi/common/parser/operation'; -import type { Client } from '../types/client'; +import type { + Client, + Model, + Operation, + OperationParameter, + Service, +} from '../types/client'; import type { Files } from '../types/utils'; import { camelCase } from '../utils/camelCase'; import { getConfig, isStandaloneClient } from '../utils/config'; diff --git a/packages/openapi-ts/src/openApi/common/interfaces/client.ts b/packages/openapi-ts/src/openApi/common/interfaces/client.ts index fafa8a34e..ba9985f15 100644 --- a/packages/openapi-ts/src/openApi/common/interfaces/client.ts +++ b/packages/openapi-ts/src/openApi/common/interfaces/client.ts @@ -61,12 +61,8 @@ export interface Operation extends OperationParameters { * Sorted by status code. */ responses: OperationResponse[]; - /** - * Service name, might be without postfix. This will be used to name the - * exported class. - */ - service: string; summary: string | null; + tags: string[] | null; } export interface Schema { @@ -153,6 +149,16 @@ export interface Model extends Schema { type: string; } -export interface Service extends Pick { +export interface Client { + models: Model[]; operations: Operation[]; + server: string; + /** + * Map of generated types where type names are keys. This is used to track + * uniquely generated types as we may want to deduplicate if there are + * multiple definitions with the same name but different value, or if we + * want to transform names. + */ + types: Record; + version: string; } diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/service.spec.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/service.spec.ts index e553a2f25..c6577298d 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/service.spec.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/service.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getServiceName, getServiceVersion } from '../service'; +import { getServiceVersion } from '../service'; describe('getServiceVersion', () => { it.each([ @@ -11,21 +11,3 @@ describe('getServiceVersion', () => { expect(getServiceVersion(input)).toEqual(expected); }); }); - -describe('getServiceName', () => { - it.each([ - { expected: '', input: '' }, - { expected: 'FooBar', input: 'FooBar' }, - { expected: 'FooBar', input: 'Foo Bar' }, - { expected: 'FooBar', input: 'foo bar' }, - { expected: 'FooBar', input: '@fooBar' }, - { expected: 'FooBar', input: '$fooBar' }, - { expected: 'FooBar', input: '123fooBar' }, - { - expected: 'NonAsciiÆøåÆøÅöôêÊ字符串', - input: 'non-ascii-æøåÆØÅöôêÊ字符串', - }, - ])('getServiceName($input) -> $expected', ({ input, expected }) => { - expect(getServiceName(input)).toEqual(expected); - }); -}); diff --git a/packages/openapi-ts/src/openApi/common/parser/service.ts b/packages/openapi-ts/src/openApi/common/parser/service.ts index 218e67cdf..0351896f8 100644 --- a/packages/openapi-ts/src/openApi/common/parser/service.ts +++ b/packages/openapi-ts/src/openApi/common/parser/service.ts @@ -1,7 +1,3 @@ -import { camelCase } from '../../../utils/camelCase'; -import type { Operation, Service } from '../interfaces/client'; -import { sanitizeNamespaceIdentifier } from './sanitize'; - export const allowedServiceMethods = [ 'connect', 'delete', @@ -14,13 +10,6 @@ export const allowedServiceMethods = [ 'trace', ] as const; -export const getNewService = (operation: Operation): Service => ({ - $refs: [], - imports: [], - name: operation.service, - operations: [], -}); - /** * Convert the service version to 'normal' version. * This basically removes any "v" prefix from the version string. @@ -29,13 +18,3 @@ export const getNewService = (operation: Operation): Service => ({ export function getServiceVersion(version = '1.0'): string { return String(version).replace(/^v/gi, ''); } - -/** - * Convert the input value to a correct service name. This converts - * the input string to PascalCase. - */ -export const getServiceName = (value: string): string => - camelCase({ - input: sanitizeNamespaceIdentifier(value), - pascalCase: true, - }); diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts index 06d27ff76..7a9a4dfb9 100644 --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -1,16 +1,17 @@ -import type { Client } from '../types/client'; +import type { Client } from './common/interfaces/client'; import type { Config } from './common/interfaces/config'; -import { OpenApi } from './common/interfaces/OpenApi'; +import type { OpenApi } from './common/interfaces/OpenApi'; import { setConfig } from './config'; import { parse as parseV2 } from './v2/index'; import { parse as parseV3 } from './v3/index'; export type { + Client, Enum, + Method, Model, Operation, OperationParameter, - Service, } from './common/interfaces/client'; export type { Config } from './common/interfaces/config'; export type { OpenApi } from './common/interfaces/OpenApi'; diff --git a/packages/openapi-ts/src/openApi/v2/index.ts b/packages/openapi-ts/src/openApi/v2/index.ts index 98d4bc613..6ef57cdae 100644 --- a/packages/openapi-ts/src/openApi/v2/index.ts +++ b/packages/openapi-ts/src/openApi/v2/index.ts @@ -1,25 +1,25 @@ -import type { Client } from '../../types/client'; +import type { Client } from '../common/interfaces/client'; import { getServiceVersion } from '../common/parser/service'; import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; +import { getOperations } from './parser/getOperations'; import { getServer } from './parser/getServer'; -import { getServices } from './parser/getServices'; /** * Parse the OpenAPI specification to a Client model that contains - * all the models, services and schema's we should output. + * all the models, operations and schema's we should output. * @param openApi The OpenAPI spec that we have loaded from disk. */ export const parse = (openApi: OpenApi): Client => { const version = getServiceVersion(openApi.info.version); const server = getServer(openApi); const { models, types } = getModels(openApi); - const services = getServices({ openApi, types }); + const operations = getOperations({ openApi, types }); return { models, + operations, server, - services, types, version, }; diff --git a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts deleted file mode 100644 index e5e4dca42..000000000 --- a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { setConfig } from '../../../config'; -import { getServices } from '../getServices'; - -describe('getServices', () => { - it('should create an unnamed service if tags are empty', () => { - setConfig({ - nameFn: { - operation: () => 'operation', - operationParameter: () => 'operationParameter', - }, - }); - - const services = getServices({ - openApi: { - info: { - title: 'x', - version: '1', - }, - paths: { - '/api/trips': { - get: { - responses: { - 200: { - description: 'x', - }, - default: { - description: 'default', - }, - }, - tags: [], - }, - }, - }, - swagger: '2.0', - }, - types: {}, - }); - - expect(services).toHaveLength(1); - expect(services[0].name).toEqual('Default'); - }); -}); diff --git a/packages/openapi-ts/src/openApi/v2/parser/getServices.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts similarity index 50% rename from packages/openapi-ts/src/openApi/v2/parser/getServices.ts rename to packages/openapi-ts/src/openApi/v2/parser/getOperations.ts index c8458193c..03055570d 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getServices.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts @@ -1,26 +1,21 @@ -import type { Client } from '../../../types/client'; -import { unique } from '../../../utils/unique'; -import type { Operation, Service } from '../../common/interfaces/client'; -import { - allowedServiceMethods, - getNewService, -} from '../../common/parser/service'; +import type { Client, Operation } from '../../common/interfaces/client'; +import { allowedServiceMethods } from '../../common/parser/service'; import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; -export const getServices = ({ +export const getOperations = ({ openApi, types, }: { openApi: OpenApi; types: Client['types']; -}): Service[] => { +}): Operation[] => { const config = getConfig(); const operationIds = new Map(); - const services = new Map(); + const operations: Operation[] = []; for (const url in openApi.paths) { const path = openApi.paths[url]; @@ -48,32 +43,24 @@ export const getServices = ({ } } - const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; - tags.forEach((tag) => { - const operation = getOperation({ - method, - op, - openApi, - pathParams: pathParameters, - tag, - types, - url, - }); - if ( - !config.filterFn?.operation || - config.filterFn?.operation(operation) - ) { - const service = - services.get(operation.service) || getNewService(operation); - service.$refs = [...service.$refs, ...operation.$refs]; - service.imports = [...service.imports, ...operation.imports]; - service.operations = [...service.operations, operation]; - services.set(operation.service, service); - } + const operation = getOperation({ + method, + op, + openApi, + pathParams: pathParameters, + types, + url, }); + + if ( + !config.filterFn?.operation || + config.filterFn?.operation(operation) + ) { + operations.push(operation); + } } } } - return Array.from(services.values()); + return operations; }; diff --git a/packages/openapi-ts/src/openApi/v2/parser/operation.ts b/packages/openapi-ts/src/openApi/v2/parser/operation.ts index 5c4e15f8d..9288d6df2 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/operation.ts @@ -4,7 +4,6 @@ import type { OperationParameters, } from '../../common/interfaces/client'; import { getOperationResponseHeader } from '../../common/parser/operation'; -import { getServiceName } from '../../common/parser/service'; import { toSortedByRequired } from '../../common/parser/sort'; import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; @@ -18,19 +17,16 @@ export const getOperation = ({ openApi, types, pathParams, - tag, url, }: { method: Lowercase; op: OpenApiOperation; openApi: OpenApi; pathParams: OperationParameters; - tag: string; types: Client['types']; url: string; }): Operation => { const config = getConfig(); - const service = getServiceName(tag); const operationWithoutName: Omit = { $refs: [], @@ -49,8 +45,8 @@ export const getOperation = ({ path: url, responseHeader: null, responses: [], - service, summary: op.summary || null, + tags: op.tags || null, }; const operation = { ...operationWithoutName, diff --git a/packages/openapi-ts/src/openApi/v3/index.ts b/packages/openapi-ts/src/openApi/v3/index.ts index 98d4bc613..6ef57cdae 100644 --- a/packages/openapi-ts/src/openApi/v3/index.ts +++ b/packages/openapi-ts/src/openApi/v3/index.ts @@ -1,25 +1,25 @@ -import type { Client } from '../../types/client'; +import type { Client } from '../common/interfaces/client'; import { getServiceVersion } from '../common/parser/service'; import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; +import { getOperations } from './parser/getOperations'; import { getServer } from './parser/getServer'; -import { getServices } from './parser/getServices'; /** * Parse the OpenAPI specification to a Client model that contains - * all the models, services and schema's we should output. + * all the models, operations and schema's we should output. * @param openApi The OpenAPI spec that we have loaded from disk. */ export const parse = (openApi: OpenApi): Client => { const version = getServiceVersion(openApi.info.version); const server = getServer(openApi); const { models, types } = getModels(openApi); - const services = getServices({ openApi, types }); + const operations = getOperations({ openApi, types }); return { models, + operations, server, - services, types, version, }; diff --git a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts deleted file mode 100644 index 941c88ad2..000000000 --- a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { setConfig } from '../../../config'; -import { getServices } from '../getServices'; - -describe('getServices', () => { - it('should create a unnamed service if tags are empty', () => { - setConfig({ - nameFn: { - operation: () => 'operation', - operationParameter: () => 'operationParameter', - }, - }); - - const services = getServices({ - openApi: { - info: { - title: 'x', - version: '1', - }, - openapi: '3.0.0', - paths: { - '/api/trips': { - get: { - responses: { - 200: { - description: 'x', - }, - default: { - description: 'default', - }, - }, - tags: [], - }, - }, - }, - }, - types: {}, - }); - - expect(services).toHaveLength(1); - expect(services[0].name).toEqual('Default'); - }); -}); diff --git a/packages/openapi-ts/src/openApi/v3/parser/getServices.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts similarity index 50% rename from packages/openapi-ts/src/openApi/v3/parser/getServices.ts rename to packages/openapi-ts/src/openApi/v3/parser/getOperations.ts index c8458193c..03055570d 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getServices.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts @@ -1,26 +1,21 @@ -import type { Client } from '../../../types/client'; -import { unique } from '../../../utils/unique'; -import type { Operation, Service } from '../../common/interfaces/client'; -import { - allowedServiceMethods, - getNewService, -} from '../../common/parser/service'; +import type { Client, Operation } from '../../common/interfaces/client'; +import { allowedServiceMethods } from '../../common/parser/service'; import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; -export const getServices = ({ +export const getOperations = ({ openApi, types, }: { openApi: OpenApi; types: Client['types']; -}): Service[] => { +}): Operation[] => { const config = getConfig(); const operationIds = new Map(); - const services = new Map(); + const operations: Operation[] = []; for (const url in openApi.paths) { const path = openApi.paths[url]; @@ -48,32 +43,24 @@ export const getServices = ({ } } - const tags = op.tags?.length ? op.tags.filter(unique) : ['Default']; - tags.forEach((tag) => { - const operation = getOperation({ - method, - op, - openApi, - pathParams: pathParameters, - tag, - types, - url, - }); - if ( - !config.filterFn?.operation || - config.filterFn?.operation(operation) - ) { - const service = - services.get(operation.service) || getNewService(operation); - service.$refs = [...service.$refs, ...operation.$refs]; - service.imports = [...service.imports, ...operation.imports]; - service.operations = [...service.operations, operation]; - services.set(operation.service, service); - } + const operation = getOperation({ + method, + op, + openApi, + pathParams: pathParameters, + types, + url, }); + + if ( + !config.filterFn?.operation || + config.filterFn?.operation(operation) + ) { + operations.push(operation); + } } } } - return Array.from(services.values()); + return operations; }; diff --git a/packages/openapi-ts/src/openApi/v3/parser/operation.ts b/packages/openapi-ts/src/openApi/v3/parser/operation.ts index d3b903750..4bdb53978 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/operation.ts @@ -6,7 +6,6 @@ import type { } from '../../common/interfaces/client'; import { getRef } from '../../common/parser/getRef'; import { getOperationResponseHeader } from '../../common/parser/operation'; -import { getServiceName } from '../../common/parser/service'; import { toSortedByRequired } from '../../common/parser/sort'; import { getConfig } from '../../config'; import type { OpenApi } from '../interfaces/OpenApi'; @@ -43,7 +42,6 @@ export const getOperation = ({ op, openApi, pathParams, - tag, types, url, }: { @@ -52,12 +50,10 @@ export const getOperation = ({ op: OpenApiOperation; openApi: OpenApi; pathParams: OperationParameters; - tag: string; types: Client['types']; url: string; }): Operation => { const config = getConfig(); - const service = getServiceName(tag); const operationWithoutName: Omit = { $refs: [], @@ -76,8 +72,8 @@ export const getOperation = ({ path: url, responseHeader: null, responses: [], - service, summary: op.summary || null, + tags: op.tags || null, }; const operation = { ...operationWithoutName, diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts index df0ede0bb..4e7a851b5 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -15,14 +15,14 @@ import { operationResponseTypeName, toOperationName, } from '../../../generate/services'; -import type { Operation } from '../../../openApi'; +import { isOperationParameterRequired } from '../../../openApi/common/parser/operation'; +import type { Client } from '../../../types/client'; import type { Method, Model, + Operation, OperationParameter, -} from '../../../openApi/common/interfaces/client'; -import { isOperationParameterRequired } from '../../../openApi/common/parser/operation'; -import type { Client } from '../../../types/client'; +} from '../../../types/client'; import type { Files } from '../../../types/utils'; import { getConfig } from '../../../utils/config'; import type { PluginDefinition } from '../../types'; diff --git a/packages/openapi-ts/src/types/client.ts b/packages/openapi-ts/src/types/client.ts index 29fffdfa3..3728d13e2 100644 --- a/packages/openapi-ts/src/types/client.ts +++ b/packages/openapi-ts/src/types/client.ts @@ -1,16 +1,21 @@ -import { Model, Service } from '../openApi'; -import type { ModelMeta } from '../openApi/common/interfaces/client'; +import type { + Client as ParserClient, + Method, + Model, + Operation as ParserOperation, + OperationParameter, +} from '../openApi'; -export interface Client { - models: Model[]; - server: string; +export type { Method, Model, OperationParameter }; + +export interface Operation extends Omit { + service: string; +} + +export interface Service extends Pick { + operations: Operation[]; +} + +export interface Client extends Omit { services: Service[]; - /** - * Map of generated types where type names are keys. This is used to track - * uniquely generated types as we may want to deduplicate if there are - * multiple definitions with the same name but different value, or if we - * want to transform names. - */ - types: Record; - version: string; } diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 27a596e56..9fde1247a 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -1,7 +1,7 @@ -import type { Operation } from '../openApi'; import type { OpenApiSchema as OpenApiV2Schema } from '../openApi/v2/interfaces/OpenApiSchema'; import type { OpenApiSchema as OpenApiV3Schema } from '../openApi/v3/interfaces/OpenApiSchema'; import type { Plugins } from '../plugins/'; +import type { Operation } from '../types/client'; import type { ExtractArrayOfObjects } from './utils'; type Client = diff --git a/packages/openapi-ts/src/utils/__tests__/postprocess.spec.ts b/packages/openapi-ts/src/utils/__tests__/postprocess.spec.ts new file mode 100644 index 000000000..43d8ef2e2 --- /dev/null +++ b/packages/openapi-ts/src/utils/__tests__/postprocess.spec.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import { type Config, parse } from '../../openApi'; +import { getServiceName, postProcessClient } from '../postprocess'; + +const config: Config = { + nameFn: { + operation: () => 'operation', + operationParameter: () => 'operationParameter', + }, +}; + +describe('getServiceName', () => { + it.each([ + { expected: '', input: '' }, + { expected: 'FooBar', input: 'FooBar' }, + { expected: 'FooBar', input: 'Foo Bar' }, + { expected: 'FooBar', input: 'foo bar' }, + { expected: 'FooBar', input: '@fooBar' }, + { expected: 'FooBar', input: '$fooBar' }, + { expected: 'FooBar', input: '123fooBar' }, + { + expected: 'NonAsciiÆøåÆøÅöôêÊ字符串', + input: 'non-ascii-æøåÆØÅöôêÊ字符串', + }, + ])('getServiceName($input) -> $expected', ({ input, expected }) => { + expect(getServiceName(input)).toEqual(expected); + }); +}); + +describe('getServices', () => { + it('should create a unnamed service if tags are empty', () => { + const parserClient = parse({ + config, + openApi: { + info: { + title: 'x', + version: '1', + }, + openapi: '3.0.0', + paths: { + '/api/trips': { + get: { + responses: { + 200: { + description: 'x', + }, + default: { + description: 'default', + }, + }, + tags: [], + }, + }, + }, + }, + }); + const { services } = postProcessClient(parserClient); + + expect(services).toHaveLength(1); + expect(services[0].name).toEqual('Default'); + }); +}); + +describe('getServices', () => { + it('should create a unnamed service if tags are empty', () => { + const parserClient = parse({ + config, + openApi: { + info: { + title: 'x', + version: '1', + }, + openapi: '3.0.0', + paths: { + '/api/trips': { + get: { + responses: { + 200: { + description: 'x', + }, + default: { + description: 'default', + }, + }, + tags: [], + }, + }, + }, + }, + }); + const { services } = postProcessClient(parserClient); + + expect(services).toHaveLength(1); + expect(services[0].name).toEqual('Default'); + }); +}); diff --git a/packages/openapi-ts/src/utils/__tests__/sort.spec.ts b/packages/openapi-ts/src/utils/__tests__/sort.spec.ts index 3b4d2ac8e..4e6119eaf 100644 --- a/packages/openapi-ts/src/utils/__tests__/sort.spec.ts +++ b/packages/openapi-ts/src/utils/__tests__/sort.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import type { Model, Service } from '../../openApi'; +import type { Model, Service } from '../../types/client'; import { sort, sortByName } from '../sort'; describe('sort', () => { diff --git a/packages/openapi-ts/src/utils/postprocess.ts b/packages/openapi-ts/src/utils/postprocess.ts index daaf76f10..e3de6eaa6 100644 --- a/packages/openapi-ts/src/utils/postprocess.ts +++ b/packages/openapi-ts/src/utils/postprocess.ts @@ -1,5 +1,8 @@ -import type { Model, Operation, Service } from '../openApi'; -import type { Client } from '../types/client'; +import type { Client as ParserClient, Model } from '../openApi'; +import { sanitizeNamespaceIdentifier } from '../openApi/common/parser/sanitize'; +import type { Client, Operation, Service } from '../types/client'; +import { camelCase } from './camelCase'; +import { getConfig } from './config'; import { sort } from './sort'; import { unique } from './unique'; @@ -7,11 +10,11 @@ import { unique } from './unique'; * Post process client * @param client Client object with all the models, services, etc. */ -export function postProcessClient(client: Client): Client { +export function postProcessClient(client: ParserClient): Client { return { ...client, models: client.models.map((model) => postProcessModel(model)), - services: client.services.map((service) => postProcessService(service)), + services: postProcessOperations(client.operations).map(postProcessService), types: {}, }; } @@ -34,6 +37,35 @@ const postProcessModel = (model: Model): Model => ({ .sort(sort), }); +const postProcessOperations = ( + operations: ParserClient['operations'], +): Client['services'] => { + const config = getConfig(); + + const services = new Map(); + + operations.forEach((parserOperation) => { + const tags = + parserOperation.tags?.length && (config.services.asClass || config.name) + ? parserOperation.tags.filter(unique) + : ['Default']; + tags.forEach((tag) => { + const operation: Operation = { + ...parserOperation, + service: getServiceName(tag), + }; + const service = + services.get(operation.service) || getNewService(operation); + service.$refs = [...service.$refs, ...operation.$refs]; + service.imports = [...service.imports, ...operation.imports]; + service.operations = [...service.operations, operation]; + services.set(operation.service, service); + }); + }); + + return Array.from(services.values()); +}; + const postProcessService = (service: Service): Service => { const clone = { ...service }; clone.operations = postProcessServiceOperations(clone); @@ -71,3 +103,20 @@ const postProcessServiceOperations = (service: Service): Operation[] => { return clone; }); }; + +export const getNewService = (operation: Operation): Service => ({ + $refs: [], + imports: [], + name: operation.service, + operations: [], +}); + +/** + * Convert the input value to a correct service name. This converts + * the input string to PascalCase. + */ +export const getServiceName = (value: string): string => + camelCase({ + input: sanitizeNamespaceIdentifier(value), + pascalCase: true, + }); -- 2.51.2 From d8bb295b15b3661a9c884736258b13411b4bee5e Mon Sep 17 00:00:00 2001 From: Jacob Cohen Date: Fri, 6 Sep 2024 17:38:21 -0400 Subject: [PATCH 3/3] refactor: elimitate deep dependencies into parser --- .../openapi-ts/src/generate/__tests__/schemas.spec.ts | 4 ++-- packages/openapi-ts/src/generate/schemas.ts | 6 ++---- packages/openapi-ts/src/generate/services.ts | 2 +- packages/openapi-ts/src/generate/transformers.ts | 5 +---- packages/openapi-ts/src/generate/types.ts | 5 ++--- packages/openapi-ts/src/openApi/index.ts | 11 +++++++++++ .../src/plugins/@tanstack/query-core/plugin.ts | 2 +- packages/openapi-ts/src/types/client.ts | 4 +++- packages/openapi-ts/src/types/config.ts | 3 +-- packages/openapi-ts/src/utils/enum.ts | 2 +- packages/openapi-ts/src/utils/meta.ts | 2 +- packages/openapi-ts/src/utils/parse.ts | 7 ++----- packages/openapi-ts/src/utils/postprocess.ts | 2 +- packages/openapi-ts/src/utils/transform.ts | 2 +- packages/openapi-ts/src/utils/type.ts | 2 +- 15 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts b/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts index 6792cf83c..31fcc0111 100644 --- a/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import type { OpenApiSchema } from '../../openApi/v3/interfaces/OpenApiSchema'; +import type { OpenApiV3Schema } from '../../openApi'; import type { Files } from '../../types/utils'; import { setConfig } from '../../utils/config'; import { generateSchemas } from '../schemas'; @@ -89,7 +89,7 @@ describe('generateSchemas', () => { useOptions: true, }); - const schema: OpenApiSchema = { + const schema: OpenApiV3Schema = { type: 'object', }; diff --git a/packages/openapi-ts/src/generate/schemas.ts b/packages/openapi-ts/src/generate/schemas.ts index 3a21e7ea7..f15900f95 100644 --- a/packages/openapi-ts/src/generate/schemas.ts +++ b/packages/openapi-ts/src/generate/schemas.ts @@ -1,8 +1,6 @@ import { compiler, TypeScriptFile } from '../compiler'; -import type { OpenApi } from '../openApi'; -import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi/common/parser/sanitize'; -import type { OpenApiSchema as OpenApiV2Schema } from '../openApi/v2/interfaces/OpenApiSchema'; -import type { OpenApiSchema as OpenApiV3Schema } from '../openApi/v3/interfaces/OpenApiSchema'; +import type { OpenApi, OpenApiV2Schema, OpenApiV3Schema } from '../openApi'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi'; import type { Files } from '../types/utils'; import { getConfig } from '../utils/config'; diff --git a/packages/openapi-ts/src/generate/services.ts b/packages/openapi-ts/src/generate/services.ts index cada63da6..e0ba95254 100644 --- a/packages/openapi-ts/src/generate/services.ts +++ b/packages/openapi-ts/src/generate/services.ts @@ -7,7 +7,7 @@ import { TypeScriptFile, } from '../compiler'; import type { FunctionTypeParameter, ObjectValue } from '../compiler/types'; -import { isOperationParameterRequired } from '../openApi/common/parser/operation'; +import { isOperationParameterRequired } from '../openApi'; import type { Client, Model, diff --git a/packages/openapi-ts/src/generate/transformers.ts b/packages/openapi-ts/src/generate/transformers.ts index e7e670396..1bff36fc3 100644 --- a/packages/openapi-ts/src/generate/transformers.ts +++ b/packages/openapi-ts/src/generate/transformers.ts @@ -1,10 +1,7 @@ import ts from 'typescript'; import { compiler } from '../compiler'; -import type { - ModelMeta, - OperationResponse, -} from '../openApi/common/interfaces/client'; +import type { ModelMeta, OperationResponse } from '../types/client'; import { getConfig } from '../utils/config'; import { unsetUniqueTypeName } from '../utils/type'; import { diff --git a/packages/openapi-ts/src/generate/types.ts b/packages/openapi-ts/src/generate/types.ts index 7eeec2701..c69aab42a 100644 --- a/packages/openapi-ts/src/generate/types.ts +++ b/packages/openapi-ts/src/generate/types.ts @@ -6,9 +6,8 @@ import { type Node, TypeScriptFile, } from '../compiler'; -import type { Model, OperationParameter } from '../openApi'; -import type { Method } from '../openApi/common/interfaces/client'; -import { isOperationParameterRequired } from '../openApi/common/parser/operation'; +import { isOperationParameterRequired } from '../openApi'; +import type { Method, Model, OperationParameter } from '../types/client'; import type { Client } from '../types/client'; import type { Files } from '../types/utils'; import { getConfig, isStandaloneClient } from '../utils/config'; diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts index 7a9a4dfb9..5f9ec857b 100644 --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -10,11 +10,22 @@ export type { Enum, Method, Model, + ModelMeta, Operation, OperationParameter, + OperationResponse, } from './common/interfaces/client'; export type { Config } from './common/interfaces/config'; export type { OpenApi } from './common/interfaces/OpenApi'; +export { isOperationParameterRequired } from './common/parser/operation'; +export { + ensureValidTypeScriptJavaScriptIdentifier, + sanitizeNamespaceIdentifier, + sanitizeOperationParameterName, +} from './common/parser/sanitize'; +export { getType } from './common/parser/type'; +export type { OpenApiSchema as OpenApiV2Schema } from './v2/interfaces/OpenApiSchema'; +export type { OpenApiSchema as OpenApiV3Schema } from './v3/interfaces/OpenApiSchema'; /** * Parse the OpenAPI specification to a Client model that contains diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts index 4e7a851b5..4231987a1 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -15,7 +15,7 @@ import { operationResponseTypeName, toOperationName, } from '../../../generate/services'; -import { isOperationParameterRequired } from '../../../openApi/common/parser/operation'; +import { isOperationParameterRequired } from '../../../openApi'; import type { Client } from '../../../types/client'; import type { Method, diff --git a/packages/openapi-ts/src/types/client.ts b/packages/openapi-ts/src/types/client.ts index 3728d13e2..0d64908c7 100644 --- a/packages/openapi-ts/src/types/client.ts +++ b/packages/openapi-ts/src/types/client.ts @@ -2,11 +2,13 @@ import type { Client as ParserClient, Method, Model, + ModelMeta, Operation as ParserOperation, OperationParameter, + OperationResponse, } from '../openApi'; -export type { Method, Model, OperationParameter }; +export type { Method, Model, ModelMeta, OperationParameter, OperationResponse }; export interface Operation extends Omit { service: string; diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 9fde1247a..9ed4919fd 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -1,5 +1,4 @@ -import type { OpenApiSchema as OpenApiV2Schema } from '../openApi/v2/interfaces/OpenApiSchema'; -import type { OpenApiSchema as OpenApiV3Schema } from '../openApi/v3/interfaces/OpenApiSchema'; +import type { OpenApiV2Schema, OpenApiV3Schema } from '../openApi'; import type { Plugins } from '../plugins/'; import type { Operation } from '../types/client'; import type { ExtractArrayOfObjects } from './utils'; diff --git a/packages/openapi-ts/src/utils/enum.ts b/packages/openapi-ts/src/utils/enum.ts index 6a8cabcfa..5d863b033 100644 --- a/packages/openapi-ts/src/utils/enum.ts +++ b/packages/openapi-ts/src/utils/enum.ts @@ -1,5 +1,5 @@ import type { Enum, Model } from '../openApi'; -import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi/common/parser/sanitize'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi'; import { unescapeName } from './escape'; import { sort } from './sort'; import { unique } from './unique'; diff --git a/packages/openapi-ts/src/utils/meta.ts b/packages/openapi-ts/src/utils/meta.ts index 8fc2d7495..2617a0490 100644 --- a/packages/openapi-ts/src/utils/meta.ts +++ b/packages/openapi-ts/src/utils/meta.ts @@ -1,4 +1,4 @@ -import { getType } from '../openApi/common/parser/type'; +import { getType } from '../openApi'; import { refParametersPartial, refSchemasPartial } from './const'; import { reservedWordsRegExp } from './reservedWords'; import { cleanAndTransformTypeName } from './transform'; diff --git a/packages/openapi-ts/src/utils/parse.ts b/packages/openapi-ts/src/utils/parse.ts index 2fc624861..c480fbffd 100644 --- a/packages/openapi-ts/src/utils/parse.ts +++ b/packages/openapi-ts/src/utils/parse.ts @@ -1,8 +1,5 @@ -import type { - Operation, - OperationParameter, -} from '../openApi/common/interfaces/client'; -import { sanitizeNamespaceIdentifier } from '../openApi/common/parser/sanitize'; +import type { Operation, OperationParameter } from '../openApi'; +import { sanitizeNamespaceIdentifier } from '../openApi'; import { camelCase } from './camelCase'; import { getConfig, isStandaloneClient } from './config'; import { transformTypeKeyName } from './type'; diff --git a/packages/openapi-ts/src/utils/postprocess.ts b/packages/openapi-ts/src/utils/postprocess.ts index e3de6eaa6..a567244c1 100644 --- a/packages/openapi-ts/src/utils/postprocess.ts +++ b/packages/openapi-ts/src/utils/postprocess.ts @@ -1,5 +1,5 @@ import type { Client as ParserClient, Model } from '../openApi'; -import { sanitizeNamespaceIdentifier } from '../openApi/common/parser/sanitize'; +import { sanitizeNamespaceIdentifier } from '../openApi'; import type { Client, Operation, Service } from '../types/client'; import { camelCase } from './camelCase'; import { getConfig } from './config'; diff --git a/packages/openapi-ts/src/utils/transform.ts b/packages/openapi-ts/src/utils/transform.ts index e1152da3c..a1ddd35dd 100644 --- a/packages/openapi-ts/src/utils/transform.ts +++ b/packages/openapi-ts/src/utils/transform.ts @@ -1,4 +1,4 @@ -import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi/common/parser/sanitize'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi'; import { camelCase } from './camelCase'; import { getConfig } from './config'; import { reservedWordsRegExp } from './reservedWords'; diff --git a/packages/openapi-ts/src/utils/type.ts b/packages/openapi-ts/src/utils/type.ts index af1c52986..95e526260 100644 --- a/packages/openapi-ts/src/utils/type.ts +++ b/packages/openapi-ts/src/utils/type.ts @@ -1,6 +1,6 @@ import { compiler, type Property, type TypeNode } from '../compiler'; import type { Model } from '../openApi'; -import { sanitizeOperationParameterName } from '../openApi/common/parser/sanitize'; +import { sanitizeOperationParameterName } from '../openApi'; import type { Client } from '../types/client'; import { camelCase } from './camelCase'; import { getConfig, isStandaloneClient } from './config';