diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d1fe98..833f82b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.20.0] - 2022-02-25 +### Fixed +- Support enums with single quotes in names +- Generating better names when `operationId` is not given (breaking change) + ## [0.19.0] - 2022-02-02 ### Added - Support for Angular client with `--name` option diff --git a/LICENSE b/LICENSE index 66740897..9811f4b0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Ferdi Koomen +Copyright (c) Ferdi Koomen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/openApi/v2/parser/escapeName.spec.ts b/src/openApi/v2/parser/escapeName.spec.ts index 8394451a..8e343d4f 100644 --- a/src/openApi/v2/parser/escapeName.spec.ts +++ b/src/openApi/v2/parser/escapeName.spec.ts @@ -2,7 +2,7 @@ import { escapeName } from './escapeName'; describe('escapeName', () => { it('should escape', () => { - expect(escapeName('')).toEqual(''); + expect(escapeName('')).toEqual("''"); expect(escapeName('fooBar')).toEqual('fooBar'); expect(escapeName('Foo Bar')).toEqual(`'Foo Bar'`); expect(escapeName('foo bar')).toEqual(`'foo bar'`); diff --git a/src/openApi/v2/parser/getOperation.ts b/src/openApi/v2/parser/getOperation.ts index 089ade17..9aa15746 100644 --- a/src/openApi/v2/parser/getOperation.ts +++ b/src/openApi/v2/parser/getOperation.ts @@ -20,7 +20,7 @@ export const getOperation = ( pathParams: OperationParameters ): Operation => { const serviceName = getServiceName(tag); - const operationName = getOperationName(op.operationId || `${method}`); + const operationName = getOperationName(url, method, op.operationId); // Create a new operation object for this method. const operation: Operation = { diff --git a/src/openApi/v2/parser/getOperationName.spec.ts b/src/openApi/v2/parser/getOperationName.spec.ts index 24332ab3..bdaecb8f 100644 --- a/src/openApi/v2/parser/getOperationName.spec.ts +++ b/src/openApi/v2/parser/getOperationName.spec.ts @@ -2,17 +2,26 @@ import { getOperationName } from './getOperationName'; describe('getOperationName', () => { it('should produce correct result', () => { - expect(getOperationName('')).toEqual(''); - expect(getOperationName('FooBar')).toEqual('fooBar'); - expect(getOperationName('Foo Bar')).toEqual('fooBar'); - expect(getOperationName('foo bar')).toEqual('fooBar'); - expect(getOperationName('foo-bar')).toEqual('fooBar'); - expect(getOperationName('foo_bar')).toEqual('fooBar'); - expect(getOperationName('foo.bar')).toEqual('fooBar'); - expect(getOperationName('@foo.bar')).toEqual('fooBar'); - expect(getOperationName('$foo.bar')).toEqual('fooBar'); - expect(getOperationName('_foo.bar')).toEqual('fooBar'); - expect(getOperationName('-foo.bar')).toEqual('fooBar'); - expect(getOperationName('123.foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'GetAllUsers')).toEqual('getAllUsers'); + expect(getOperationName('/api/v{api-version}/users', 'GET', undefined)).toEqual('getApiUsers'); + expect(getOperationName('/api/v{api-version}/users', 'POST', undefined)).toEqual('postApiUsers'); + expect(getOperationName('/api/v1/users', 'GET', 'GetAllUsers')).toEqual('getAllUsers'); + expect(getOperationName('/api/v1/users', 'GET', undefined)).toEqual('getApiV1Users'); + expect(getOperationName('/api/v1/users', 'POST', undefined)).toEqual('postApiV1Users'); + expect(getOperationName('/api/v1/users/{id}', 'GET', undefined)).toEqual('getApiV1Users'); + expect(getOperationName('/api/v1/users/{id}', 'POST', undefined)).toEqual('postApiV1Users'); + + expect(getOperationName('/api/v{api-version}/users', 'GET', 'fooBar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'FooBar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'Foo Bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo-bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo_bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '@foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '$foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '_foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '-foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '123.foo.bar')).toEqual('fooBar'); }); }); diff --git a/src/openApi/v2/parser/getOperationName.ts b/src/openApi/v2/parser/getOperationName.ts index 663a758d..124bf66b 100644 --- a/src/openApi/v2/parser/getOperationName.ts +++ b/src/openApi/v2/parser/getOperationName.ts @@ -2,13 +2,23 @@ import camelCase from 'camelcase'; /** * Convert the input value to a correct operation (method) classname. - * This converts the input string to camelCase, so the method name follows - * the most popular Javascript and Typescript writing style. + * This will use the operation ID - if available - and otherwise fallback + * on a generated name from the URL */ -export const getOperationName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim(); - return camelCase(clean); +export const getOperationName = (url: string, method: string, operationId?: string): string => { + if (operationId) { + return camelCase( + operationId + .replace(/^[^a-zA-Z]+/g, '') + .replace(/[^\w\-]+/g, '-') + .trim() + ); + } + + const urlWithoutPlaceholders = url + .replace(/[^/]*?{api-version}.*?\//g, '') + .replace(/{(.*?)}/g, '') + .replace(/\//g, '-'); + + return camelCase(`${method}-${urlWithoutPlaceholders}`); }; diff --git a/src/openApi/v3/parser/getEnum.ts b/src/openApi/v3/parser/getEnum.ts index c684b466..607022fa 100644 --- a/src/openApi/v3/parser/getEnum.ts +++ b/src/openApi/v3/parser/getEnum.ts @@ -23,7 +23,7 @@ export const getEnum = (values?: (string | number)[]): Enum[] => { .replace(/^(\d+)/g, '_$1') .replace(/([a-z])([A-Z]+)/g, '$1_$2') .toUpperCase(), - value: `'${value}'`, + value: `'${value.replace(/'/g, "\\'")}'`, type: 'string', description: null, }; diff --git a/src/openApi/v3/parser/getOperation.ts b/src/openApi/v3/parser/getOperation.ts index c8b3e316..aee4bd0c 100644 --- a/src/openApi/v3/parser/getOperation.ts +++ b/src/openApi/v3/parser/getOperation.ts @@ -23,7 +23,7 @@ export const getOperation = ( pathParams: OperationParameters ): Operation => { const serviceName = getServiceName(tag); - const operationName = getOperationName(op.operationId || `${method}`); + const operationName = getOperationName(url, method, op.operationId); // Create a new operation object for this method. const operation: Operation = { diff --git a/src/openApi/v3/parser/getOperationName.spec.ts b/src/openApi/v3/parser/getOperationName.spec.ts index 24332ab3..bdaecb8f 100644 --- a/src/openApi/v3/parser/getOperationName.spec.ts +++ b/src/openApi/v3/parser/getOperationName.spec.ts @@ -2,17 +2,26 @@ import { getOperationName } from './getOperationName'; describe('getOperationName', () => { it('should produce correct result', () => { - expect(getOperationName('')).toEqual(''); - expect(getOperationName('FooBar')).toEqual('fooBar'); - expect(getOperationName('Foo Bar')).toEqual('fooBar'); - expect(getOperationName('foo bar')).toEqual('fooBar'); - expect(getOperationName('foo-bar')).toEqual('fooBar'); - expect(getOperationName('foo_bar')).toEqual('fooBar'); - expect(getOperationName('foo.bar')).toEqual('fooBar'); - expect(getOperationName('@foo.bar')).toEqual('fooBar'); - expect(getOperationName('$foo.bar')).toEqual('fooBar'); - expect(getOperationName('_foo.bar')).toEqual('fooBar'); - expect(getOperationName('-foo.bar')).toEqual('fooBar'); - expect(getOperationName('123.foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'GetAllUsers')).toEqual('getAllUsers'); + expect(getOperationName('/api/v{api-version}/users', 'GET', undefined)).toEqual('getApiUsers'); + expect(getOperationName('/api/v{api-version}/users', 'POST', undefined)).toEqual('postApiUsers'); + expect(getOperationName('/api/v1/users', 'GET', 'GetAllUsers')).toEqual('getAllUsers'); + expect(getOperationName('/api/v1/users', 'GET', undefined)).toEqual('getApiV1Users'); + expect(getOperationName('/api/v1/users', 'POST', undefined)).toEqual('postApiV1Users'); + expect(getOperationName('/api/v1/users/{id}', 'GET', undefined)).toEqual('getApiV1Users'); + expect(getOperationName('/api/v1/users/{id}', 'POST', undefined)).toEqual('postApiV1Users'); + + expect(getOperationName('/api/v{api-version}/users', 'GET', 'fooBar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'FooBar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'Foo Bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo-bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo_bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', 'foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '@foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '$foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '_foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '-foo.bar')).toEqual('fooBar'); + expect(getOperationName('/api/v{api-version}/users', 'GET', '123.foo.bar')).toEqual('fooBar'); }); }); diff --git a/src/openApi/v3/parser/getOperationName.ts b/src/openApi/v3/parser/getOperationName.ts index 663a758d..124bf66b 100644 --- a/src/openApi/v3/parser/getOperationName.ts +++ b/src/openApi/v3/parser/getOperationName.ts @@ -2,13 +2,23 @@ import camelCase from 'camelcase'; /** * Convert the input value to a correct operation (method) classname. - * This converts the input string to camelCase, so the method name follows - * the most popular Javascript and Typescript writing style. + * This will use the operation ID - if available - and otherwise fallback + * on a generated name from the URL */ -export const getOperationName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim(); - return camelCase(clean); +export const getOperationName = (url: string, method: string, operationId?: string): string => { + if (operationId) { + return camelCase( + operationId + .replace(/^[^a-zA-Z]+/g, '') + .replace(/[^\w\-]+/g, '-') + .trim() + ); + } + + const urlWithoutPlaceholders = url + .replace(/[^/]*?{api-version}.*?\//g, '') + .replace(/{(.*?)}/g, '') + .replace(/\//g, '-'); + + return camelCase(`${method}-${urlWithoutPlaceholders}`); }; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index f294281b..e1061e7d 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -1,5 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`v2 should generate: ./test/generated/v2/Demo.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import { NgModule} from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; + +import { AngularHttpRequest } from './core/AngularHttpRequest'; +import { BaseHttpRequest } from './core/BaseHttpRequest'; +import type { OpenAPIConfig } from './core/OpenAPI'; +import { OpenAPI } from './core/OpenAPI'; + +import { CollectionFormatService } from './services/CollectionFormatService'; +import { ComplexService } from './services/ComplexService'; +import { DefaultService } from './services/DefaultService'; +import { DefaultsService } from './services/DefaultsService'; +import { DescriptionsService } from './services/DescriptionsService'; +import { DuplicateService } from './services/DuplicateService'; +import { ErrorService } from './services/ErrorService'; +import { HeaderService } from './services/HeaderService'; +import { MultipleTags1Service } from './services/MultipleTags1Service'; +import { MultipleTags2Service } from './services/MultipleTags2Service'; +import { MultipleTags3Service } from './services/MultipleTags3Service'; +import { NoContentService } from './services/NoContentService'; +import { ParametersService } from './services/ParametersService'; +import { ResponseService } from './services/ResponseService'; +import { SimpleService } from './services/SimpleService'; +import { TypesService } from './services/TypesService'; + +@NgModule({ + imports: [HttpClientModule], + providers: [ + { + provide: OpenAPI, + useValue: { + BASE: OpenAPI?.BASE ?? 'http://localhost:3000/base', + VERSION: OpenAPI?.VERSION ?? '1.0', + WITH_CREDENTIALS: OpenAPI?.WITH_CREDENTIALS ?? false, + CREDENTIALS: OpenAPI?.CREDENTIALS ?? 'include', + TOKEN: OpenAPI?.TOKEN, + USERNAME: OpenAPI?.USERNAME, + PASSWORD: OpenAPI?.PASSWORD, + HEADERS: OpenAPI?.HEADERS, + ENCODE_PATH: OpenAPI?.ENCODE_PATH, + } as OpenAPIConfig, + }, + { + provide: BaseHttpRequest, + useClass: AngularHttpRequest, + }, + CollectionFormatService, + ComplexService, + DefaultService, + DefaultsService, + DescriptionsService, + DuplicateService, + ErrorService, + HeaderService, + MultipleTags1Service, + MultipleTags2Service, + MultipleTags3Service, + NoContentService, + ParametersService, + ResponseService, + SimpleService, + TypesService, + ] +}) +export class Demo {} +" +`; + exports[`v2 should generate: ./test/generated/v2/core/ApiError.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -943,6 +1015,8 @@ export enum EnumWithStrings { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', + _SINGLE_QUOTE_ = ''Single Quote'', + _DOUBLE_QUOTES_ = '\\"Double Quotes\\"', }" `; @@ -2949,6 +3023,86 @@ export class TypesService { }" `; +exports[`v3 should generate: ./test/generated/v3/Demo.ts 1`] = ` +"/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import { NgModule} from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; + +import { AngularHttpRequest } from './core/AngularHttpRequest'; +import { BaseHttpRequest } from './core/BaseHttpRequest'; +import type { OpenAPIConfig } from './core/OpenAPI'; +import { OpenAPI } from './core/OpenAPI'; + +import { CollectionFormatService } from './services/CollectionFormatService'; +import { ComplexService } from './services/ComplexService'; +import { DefaultService } from './services/DefaultService'; +import { DefaultsService } from './services/DefaultsService'; +import { DescriptionsService } from './services/DescriptionsService'; +import { DuplicateService } from './services/DuplicateService'; +import { ErrorService } from './services/ErrorService'; +import { FormDataService } from './services/FormDataService'; +import { HeaderService } from './services/HeaderService'; +import { MultipartService } from './services/MultipartService'; +import { MultipleTags1Service } from './services/MultipleTags1Service'; +import { MultipleTags2Service } from './services/MultipleTags2Service'; +import { MultipleTags3Service } from './services/MultipleTags3Service'; +import { NoContentService } from './services/NoContentService'; +import { ParametersService } from './services/ParametersService'; +import { RequestBodyService } from './services/RequestBodyService'; +import { ResponseService } from './services/ResponseService'; +import { SimpleService } from './services/SimpleService'; +import { TypesService } from './services/TypesService'; +import { UploadService } from './services/UploadService'; + +@NgModule({ + imports: [HttpClientModule], + providers: [ + { + provide: OpenAPI, + useValue: { + BASE: OpenAPI?.BASE ?? 'http://localhost:3000/base', + VERSION: OpenAPI?.VERSION ?? '1.0', + WITH_CREDENTIALS: OpenAPI?.WITH_CREDENTIALS ?? false, + CREDENTIALS: OpenAPI?.CREDENTIALS ?? 'include', + TOKEN: OpenAPI?.TOKEN, + USERNAME: OpenAPI?.USERNAME, + PASSWORD: OpenAPI?.PASSWORD, + HEADERS: OpenAPI?.HEADERS, + ENCODE_PATH: OpenAPI?.ENCODE_PATH, + } as OpenAPIConfig, + }, + { + provide: BaseHttpRequest, + useClass: AngularHttpRequest, + }, + CollectionFormatService, + ComplexService, + DefaultService, + DefaultsService, + DescriptionsService, + DuplicateService, + ErrorService, + FormDataService, + HeaderService, + MultipartService, + MultipleTags1Service, + MultipleTags2Service, + MultipleTags3Service, + NoContentService, + ParametersService, + RequestBodyService, + ResponseService, + SimpleService, + TypesService, + UploadService, + ] +}) +export class Demo {} +" +`; + exports[`v3 should generate: ./test/generated/v3/core/ApiError.ts 1`] = ` "/* istanbul ignore file */ /* tslint:disable */ @@ -4089,6 +4243,8 @@ export enum EnumWithStrings { SUCCESS = 'Success', WARNING = 'Warning', ERROR = 'Error', + _SINGLE_QUOTE_ = '\\\\'Single Quote\\\\'', + _DOUBLE_QUOTES_ = '\\"Double Quotes\\"', }" `; @@ -6125,7 +6281,7 @@ export class FormDataService { * @param formData A reusable request body * @throws ApiError */ - public static post( + public static postApiFormData( parameter?: string, formData?: ModelWithString, ): CancelablePromise { @@ -6502,7 +6658,7 @@ export class RequestBodyService { * @param requestBody A reusable request body * @throws ApiError */ - public static post( + public static postApiRequestBody( parameter?: string, requestBody?: ModelWithString, ): CancelablePromise { diff --git a/test/index.js b/test/index.js index e17dddad..6d276c41 100644 --- a/test/index.js +++ b/test/index.js @@ -7,14 +7,14 @@ const generate = async (input, output) => { await OpenAPI.generate({ input, output, - httpClient: OpenAPI.HttpClient.ANGULAR, + httpClient: OpenAPI.HttpClient.FETCH, useOptions: true, useUnionTypes: false, exportCore: true, exportSchemas: true, exportModels: true, exportServices: true, - clientName: 'Demo', + // clientName: 'Demo', // indent: OpenAPI.Indent.SPACE_2, // postfix: 'Service', // request: './test/custom/request.ts', diff --git a/test/spec/v2.json b/test/spec/v2.json index bb833366..2cc00b36 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -963,7 +963,9 @@ "enum": [ "Success", "Warning", - "Error" + "Error", + "'Single Quote'", + "\"Double Quotes\"" ] }, "EnumWithNumbers": { diff --git a/test/spec/v3.json b/test/spec/v3.json index c336bda8..3def2941 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1537,7 +1537,9 @@ "enum": [ "Success", "Warning", - "Error" + "Error", + "'Single Quote'", + "\"Double Quotes\"" ] }, "EnumWithNumbers": {