From f083ffb79d961954495dad03a0b6c39e64c0c046 Mon Sep 17 00:00:00 2001 From: Ferdi Koomen Date: Thu, 11 Nov 2021 13:01:11 +0100 Subject: [PATCH] - Added multiple tags support --- README.md | 2 +- src/openApi/v2/parser/getOperation.ts | 10 +- .../v2/parser/getServiceClassName.spec.ts | 13 -- src/openApi/v2/parser/getServiceName.spec.ts | 13 ++ ...tServiceClassName.ts => getServiceName.ts} | 4 +- src/openApi/v2/parser/getServices.ts | 28 +-- src/openApi/v3/parser/getOperation.ts | 10 +- .../v3/parser/getServiceClassName.spec.ts | 13 -- src/openApi/v3/parser/getServiceName.spec.ts | 13 ++ ...tServiceClassName.ts => getServiceName.ts} | 4 +- src/openApi/v3/parser/getServices.ts | 24 ++- src/templates/core/OpenAPI.hbs | 3 +- test/__snapshots__/index.spec.js.snap | 200 +++++++++++++++++- test/spec/v2.json | 33 +++ test/spec/v3.json | 29 +++ 15 files changed, 329 insertions(+), 70 deletions(-) delete mode 100644 src/openApi/v2/parser/getServiceClassName.spec.ts create mode 100644 src/openApi/v2/parser/getServiceName.spec.ts rename src/openApi/v2/parser/{getServiceClassName.ts => getServiceName.ts} (64%) delete mode 100644 src/openApi/v3/parser/getServiceClassName.spec.ts create mode 100644 src/openApi/v3/parser/getServiceName.spec.ts rename src/openApi/v3/parser/{getServiceClassName.ts => getServiceName.ts} (64%) diff --git a/README.md b/README.md index 022573fd..471d6ec4 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ export const $MyModel = { format: 'date-time', }, }, -}; +} as const; ``` These runtime object are prefixed with a `$` character and expose all the interesting attributes of a model diff --git a/src/openApi/v2/parser/getOperation.ts b/src/openApi/v2/parser/getOperation.ts index 68aa30cf..f56a0d8c 100644 --- a/src/openApi/v2/parser/getOperation.ts +++ b/src/openApi/v2/parser/getOperation.ts @@ -10,25 +10,25 @@ import { getOperationPath } from './getOperationPath'; import { getOperationResponseHeader } from './getOperationResponseHeader'; import { getOperationResponses } from './getOperationResponses'; import { getOperationResults } from './getOperationResults'; -import { getServiceClassName } from './getServiceClassName'; +import { getServiceName } from './getServiceName'; import { sortByRequired } from './sortByRequired'; export function getOperation( openApi: OpenApi, url: string, method: string, + tag: string, op: OpenApiOperation, pathParams: OperationParameters ): Operation { - const serviceName = op.tags?.[0] || 'Service'; - const serviceClassName = getServiceClassName(serviceName); - const operationNameFallback = `${method}${serviceClassName}`; + const serviceName = getServiceName(tag); + const operationNameFallback = `${method}${serviceName}`; const operationName = getOperationName(op.operationId || operationNameFallback); const operationPath = getOperationPath(url); // Create a new operation object for this method. const operation: Operation = { - service: serviceClassName, + service: serviceName, name: operationName, summary: getComment(op.summary), description: getComment(op.description), diff --git a/src/openApi/v2/parser/getServiceClassName.spec.ts b/src/openApi/v2/parser/getServiceClassName.spec.ts deleted file mode 100644 index cacb0876..00000000 --- a/src/openApi/v2/parser/getServiceClassName.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getServiceClassName } from './getServiceClassName'; - -describe('getServiceClassName', () => { - it('should produce correct result', () => { - expect(getServiceClassName('')).toEqual(''); - expect(getServiceClassName('FooBar')).toEqual('FooBar'); - expect(getServiceClassName('Foo Bar')).toEqual('FooBar'); - expect(getServiceClassName('foo bar')).toEqual('FooBar'); - expect(getServiceClassName('@fooBar')).toEqual('FooBar'); - expect(getServiceClassName('$fooBar')).toEqual('FooBar'); - expect(getServiceClassName('123fooBar')).toEqual('FooBar'); - }); -}); diff --git a/src/openApi/v2/parser/getServiceName.spec.ts b/src/openApi/v2/parser/getServiceName.spec.ts new file mode 100644 index 00000000..77c420d3 --- /dev/null +++ b/src/openApi/v2/parser/getServiceName.spec.ts @@ -0,0 +1,13 @@ +import { getServiceName } from './getServiceName'; + +describe('getServiceName', () => { + it('should produce correct result', () => { + expect(getServiceName('')).toEqual(''); + expect(getServiceName('FooBar')).toEqual('FooBar'); + expect(getServiceName('Foo Bar')).toEqual('FooBar'); + expect(getServiceName('foo bar')).toEqual('FooBar'); + expect(getServiceName('@fooBar')).toEqual('FooBar'); + expect(getServiceName('$fooBar')).toEqual('FooBar'); + expect(getServiceName('123fooBar')).toEqual('FooBar'); + }); +}); diff --git a/src/openApi/v2/parser/getServiceClassName.ts b/src/openApi/v2/parser/getServiceName.ts similarity index 64% rename from src/openApi/v2/parser/getServiceClassName.ts rename to src/openApi/v2/parser/getServiceName.ts index 5e54c621..14a003b1 100644 --- a/src/openApi/v2/parser/getServiceClassName.ts +++ b/src/openApi/v2/parser/getServiceName.ts @@ -1,10 +1,10 @@ import camelCase from 'camelcase'; /** - * Convert the input value to a correct service classname. This converts + * Convert the input value to a correct service name. This converts * the input string to PascalCase. */ -export function getServiceClassName(value: string): string { +export function getServiceName(value: string): string { const clean = value .replace(/^[^a-zA-Z]+/g, '') .replace(/[^\w\-]+/g, '-') diff --git a/src/openApi/v2/parser/getServices.ts b/src/openApi/v2/parser/getServices.ts index dd9ef9db..486608fd 100644 --- a/src/openApi/v2/parser/getServices.ts +++ b/src/openApi/v2/parser/getServices.ts @@ -1,4 +1,5 @@ import type { Service } from '../../../client/interfaces/Service'; +import { unique } from '../../../utils/unique'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperation } from './getOperation'; import { getOperationParameters } from './getOperationParameters'; @@ -27,20 +28,23 @@ export function getServices(openApi: OpenApi): Service[] { case 'patch': // Each method contains an OpenAPI operation, we parse the operation const op = path[method]!; - const operation = getOperation(openApi, url, method, op, pathParams); + const tags = op.tags?.filter(unique) || ['Service']; + tags.forEach(tag => { + const operation = getOperation(openApi, url, method, tag, op, pathParams); - // If we have already declared a service, then we should fetch that and - // append the new method to it. Otherwise we should create a new service object. - const service: Service = services.get(operation.service) || { - name: operation.service, - operations: [], - imports: [], - }; + // If we have already declared a service, then we should fetch that and + // append the new method to it. Otherwise we should create a new service object. + const service: Service = services.get(operation.service) || { + name: operation.service, + operations: [], + imports: [], + }; - // Push the operation in the service - service.operations.push(operation); - service.imports.push(...operation.imports); - services.set(operation.service, service); + // Push the operation in the service + service.operations.push(operation); + service.imports.push(...operation.imports); + services.set(operation.service, service); + }); break; } } diff --git a/src/openApi/v3/parser/getOperation.ts b/src/openApi/v3/parser/getOperation.ts index ccdae984..67873e11 100644 --- a/src/openApi/v3/parser/getOperation.ts +++ b/src/openApi/v3/parser/getOperation.ts @@ -13,25 +13,25 @@ import { getOperationResponseHeader } from './getOperationResponseHeader'; import { getOperationResponses } from './getOperationResponses'; import { getOperationResults } from './getOperationResults'; import { getRef } from './getRef'; -import { getServiceClassName } from './getServiceClassName'; +import { getServiceName } from './getServiceName'; import { sortByRequired } from './sortByRequired'; export function getOperation( openApi: OpenApi, url: string, method: string, + tag: string, op: OpenApiOperation, pathParams: OperationParameters ): Operation { - const serviceName = op.tags?.[0] || 'Service'; - const serviceClassName = getServiceClassName(serviceName); - const operationNameFallback = `${method}${serviceClassName}`; + const serviceName = getServiceName(tag); + const operationNameFallback = `${method}${serviceName}`; const operationName = getOperationName(op.operationId || operationNameFallback); const operationPath = getOperationPath(url); // Create a new operation object for this method. const operation: Operation = { - service: serviceClassName, + service: serviceName, name: operationName, summary: getComment(op.summary), description: getComment(op.description), diff --git a/src/openApi/v3/parser/getServiceClassName.spec.ts b/src/openApi/v3/parser/getServiceClassName.spec.ts deleted file mode 100644 index cacb0876..00000000 --- a/src/openApi/v3/parser/getServiceClassName.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getServiceClassName } from './getServiceClassName'; - -describe('getServiceClassName', () => { - it('should produce correct result', () => { - expect(getServiceClassName('')).toEqual(''); - expect(getServiceClassName('FooBar')).toEqual('FooBar'); - expect(getServiceClassName('Foo Bar')).toEqual('FooBar'); - expect(getServiceClassName('foo bar')).toEqual('FooBar'); - expect(getServiceClassName('@fooBar')).toEqual('FooBar'); - expect(getServiceClassName('$fooBar')).toEqual('FooBar'); - expect(getServiceClassName('123fooBar')).toEqual('FooBar'); - }); -}); diff --git a/src/openApi/v3/parser/getServiceName.spec.ts b/src/openApi/v3/parser/getServiceName.spec.ts new file mode 100644 index 00000000..77c420d3 --- /dev/null +++ b/src/openApi/v3/parser/getServiceName.spec.ts @@ -0,0 +1,13 @@ +import { getServiceName } from './getServiceName'; + +describe('getServiceName', () => { + it('should produce correct result', () => { + expect(getServiceName('')).toEqual(''); + expect(getServiceName('FooBar')).toEqual('FooBar'); + expect(getServiceName('Foo Bar')).toEqual('FooBar'); + expect(getServiceName('foo bar')).toEqual('FooBar'); + expect(getServiceName('@fooBar')).toEqual('FooBar'); + expect(getServiceName('$fooBar')).toEqual('FooBar'); + expect(getServiceName('123fooBar')).toEqual('FooBar'); + }); +}); diff --git a/src/openApi/v3/parser/getServiceClassName.ts b/src/openApi/v3/parser/getServiceName.ts similarity index 64% rename from src/openApi/v3/parser/getServiceClassName.ts rename to src/openApi/v3/parser/getServiceName.ts index 5e54c621..14a003b1 100644 --- a/src/openApi/v3/parser/getServiceClassName.ts +++ b/src/openApi/v3/parser/getServiceName.ts @@ -1,10 +1,10 @@ import camelCase from 'camelcase'; /** - * Convert the input value to a correct service classname. This converts + * Convert the input value to a correct service name. This converts * the input string to PascalCase. */ -export function getServiceClassName(value: string): string { +export function getServiceName(value: string): string { const clean = value .replace(/^[^a-zA-Z]+/g, '') .replace(/[^\w\-]+/g, '-') diff --git a/src/openApi/v3/parser/getServices.ts b/src/openApi/v3/parser/getServices.ts index b4db472b..486608fd 100644 --- a/src/openApi/v3/parser/getServices.ts +++ b/src/openApi/v3/parser/getServices.ts @@ -1,4 +1,5 @@ import type { Service } from '../../../client/interfaces/Service'; +import { unique } from '../../../utils/unique'; import type { OpenApi } from '../interfaces/OpenApi'; import { getOperation } from './getOperation'; import { getOperationParameters } from './getOperationParameters'; @@ -27,22 +28,23 @@ export function getServices(openApi: OpenApi): Service[] { case 'patch': // Each method contains an OpenAPI operation, we parse the operation const op = path[method]!; - const operation = getOperation(openApi, url, method, op, pathParams); + const tags = op.tags?.filter(unique) || ['Service']; + tags.forEach(tag => { + const operation = getOperation(openApi, url, method, tag, op, pathParams); - // If we have already declared a service, then we should fetch that and - // append the new method to it. Otherwise we should create a new service object. - const service = - services.get(operation.service) || - ({ + // If we have already declared a service, then we should fetch that and + // append the new method to it. Otherwise we should create a new service object. + const service: Service = services.get(operation.service) || { name: operation.service, operations: [], imports: [], - } as Service); + }; - // Push the operation in the service - service.operations.push(operation); - service.imports.push(...operation.imports); - services.set(operation.service, service); + // Push the operation in the service + service.operations.push(operation); + service.imports.push(...operation.imports); + services.set(operation.service, service); + }); break; } } diff --git a/src/templates/core/OpenAPI.hbs b/src/templates/core/OpenAPI.hbs index 9721fc82..6abb0af4 100644 --- a/src/templates/core/OpenAPI.hbs +++ b/src/templates/core/OpenAPI.hbs @@ -4,13 +4,12 @@ import type { ApiRequestOptions } from './ApiRequestOptions'; type Resolver = (options: ApiRequestOptions) => Promise; type Headers = Record; -type CredentialModes = 'include' | 'omit' | 'same-origin'; type Config = { BASE: string; VERSION: string; WITH_CREDENTIALS: boolean; - CREDENTIALS: CredentialModes; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; TOKEN?: string | Resolver; USERNAME?: string | Resolver; PASSWORD?: string | Resolver; diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index e317c3d9..0bc3c58b 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -180,13 +180,12 @@ import type { ApiRequestOptions } from './ApiRequestOptions'; type Resolver = (options: ApiRequestOptions) => Promise; type Headers = Record; -type CredentialModes = 'include' | 'omit' | 'same-origin'; type Config = { BASE: string; VERSION: string; WITH_CREDENTIALS: boolean; - CREDENTIALS: CredentialModes; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; TOKEN?: string | Resolver; USERNAME?: string | Resolver; PASSWORD?: string | Resolver; @@ -573,6 +572,9 @@ export { ComplexService } from './services/ComplexService'; export { DefaultsService } from './services/DefaultsService'; export { DuplicateService } from './services/DuplicateService'; export { HeaderService } from './services/HeaderService'; +export { MultipleTags1Service } from './services/MultipleTags1Service'; +export { MultipleTags2Service } from './services/MultipleTags2Service'; +export { MultipleTags3Service } from './services/MultipleTags3Service'; export { NoContentService } from './services/NoContentService'; export { ParametersService } from './services/ParametersService'; export { ResponseService } from './services/ResponseService'; @@ -2206,6 +2208,100 @@ export class HeaderService { }" `; +exports[`v2 should generate: ./test/generated/v2/services/MultipleTags1Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags1Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyA(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/a\`, + }); + } + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + +exports[`v2 should generate: ./test/generated/v2/services/MultipleTags2Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags2Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyA(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/a\`, + }); + } + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + +exports[`v2 should generate: ./test/generated/v2/services/MultipleTags3Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags3Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + exports[`v2 should generate: ./test/generated/v2/services/NoContentService.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -2694,13 +2790,12 @@ import type { ApiRequestOptions } from './ApiRequestOptions'; type Resolver = (options: ApiRequestOptions) => Promise; type Headers = Record; -type CredentialModes = 'include' | 'omit' | 'same-origin'; type Config = { BASE: string; VERSION: string; WITH_CREDENTIALS: boolean; - CREDENTIALS: CredentialModes; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; TOKEN?: string | Resolver; USERNAME?: string | Resolver; PASSWORD?: string | Resolver; @@ -3105,6 +3200,9 @@ export { DuplicateService } from './services/DuplicateService'; export { FormDataService } from './services/FormDataService'; export { HeaderService } from './services/HeaderService'; export { MultipartService } from './services/MultipartService'; +export { MultipleTags1Service } from './services/MultipleTags1Service'; +export { MultipleTags2Service } from './services/MultipleTags2Service'; +export { MultipleTags3Service } from './services/MultipleTags3Service'; export { NoContentService } from './services/NoContentService'; export { ParametersService } from './services/ParametersService'; export { RequestBodyService } from './services/RequestBodyService'; @@ -5241,6 +5339,100 @@ export class MultipartService { }" `; +exports[`v3 should generate: ./test/generated/v3/services/MultipleTags1Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags1Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyA(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/a\`, + }); + } + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + +exports[`v3 should generate: ./test/generated/v3/services/MultipleTags2Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags2Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyA(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/a\`, + }); + } + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + +exports[`v3 should generate: ./test/generated/v3/services/MultipleTags3Service.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { request as __request } from '../core/request'; +import { OpenAPI } from '../core/OpenAPI'; + +export class MultipleTags3Service { + + /** + * @returns void + * @throws ApiError + */ + public static dummyB(): CancelablePromise { + return __request({ + method: 'GET', + path: \`/api/v\${OpenAPI.VERSION}/multiple-tags/b\`, + }); + } + +}" +`; + exports[`v3 should generate: ./test/generated/v3/services/NoContentService.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ diff --git a/test/spec/v2.json b/test/spec/v2.json index 433edc94..ca4e26bc 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -347,24 +347,28 @@ "/api/v{api-version}/duplicate": { "get": { "tags": [ + "Duplicate", "Duplicate" ], "operationId": "DuplicateName" }, "post": { "tags": [ + "Duplicate", "Duplicate" ], "operationId": "DuplicateName" }, "put": { "tags": [ + "Duplicate", "Duplicate" ], "operationId": "DuplicateName" }, "delete": { "tags": [ + "Duplicate", "Duplicate" ], "operationId": "DuplicateName" @@ -383,6 +387,35 @@ } } }, + "/api/v{api-version}/multiple-tags/a": { + "get": { + "tags": [ + "MultipleTags1", + "MultipleTags2" + ], + "operationId": "DummyA", + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v{api-version}/multiple-tags/b": { + "get": { + "tags": [ + "MultipleTags1", + "MultipleTags2", + "MultipleTags3" + ], + "operationId": "DummyB", + "responses": { + "204": { + "description": "Success" + } + } + } + }, "/api/v{api-version}/response": { "get": { "tags": [ diff --git a/test/spec/v3.json b/test/spec/v3.json index 903c1222..9e3b25eb 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -572,6 +572,35 @@ } } }, + "/api/v{api-version}/multiple-tags/a": { + "get": { + "tags": [ + "MultipleTags1", + "MultipleTags2" + ], + "operationId": "DummyA", + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v{api-version}/multiple-tags/b": { + "get": { + "tags": [ + "MultipleTags1", + "MultipleTags2", + "MultipleTags3" + ], + "operationId": "DummyB", + "responses": { + "204": { + "description": "Success" + } + } + } + }, "/api/v{api-version}/response": { "get": { "tags": [