Skip to content

Commit a2a1ab8

Browse files
committed
fix: camelcase dot name
1 parent cd25f5d commit a2a1ab8

File tree

37 files changed

+1667
-65
lines changed

37 files changed

+1667
-65
lines changed

.changeset/plenty-weeks-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
fix: correctly transform string to pascalcase when referenced inside schema

packages/openapi-ts/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"dependencies": {
6363
"@apidevtools/json-schema-ref-parser": "11.7.0",
6464
"c12": "1.11.1",
65-
"camelcase": "8.0.0",
6665
"commander": "12.1.0",
6766
"handlebars": "4.7.8"
6867
},

packages/openapi-ts/src/generate/services.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import camelcase from 'camelcase';
2-
31
import {
42
ClassElement,
53
type Comments,
@@ -13,6 +11,7 @@ import type { Model, Operation, OperationParameter, Service } from '../openApi';
1311
import { isOperationParameterRequired } from '../openApi/common/parser/operation';
1412
import type { Client } from '../types/client';
1513
import type { Files } from '../types/utils';
14+
import { camelCase } from '../utils/camelCase';
1615
import { getConfig, isStandaloneClient } from '../utils/config';
1716
import { escapeComment, escapeName } from '../utils/escape';
1817
import { reservedWordsRegExp } from '../utils/reservedWords';
@@ -56,17 +55,26 @@ export const modelResponseTransformerTypeName = (name: string) =>
5655
`${name}ModelResponseTransformer`;
5756

5857
export const operationDataTypeName = (name: string) =>
59-
`${camelcase(name, { pascalCase: true })}Data`;
58+
`${camelCase({
59+
input: name,
60+
pascalCase: true,
61+
})}Data`;
6062

6163
export const operationErrorTypeName = (name: string) =>
62-
`${camelcase(name, { pascalCase: true })}Error`;
64+
`${camelCase({
65+
input: name,
66+
pascalCase: true,
67+
})}Error`;
6368

6469
// operation response type ends with "Response", it's enough to append "Transformer"
6570
export const operationResponseTransformerTypeName = (name: string) =>
6671
`${name}Transformer`;
6772

6873
export const operationResponseTypeName = (name: string) =>
69-
`${camelcase(name, { pascalCase: true })}Response`;
74+
`${camelCase({
75+
input: name,
76+
pascalCase: true,
77+
})}Response`;
7078

7179
/**
7280
* @param importedType unique type name returned from `setUniqueTypeName()`

packages/openapi-ts/src/openApi/common/parser/operation.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import camelCase from 'camelcase';
2-
1+
import { camelCase } from '../../../utils/camelCase';
32
import { getConfig, isStandaloneClient } from '../../../utils/config';
43
import type {
54
OperationParameter,
@@ -20,7 +19,9 @@ export const getOperationName = (
2019
const config = getConfig();
2120

2221
if (config.services.operationId && operationId) {
23-
return camelCase(sanitizeNamespaceIdentifier(operationId).trim());
22+
return camelCase({
23+
input: sanitizeNamespaceIdentifier(operationId),
24+
});
2425
}
2526

2627
let urlWithoutPlaceholders = url;
@@ -39,7 +40,9 @@ export const getOperationName = (
3940
// replace slashes with hyphens for camelcase method at the end
4041
.replace(/\//g, '-');
4142

42-
return camelCase(`${method}-${urlWithoutPlaceholders}`);
43+
return camelCase({
44+
input: `${method}-${urlWithoutPlaceholders}`,
45+
});
4346
};
4447

4548
export const getOperationResponseHeader = (

packages/openapi-ts/src/openApi/common/parser/service.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import camelCase from 'camelcase';
2-
1+
import { camelCase } from '../../../utils/camelCase';
32
import type { Operation, Service } from '../interfaces/client';
43
import { sanitizeNamespaceIdentifier } from './sanitize';
54

@@ -35,7 +34,8 @@ export function getServiceVersion(version = '1.0'): string {
3534
* Convert the input value to a correct service name. This converts
3635
* the input string to PascalCase.
3736
*/
38-
export const getServiceName = (value: string): string => {
39-
const clean = sanitizeNamespaceIdentifier(value).trim();
40-
return camelCase(clean, { pascalCase: true });
41-
};
37+
export const getServiceName = (value: string): string =>
38+
camelCase({
39+
input: sanitizeNamespaceIdentifier(value),
40+
pascalCase: true,
41+
});

packages/openapi-ts/src/openApi/common/parser/type.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import camelcase from 'camelcase';
2-
1+
import { camelCase } from '../../../utils/camelCase';
32
import { getConfig, isStandaloneClient } from '../../../utils/config';
43
import { refParametersPartial } from '../../../utils/const';
54
import { reservedWordsRegExp } from '../../../utils/reservedWords';
@@ -172,7 +171,8 @@ export const transformTypeKeyName = (value: string): string => {
172171
return value;
173172
}
174173

175-
const clean = sanitizeOperationParameterName(value).trim();
176-
const name = camelcase(clean).replace(reservedWordsRegExp, '_$1');
174+
const name = camelCase({
175+
input: sanitizeOperationParameterName(value),
176+
}).replace(reservedWordsRegExp, '_$1');
177177
return name;
178178
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const UPPERCASE = /[\p{Lu}]/u;
2+
const LOWERCASE = /[\p{Ll}]/u;
3+
const IDENTIFIER = /([\p{Alpha}\p{N}_]|$)/u;
4+
const SEPARATORS = /[_.\- ]+/;
5+
6+
const LEADING_SEPARATORS = new RegExp('^' + SEPARATORS.source);
7+
const SEPARATORS_AND_IDENTIFIER = new RegExp(
8+
SEPARATORS.source + IDENTIFIER.source,
9+
'gu',
10+
);
11+
const NUMBERS_AND_IDENTIFIER = new RegExp('\\d+' + IDENTIFIER.source, 'gu');
12+
13+
const preserveCamelCase = (string: string) => {
14+
let isLastCharLower = false;
15+
let isLastCharUpper = false;
16+
let isLastLastCharUpper = false;
17+
let isLastLastCharPreserved = false;
18+
19+
for (let index = 0; index < string.length; index++) {
20+
const character = string[index];
21+
isLastLastCharPreserved = index > 2 ? string[index - 3] === '-' : true;
22+
23+
if (isLastCharLower && UPPERCASE.test(character)) {
24+
string = string.slice(0, index) + '-' + string.slice(index);
25+
isLastCharLower = false;
26+
isLastLastCharUpper = isLastCharUpper;
27+
isLastCharUpper = true;
28+
index++;
29+
} else if (
30+
isLastCharUpper &&
31+
isLastLastCharUpper &&
32+
LOWERCASE.test(character) &&
33+
!isLastLastCharPreserved
34+
) {
35+
string = string.slice(0, index - 1) + '-' + string.slice(index - 1);
36+
isLastLastCharUpper = isLastCharUpper;
37+
isLastCharUpper = false;
38+
isLastCharLower = true;
39+
} else {
40+
isLastCharLower =
41+
character.toLocaleLowerCase() === character &&
42+
character.toLocaleUpperCase() !== character;
43+
isLastLastCharUpper = isLastCharUpper;
44+
isLastCharUpper =
45+
character.toLocaleUpperCase() === character &&
46+
character.toLocaleLowerCase() !== character;
47+
}
48+
}
49+
50+
return string;
51+
};
52+
53+
/**
54+
* Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar`. Correctly handles Unicode strings. Returns transformed string.
55+
*/
56+
export const camelCase = ({
57+
input,
58+
pascalCase,
59+
}: {
60+
input: string;
61+
/**
62+
* Uppercase the first character: `foo-bar` → `FooBar`
63+
*
64+
* @default false
65+
*/
66+
readonly pascalCase?: boolean;
67+
}): string => {
68+
let result = input.trim();
69+
70+
if (!result.length) {
71+
return '';
72+
}
73+
74+
if (result.length === 1) {
75+
if (SEPARATORS.test(result)) {
76+
return '';
77+
}
78+
79+
return pascalCase ? result.toLocaleUpperCase() : result.toLocaleLowerCase();
80+
}
81+
82+
const hasUpperCase = result !== result.toLocaleLowerCase();
83+
84+
if (hasUpperCase) {
85+
result = preserveCamelCase(result);
86+
}
87+
88+
result = result.replace(LEADING_SEPARATORS, '');
89+
result = result.toLocaleLowerCase();
90+
91+
if (pascalCase) {
92+
result = result.charAt(0).toLocaleUpperCase() + result.slice(1);
93+
}
94+
95+
SEPARATORS_AND_IDENTIFIER.lastIndex = 0;
96+
NUMBERS_AND_IDENTIFIER.lastIndex = 0;
97+
98+
result = result.replaceAll(NUMBERS_AND_IDENTIFIER, (match, _, offset) => {
99+
if (['_', '-', '.'].includes(result.charAt(offset + match.length))) {
100+
return match;
101+
}
102+
103+
return match.toLocaleUpperCase();
104+
});
105+
106+
result = result.replaceAll(SEPARATORS_AND_IDENTIFIER, (_, identifier) =>
107+
identifier.toLocaleUpperCase(),
108+
);
109+
110+
return result;
111+
};

packages/openapi-ts/src/utils/handlebars.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import camelCase from 'camelcase';
21
import Handlebars from 'handlebars/runtime';
32

43
import templateClient from '../templates/client.hbs';
@@ -45,10 +44,18 @@ import xhrGetResponseBody from '../templates/core/xhr/getResponseBody.hbs';
4544
import xhrGetResponseHeader from '../templates/core/xhr/getResponseHeader.hbs';
4645
import xhrRequest from '../templates/core/xhr/request.hbs';
4746
import xhrSendRequest from '../templates/core/xhr/sendRequest.hbs';
47+
import { camelCase } from './camelCase';
4848
import { transformServiceName } from './transform';
4949

5050
export const registerHandlebarHelpers = (): void => {
51-
Handlebars.registerHelper('camelCase', camelCase);
51+
Handlebars.registerHelper(
52+
'camelCase',
53+
function (this: unknown, name: string) {
54+
return camelCase({
55+
input: name,
56+
});
57+
},
58+
);
5259

5360
Handlebars.registerHelper(
5461
'equals',

packages/openapi-ts/src/utils/transform.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import camelcase from 'camelcase';
2-
31
import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi/common/parser/sanitize';
2+
import { camelCase } from './camelCase';
43
import { getConfig } from './config';
54
import { reservedWordsRegExp } from './reservedWords';
65

@@ -15,7 +14,10 @@ export const transformServiceName = (name: string) => {
1514
export const transformTypeName = (name: string) => {
1615
const config = getConfig();
1716
if (config.types.name === 'PascalCase') {
18-
return camelcase(name, { pascalCase: true });
17+
return camelCase({
18+
input: name,
19+
pascalCase: true,
20+
});
1921
}
2022
return name;
2123
};

packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-axios-bundle/types.gen.ts.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,30 @@ export type SchemaWithFormRestrictedKeys = {
10191019
})>;
10201020
};
10211021

1022+
/**
1023+
* This schema was giving PascalCase transformations a hard time
1024+
*/
1025+
export type io_k8s_apimachinery_pkg_apis_meta_v1_DeleteOptions = {
1026+
/**
1027+
* Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.
1028+
*/
1029+
preconditions?: io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions;
1030+
};
1031+
1032+
/**
1033+
* This schema was giving PascalCase transformations a hard time
1034+
*/
1035+
export type io_k8s_apimachinery_pkg_apis_meta_v1_Preconditions = {
1036+
/**
1037+
* Specifies the target ResourceVersion
1038+
*/
1039+
resourceVersion?: string;
1040+
/**
1041+
* Specifies the target UID.
1042+
*/
1043+
uid?: string;
1044+
};
1045+
10221046
/**
10231047
* This is a reusable parameter
10241048
*/

0 commit comments

Comments
 (0)