Skip to content

Commit

Permalink
Support non-ascii (unicode) characters in service name, operation nam…
Browse files Browse the repository at this point in the history
…e 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
  • Loading branch information
josstn committed Feb 16, 2024
1 parent 308a180 commit 7588831
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 29 deletions.
9 changes: 3 additions & 6 deletions src/openApi/v2/parser/getOperationName.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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
* on a generated name from the URL
*/
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
Expand Down
6 changes: 2 additions & 4 deletions src/openApi/v2/parser/getOperationParameterName.ts
Original file line number Diff line number Diff line change
@@ -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');
};
7 changes: 3 additions & 4 deletions src/openApi/v2/parser/getServiceName.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
9 changes: 3 additions & 6 deletions src/openApi/v3/parser/getOperationName.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
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
* on a generated name from the URL
*/
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
Expand Down
7 changes: 2 additions & 5 deletions src/openApi/v3/parser/getOperationParameterName.ts
Original file line number Diff line number Diff line change
@@ -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');
};
1 change: 1 addition & 0 deletions src/openApi/v3/parser/getServiceName.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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ÆøåÆøÅöôêÊ字符串');
});
});
7 changes: 3 additions & 4 deletions src/openApi/v3/parser/getServiceName.ts
Original file line number Diff line number Diff line change
@@ -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 });
};
7 changes: 7 additions & 0 deletions src/utils/sanitizeOperationName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sanitizeServiceName from './sanitizeServiceName';

/**
* sanitizeOperationName does the same as sanitizeServiceName.
*/
const sanitizeOperationName = sanitizeServiceName;
export default sanitizeOperationName;
7 changes: 7 additions & 0 deletions src/utils/sanitizeOperationParameterName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sanitizeOperationName from './sanitizeOperationName';

const sanitizeOperationParameterName = (name: string): string => {
const withoutBrackets = name.replace('[]', 'Array');
return sanitizeOperationName(withoutBrackets);
};
export default sanitizeOperationParameterName;
18 changes: 18 additions & 0 deletions src/utils/sanitizeServiceName.ts
Original file line number Diff line number Diff line change
@@ -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;
62 changes: 62 additions & 0 deletions test/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<NonAsciiStringæøåÆØÅöôêÊ字符串> {
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 */
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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<Array<NonAsciiStringæøåÆØÅöôêÊ字符串>> {
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 */
Expand Down
27 changes: 27 additions & 0 deletions test/spec/v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
34 changes: 34 additions & 0 deletions test/spec/v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit 7588831

Please sign in to comment.