From 327c5fb629f0c7b9c727da87b6bc287b8e98bcd5 Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 17 Jul 2024 14:32:52 +0100 Subject: [PATCH] feat: allow filtering service endpoints with --- .changeset/tiny-owls-pull.md | 5 ++ docs/openapi-ts/output.md | 14 +++++ packages/openapi-ts/src/compiler/classes.ts | 19 +++--- .../src/generate/__tests__/services.spec.ts | 24 +++++++- packages/openapi-ts/src/generate/services.ts | 35 ++++++++--- packages/openapi-ts/src/types/config.ts | 8 +++ .../v3_services_filter/index.ts.snap | 2 + .../v3_services_filter/services.gen.ts.snap | 61 +++++++++++++++++++ packages/openapi-ts/test/index.spec.ts | 40 +++++++----- packages/openapi-ts/test/sample.cjs | 3 +- 10 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 .changeset/tiny-owls-pull.md create mode 100644 packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/index.ts.snap create mode 100644 packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/services.gen.ts.snap diff --git a/.changeset/tiny-owls-pull.md b/.changeset/tiny-owls-pull.md new file mode 100644 index 000000000..804801518 --- /dev/null +++ b/.changeset/tiny-owls-pull.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': minor +--- + +feat: allow filtering service endpoints with `services.filter` diff --git a/docs/openapi-ts/output.md b/docs/openapi-ts/output.md index a115a1548..1fcfbc4a8 100644 --- a/docs/openapi-ts/output.md +++ b/docs/openapi-ts/output.md @@ -127,6 +127,20 @@ export default { ::: +### Filtering Endpoints + +If you only want to include specific endpoints in the generated services, you can use the `services.filter` config option to filter endpoints. The provided value should be a regular expression to match against endpoints with the `{method} {path}` pattern. For example, the config below will only include all `/api/v1/foo` endpoints. + +```js{5} +export default { + input: 'path/to/openapi.json', + output: 'src/client', + services: { + filter: '^\\w+ /api/v1/foo$' + } +} +``` + ### Output Below are different outputs depending on your chosen style. No services approach will not generate the `services.gen.ts` file. diff --git a/packages/openapi-ts/src/compiler/classes.ts b/packages/openapi-ts/src/compiler/classes.ts index 754df592b..17ca42de3 100644 --- a/packages/openapi-ts/src/compiler/classes.ts +++ b/packages/openapi-ts/src/compiler/classes.ts @@ -74,9 +74,12 @@ export const createMethodDeclaration = ({ returnType?: string | ts.TypeNode; statements?: ts.Statement[]; }) => { - const modifiers = toAccessLevelModifiers(accessLevel); + let modifiers = toAccessLevelModifiers(accessLevel); if (isStatic) { - modifiers.push(ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)); + modifiers = [ + ...modifiers, + ts.factory.createModifier(ts.SyntaxKind.StaticKeyword), + ]; } const node = ts.factory.createMethodDeclaration( modifiers, @@ -115,11 +118,11 @@ export const createClassDeclaration = ({ members?: ts.ClassElement[]; name: string; }) => { - const modifiers: ts.ModifierLike[] = [ + let modifiers: ts.ModifierLike[] = [ ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), ]; if (decorator) { - modifiers.unshift( + modifiers = [ ts.factory.createDecorator( ts.factory.createCallExpression( ts.factory.createIdentifier(decorator.name), @@ -129,14 +132,14 @@ export const createClassDeclaration = ({ .filter(isType), ), ), - ); + ...modifiers, + ]; } // Add newline between each class member. - const m: ts.ClassElement[] = []; + let m: ts.ClassElement[] = []; members.forEach((member) => { - m.push(member); // @ts-ignore - m.push(ts.factory.createIdentifier('\n')); + m = [...m, member, ts.factory.createIdentifier('\n')]; }); return ts.factory.createClassDeclaration( modifiers, diff --git a/packages/openapi-ts/src/generate/__tests__/services.spec.ts b/packages/openapi-ts/src/generate/__tests__/services.spec.ts index f95b3f3ff..f95b8c7e4 100644 --- a/packages/openapi-ts/src/generate/__tests__/services.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/services.spec.ts @@ -39,7 +39,29 @@ describe('generateServices', () => { $refs: [], imports: [], name: 'User', - operations: [], + operations: [ + { + $refs: [], + deprecated: false, + description: null, + id: null, + imports: [], + method: 'GET', + name: '', + parameters: [], + parametersBody: null, + parametersCookie: [], + parametersForm: [], + parametersHeader: [], + parametersPath: [], + parametersQuery: [], + path: '/api/v1/foo', + responseHeader: null, + responses: [], + service: '', + summary: null, + }, + ], }, ], types: {}, diff --git a/packages/openapi-ts/src/generate/services.ts b/packages/openapi-ts/src/generate/services.ts index f478d6c54..ce8377882 100644 --- a/packages/openapi-ts/src/generate/services.ts +++ b/packages/openapi-ts/src/generate/services.ts @@ -485,7 +485,15 @@ const processService = ( const isStandalone = isStandaloneClient(config); - service.operations.forEach((operation) => { + let filteredOperations = service.operations; + if (config.services.filter) { + const regexp = new RegExp(config.services.filter); + filteredOperations = service.operations.filter((operation) => + regexp.test(`${operation.method} ${operation.path}`), + ); + } + + filteredOperations.forEach((operation) => { if (operation.parameters.length) { generateImport({ client, @@ -533,7 +541,7 @@ const processService = ( }); if (!config.services.asClass && !config.name) { - service.operations.forEach((operation) => { + filteredOperations.forEach((operation) => { const expression = compiler.types.function({ parameters: toOperationParamType(client, operation), returnType: isStandalone @@ -556,7 +564,7 @@ const processService = ( return; } - const members: ClassElement[] = service.operations.map((operation) => { + let members: ClassElement[] = filteredOperations.map((operation) => { const node = compiler.class.method({ accessLevel: 'public', comment: toOperationComment(operation), @@ -576,9 +584,13 @@ const processService = ( return node; }); + if (!members.length) { + return; + } + // Push to front constructor if needed if (config.name) { - members.unshift( + members = [ compiler.class.constructor({ multiLine: false, parameters: [ @@ -590,9 +602,10 @@ const processService = ( }, ], }), - ); + ...members, + ]; } else if (config.client === 'angular') { - members.unshift( + members = [ compiler.class.constructor({ multiLine: false, parameters: [ @@ -604,7 +617,8 @@ const processService = ( }, ], }), - ); + ...members, + ]; } const statement = compiler.class.create({ @@ -707,6 +721,11 @@ export const generateServices = async ({ asType: !name.endsWith('Transformer'), name, })); - files.services?.addImport(importedTypes, `./${files.types.getName(false)}`); + if (importedTypes.length) { + files.services?.addImport( + importedTypes, + `./${files.types.getName(false)}`, + ); + } } }; diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 93e8aa5e2..3bcc0eb99 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -123,6 +123,14 @@ export interface ClientConfig { * @default true */ export?: boolean; + /** + * Filter endpoints to be included in the generated services. + * The provided string should be a regular expression where matched + * results will be included in the output. The input pattern this + * string will be tested against is `{method} {path}`. For example, + * you can match `POST /api/v1/foo` with `^POST /api/v1/foo$`. + */ + filter?: string; /** * Include only service classes with names matching regular expression * diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/index.ts.snap new file mode 100644 index 000000000..c87f165ef --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/index.ts.snap @@ -0,0 +1,2 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './services.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/services.gen.ts.snap new file mode 100644 index 000000000..3118baeb3 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_services_filter/services.gen.ts.snap @@ -0,0 +1,61 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { CancelablePromise } from './core/CancelablePromise'; +import { OpenAPI } from './core/OpenAPI'; +import { request as __request } from './core/request'; + +/** + * @throws ApiError + */ +export const getCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'GET', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const putCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'PUT', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const postCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'POST', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const deleteCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const optionsCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'OPTIONS', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const headCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'HEAD', + url: '/api/v{api-version}/simple' +}); }; + +/** + * @throws ApiError + */ +export const patchCallWithoutParametersAndResponse = (): CancelablePromise => { return __request(OpenAPI, { + method: 'PATCH', + url: '/api/v{api-version}/simple' +}); }; \ No newline at end of file diff --git a/packages/openapi-ts/test/index.spec.ts b/packages/openapi-ts/test/index.spec.ts index f7ad18516..5399c83a6 100644 --- a/packages/openapi-ts/test/index.spec.ts +++ b/packages/openapi-ts/test/index.spec.ts @@ -217,20 +217,6 @@ describe('OpenAPI v3', () => { description: 'generate pascalcase types', name: 'v3_pascalcase', }, - { - config: createConfig({ - exportCore: false, - schemas: false, - services: { - asClass: true, - include: '^(Simple|Parameters)', - name: 'myAwesome{{name}}Api', - }, - types: false, - }), - description: 'generate services with custom name', - name: 'v3_services_name', - }, { config: createConfig({ exportCore: false, @@ -255,6 +241,32 @@ describe('OpenAPI v3', () => { description: 'generate form validation schemas', name: 'v3_schemas_form', }, + { + config: createConfig({ + exportCore: false, + schemas: false, + services: { + asClass: true, + include: '^(Simple|Parameters)', + name: 'myAwesome{{name}}Api', + }, + types: false, + }), + description: 'generate services with custom name', + name: 'v3_services_name', + }, + { + config: createConfig({ + exportCore: false, + schemas: false, + services: { + filter: '^\\w+ /api/v{api-version}/simple$', + }, + types: false, + }), + description: 'generate services with specific endpoints', + name: 'v3_services_filter', + }, { config: createConfig({ exportCore: false, diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index 68e896ff7..c169c6af8 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -21,7 +21,8 @@ const main = async () => { // export: false, }, services: { - // asClass: true, + asClass: true, + filter: '^\\w+ /api/v{api-version}/simple$', // export: false, // name: '^Parameters', },