From 75888319e686b73c863020d5fabff29d6fa8267f Mon Sep 17 00:00:00 2001 From: Jostein Stuhaug Date: Fri, 16 Feb 2024 10:13:44 +0100 Subject: [PATCH] Support non-ascii (unicode) characters in service name, operation name and parameter name. This replaces regexp patterns that only worked with ascii characters with more proper matching that supports unicode identifiers in typescript/javascript. The platform must support "unicode-aware mode" (the u flag) for this to work. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode --- src/openApi/v2/parser/getOperationName.ts | 9 +-- .../v2/parser/getOperationParameterName.ts | 6 +- src/openApi/v2/parser/getServiceName.ts | 7 +-- src/openApi/v3/parser/getOperationName.ts | 9 +-- .../v3/parser/getOperationParameterName.ts | 7 +-- src/openApi/v3/parser/getServiceName.spec.ts | 1 + src/openApi/v3/parser/getServiceName.ts | 7 +-- src/utils/sanitizeOperationName.ts | 7 +++ src/utils/sanitizeOperationParameterName.ts | 7 +++ src/utils/sanitizeServiceName.ts | 18 ++++++ test/__snapshots__/index.spec.ts.snap | 62 +++++++++++++++++++ test/spec/v2.json | 27 ++++++++ test/spec/v3.json | 34 ++++++++++ 13 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 src/utils/sanitizeOperationName.ts create mode 100644 src/utils/sanitizeOperationParameterName.ts create mode 100644 src/utils/sanitizeServiceName.ts 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 e0bf03c13..c17c8317b 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'; @@ -2841,6 +2842,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 */ @@ -3852,6 +3883,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'; @@ -7045,6 +7077,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 1f1728a06..eceaf5134 100644 --- a/test/spec/v2.json +++ b/test/spec/v2.json @@ -905,6 +905,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 b58b24559..9a3f429bc 100644 --- a/test/spec/v3.json +++ b/test/spec/v3.json @@ -1464,6 +1464,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": {