diff --git a/src/openApi/v2/parser/getOperationName.ts b/src/openApi/v2/parser/getOperationName.ts index 124bf66bd..4734bfbc9 100644 --- a/src/openApi/v2/parser/getOperationName.ts +++ b/src/openApi/v2/parser/getOperationName.ts @@ -1,5 +1,7 @@ import camelCase from 'camelcase'; +import sanitizeOperationName from '../../../utils/sanitizeOperationName'; + /** * Convert the input value to a correct operation (method) classname. * This will use the operation ID - if available - and otherwise fallback @@ -7,12 +9,7 @@ import camelCase from 'camelcase'; */ export const getOperationName = (url: string, method: string, operationId?: string): string => { if (operationId) { - return camelCase( - operationId - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim() - ); + return camelCase(sanitizeOperationName(operationId).trim()); } const urlWithoutPlaceholders = url diff --git a/src/openApi/v2/parser/getOperationParameterName.ts b/src/openApi/v2/parser/getOperationParameterName.ts index 3a7fb408b..fc7f21594 100644 --- a/src/openApi/v2/parser/getOperationParameterName.ts +++ b/src/openApi/v2/parser/getOperationParameterName.ts @@ -1,15 +1,13 @@ import camelCase from 'camelcase'; import { reservedWords } from '../../../utils/reservedWords'; +import sanitizeOperationParameterName from '../../../utils/sanitizeOperationParameterName'; /** * Replaces any invalid characters from a parameter name. * For example: 'filter.someProperty' becomes 'filterSomeProperty'. */ export const getOperationParameterName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim(); + const clean = sanitizeOperationParameterName(value).trim(); return camelCase(clean).replace(reservedWords, '_$1'); }; diff --git a/src/openApi/v2/parser/getServiceName.ts b/src/openApi/v2/parser/getServiceName.ts index b5b1718f2..2dfff844e 100644 --- a/src/openApi/v2/parser/getServiceName.ts +++ b/src/openApi/v2/parser/getServiceName.ts @@ -1,13 +1,12 @@ import camelCase from 'camelcase'; +import sanitizeServiceName from '../../../utils/sanitizeServiceName'; + /** * Convert the input value to a correct service name. This converts * the input string to PascalCase. */ export const getServiceName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim(); + const clean = sanitizeServiceName(value).trim(); return camelCase(clean, { pascalCase: true }); }; diff --git a/src/openApi/v3/parser/getOperationName.ts b/src/openApi/v3/parser/getOperationName.ts index 124bf66bd..4734bfbc9 100644 --- a/src/openApi/v3/parser/getOperationName.ts +++ b/src/openApi/v3/parser/getOperationName.ts @@ -1,5 +1,7 @@ import camelCase from 'camelcase'; +import sanitizeOperationName from '../../../utils/sanitizeOperationName'; + /** * Convert the input value to a correct operation (method) classname. * This will use the operation ID - if available - and otherwise fallback @@ -7,12 +9,7 @@ import camelCase from 'camelcase'; */ export const getOperationName = (url: string, method: string, operationId?: string): string => { if (operationId) { - return camelCase( - operationId - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim() - ); + return camelCase(sanitizeOperationName(operationId).trim()); } const urlWithoutPlaceholders = url diff --git a/src/openApi/v3/parser/getOperationParameterName.ts b/src/openApi/v3/parser/getOperationParameterName.ts index a3caa291c..fc7f21594 100644 --- a/src/openApi/v3/parser/getOperationParameterName.ts +++ b/src/openApi/v3/parser/getOperationParameterName.ts @@ -1,16 +1,13 @@ import camelCase from 'camelcase'; import { reservedWords } from '../../../utils/reservedWords'; +import sanitizeOperationParameterName from '../../../utils/sanitizeOperationParameterName'; /** * Replaces any invalid characters from a parameter name. * For example: 'filter.someProperty' becomes 'filterSomeProperty'. */ export const getOperationParameterName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace('[]', 'Array') - .replace(/[^\w\-]+/g, '-') - .trim(); + const clean = sanitizeOperationParameterName(value).trim(); return camelCase(clean).replace(reservedWords, '_$1'); }; diff --git a/src/openApi/v3/parser/getServiceName.spec.ts b/src/openApi/v3/parser/getServiceName.spec.ts index 77c420d3b..c1de86020 100644 --- a/src/openApi/v3/parser/getServiceName.spec.ts +++ b/src/openApi/v3/parser/getServiceName.spec.ts @@ -9,5 +9,6 @@ describe('getServiceName', () => { expect(getServiceName('@fooBar')).toEqual('FooBar'); expect(getServiceName('$fooBar')).toEqual('FooBar'); expect(getServiceName('123fooBar')).toEqual('FooBar'); + expect(getServiceName('non-ascii-æøåÆØÅöôêÊ字符串')).toEqual('NonAsciiÆøåÆøÅöôêÊ字符串'); }); }); diff --git a/src/openApi/v3/parser/getServiceName.ts b/src/openApi/v3/parser/getServiceName.ts index b5b1718f2..2dfff844e 100644 --- a/src/openApi/v3/parser/getServiceName.ts +++ b/src/openApi/v3/parser/getServiceName.ts @@ -1,13 +1,12 @@ import camelCase from 'camelcase'; +import sanitizeServiceName from '../../../utils/sanitizeServiceName'; + /** * Convert the input value to a correct service name. This converts * the input string to PascalCase. */ export const getServiceName = (value: string): string => { - const clean = value - .replace(/^[^a-zA-Z]+/g, '') - .replace(/[^\w\-]+/g, '-') - .trim(); + const clean = sanitizeServiceName(value).trim(); return camelCase(clean, { pascalCase: true }); }; diff --git a/src/utils/sanitizeOperationName.ts b/src/utils/sanitizeOperationName.ts new file mode 100644 index 000000000..512b0d5a4 --- /dev/null +++ b/src/utils/sanitizeOperationName.ts @@ -0,0 +1,7 @@ +import sanitizeServiceName from './sanitizeServiceName'; + +/** + * sanitizeOperationName does the same as sanitizeServiceName. + */ +const sanitizeOperationName = sanitizeServiceName; +export default sanitizeOperationName; diff --git a/src/utils/sanitizeOperationParameterName.ts b/src/utils/sanitizeOperationParameterName.ts new file mode 100644 index 000000000..8c157ecf9 --- /dev/null +++ b/src/utils/sanitizeOperationParameterName.ts @@ -0,0 +1,7 @@ +import sanitizeOperationName from './sanitizeOperationName'; + +const sanitizeOperationParameterName = (name: string): string => { + const withoutBrackets = name.replace('[]', 'Array'); + return sanitizeOperationName(withoutBrackets); +}; +export default sanitizeOperationParameterName; diff --git a/src/utils/sanitizeServiceName.ts b/src/utils/sanitizeServiceName.ts new file mode 100644 index 000000000..92c7e116b --- /dev/null +++ b/src/utils/sanitizeServiceName.ts @@ -0,0 +1,18 @@ +/** + * Sanitizes service names, so they are valid typescript identifiers of a certain form. + * + * 1: Remove any leading characters that are illegal as starting character of a typescript identifier. + * 2: Replace illegal characters in remaining part of type name with underscore (-). + * + * Step 1 should perhaps instead also replace illegal characters with underscore, or prefix with it, like sanitizeEnumName + * does. The way this is now one could perhaps end up removing all characters, if all are illegal start characters. It + * would be sort of a breaking change to do so, though, previously generated code might change then. + * + * Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers + * + * The output of this is expected to be converted to PascalCase + */ +const sanitizeServiceName = (name: string) => + name.replace(/^[^\p{ID_Start}]+/u, '').replace(/[^$\u200c\u200d\p{ID_Continue}]/gu, '-'); + +export default sanitizeServiceName; diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 919d7d707..faff498eb 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -684,6 +684,7 @@ export { MultipleTags1Service } from './services/MultipleTags1Service'; export { MultipleTags2Service } from './services/MultipleTags2Service'; export { MultipleTags3Service } from './services/MultipleTags3Service'; export { NoContentService } from './services/NoContentService'; +export { NonAsciiÆøåÆøÅöôêÊService } from './services/NonAsciiÆøåÆøÅöôêÊService'; export { ParametersService } from './services/ParametersService'; export { ResponseService } from './services/ResponseService'; export { SimpleService } from './services/SimpleService'; @@ -2852,6 +2853,36 @@ export class NoContentService { " `; +exports[`v2 should generate: test/generated/v2/services/NonAsciiÆøåÆøÅöôêÊService.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { NonAsciiStringæøåÆØÅöôêÊ字符串 } from '../models/NonAsciiStringæøåÆØÅöôêÊ字符串'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class NonAsciiÆøåÆøÅöôêÊService { + /** + * @param nonAsciiParamæøåÆøÅöôêÊ Dummy input param + * @returns NonAsciiStringæøåÆØÅöôêÊ字符串 Successful response + * @throws ApiError + */ + public static nonAsciiæøåÆøÅöôêÊ字符串( + nonAsciiParamæøåÆøÅöôêÊ: number, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串', + query: { + 'nonAsciiParamæøåÆØÅöôêÊ': nonAsciiParamæøåÆøÅöôêÊ, + }, + }); + } +} +" +`; + exports[`v2 should generate: test/generated/v2/services/ParametersService.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ @@ -3894,6 +3925,7 @@ export { MultipleTags1Service } from './services/MultipleTags1Service'; export { MultipleTags2Service } from './services/MultipleTags2Service'; export { MultipleTags3Service } from './services/MultipleTags3Service'; export { NoContentService } from './services/NoContentService'; +export { NonAsciiÆøåÆøÅöôêÊService } from './services/NonAsciiÆøåÆøÅöôêÊService'; export { ParametersService } from './services/ParametersService'; export { RequestBodyService } from './services/RequestBodyService'; export { ResponseService } from './services/ResponseService'; @@ -7487,6 +7519,36 @@ export class NoContentService { " `; +exports[`v3 should generate: test/generated/v3/services/NonAsciiÆøåÆøÅöôêÊService.ts 1`] = ` +"/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { NonAsciiStringæøåÆØÅöôêÊ字符串 } from '../models/NonAsciiStringæøåÆØÅöôêÊ字符串'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class NonAsciiÆøåÆøÅöôêÊService { + /** + * @param nonAsciiParamæøåÆøÅöôêÊ Dummy input param + * @returns NonAsciiStringæøåÆØÅöôêÊ字符串 Successful response + * @throws ApiError + */ + public static nonAsciiæøåÆøÅöôêÊ字符串( + nonAsciiParamæøåÆøÅöôêÊ: number, + ): CancelablePromise> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串', + query: { + 'nonAsciiParamæøåÆØÅöôêÊ': nonAsciiParamæøåÆøÅöôêÊ, + }, + }); + } +} +" +`; + exports[`v3 should generate: test/generated/v3/services/ParametersService.ts 1`] = ` "/* generated using openapi-typescript-codegen -- do no edit */ /* istanbul ignore file */ diff --git a/test/spec/v2.json b/test/spec/v2.json index af489ca99..ad95dfd77 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -929,6 +929,33 @@ } } } + }, + "/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串": { + "post": { + "tags": [ + "Non-Ascii-æøåÆØÅöôêÊ" + ], + "operationId": "nonAsciiæøåÆØÅöôêÊ字符串", + "parameters": [ + { + "description": "Dummy input param", + "name": "nonAsciiParamæøåÆØÅöôêÊ", + "in": "query", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "schema": { + "$ref": "#/definitions/NonAsciiStringæøåÆØÅöôêÊ字符串" + } + } + } + } } }, "definitions": { diff --git a/test/spec/v3.json b/test/spec/v3.json index 9008f534a..5149647c3 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1488,6 +1488,40 @@ } } } + }, + "/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串": { + "post": { + "tags": [ + "Non-Ascii-æøåÆØÅöôêÊ" + ], + "operationId": "nonAsciiæøåÆØÅöôêÊ字符串", + "parameters": [ + { + "description": "Dummy input param", + "name": "nonAsciiParamæøåÆØÅöôêÊ", + "in": "query", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NonAsciiStringæøåÆØÅöôêÊ字符串" + } + } + } + } + } + } + } } }, "components": {