Skip to content

Commit df308c0

Browse files
committed
CR + Fix doc
1 parent 1600f29 commit df308c0

File tree

7 files changed

+95
-12
lines changed

7 files changed

+95
-12
lines changed

docs/src/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@ Array of plugins to use. See [Using Plugins](./using-plugins) for more details.
6666
Options passed to OpenAPI TypeScript for type generation. You normally should
6767
not have to change them.
6868

69-
[!ref target="blank" text="See OpenAPI TypeScript options"](https://openapi-ts.pages.dev/cli#flags)
69+
[!ref target="blank" text="See OpenAPI TypeScript options"](https://openapi-ts.dev/cli#flags)

packages/create-schemas/src/plugins/types-plugin.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,33 +76,51 @@ export function isComponentsSchema(node: ts.Node): node is ts.PropertySignature
7676
* OpenAPI field names must match `^[a-zA-Z0-9\.\-_]+$` which allows names that
7777
* are not valid JavaScript/TypeScript identifiers. This function converts an
7878
* unsafe name into a safe name that can be used as a JavaScript/TypeScript
79-
* identifier.
79+
* identifier using PascalCase transformation to match openapi-typescript's enum naming.
8080
*/
8181
export function toSafeName(unsafeName: string): string {
8282
let safeName = "";
83+
let capitalizeNext = true;
84+
8385
for (const char of unsafeName) {
8486
const charCode = char.charCodeAt(0);
8587

88+
// Special characters that should trigger capitalization of next char
89+
if (char === "-" || char === "." || char === " ") {
90+
capitalizeNext = true;
91+
continue;
92+
}
93+
8694
// A-Z
8795
if (charCode >= 65 && charCode <= 90) {
8896
safeName += char;
97+
capitalizeNext = false;
98+
continue;
8999
}
90100

91101
// a-z
92102
if (charCode >= 97 && charCode <= 122) {
93-
safeName += char;
103+
safeName += capitalizeNext ? char.toUpperCase() : char;
104+
capitalizeNext = false;
105+
continue;
94106
}
95107

96-
if (char === "_" || char === "$") {
108+
// 0-9
109+
if (safeName.length > 0 && charCode >= 48 && charCode <= 57) {
97110
safeName += char;
111+
capitalizeNext = false;
112+
continue;
98113
}
99114

100-
// 0-9
101-
if (safeName.length > 0 && charCode >= 48 && charCode <= 57) {
115+
// _ and $ are valid identifier characters
116+
if (char === "_" || char === "$") {
102117
safeName += char;
118+
capitalizeNext = false;
119+
continue;
103120
}
104121

105-
continue;
122+
// Any other character triggers capitalization of next char but is not included
123+
capitalizeNext = true;
106124
}
107125

108126
return safeName;

packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ export interface components {
4444
Status: Status;
4545
/** @enum {string} */
4646
Priority: Priority;
47+
/** @enum {string} */
48+
"user-status": UserStatus;
49+
/** @enum {string} */
50+
"task.priority": TaskPriority;
4751
Task: {
4852
id?: string;
4953
status?: components["schemas"]["Status"];
5054
priority?: components["schemas"]["Priority"];
55+
userStatus?: components["schemas"]["user-status"];
56+
taskPriority?: components["schemas"]["task.priority"];
5157
name?: string;
5258
};
5359
};
@@ -68,6 +74,16 @@ export enum Priority {
6874
medium = "medium",
6975
high = "high"
7076
}
77+
export enum UserStatus {
78+
online = "online",
79+
offline = "offline",
80+
away = "away"
81+
}
82+
export enum TaskPriority {
83+
urgent = "urgent",
84+
normal = "normal",
85+
low_priority = "low-priority"
86+
}
7187
export type operations = Record<string, never>;
7288
7389
export type Task = components["schemas"]["Task"];

packages/create-schemas/tests/data/enums.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ components:
2222
- low
2323
- medium
2424
- high
25+
user-status:
26+
type: string
27+
enum:
28+
- online
29+
- offline
30+
- away
31+
task.priority:
32+
type: string
33+
enum:
34+
- urgent
35+
- normal
36+
- low-priority
2537
Task:
2638
type: object
2739
properties:
@@ -31,5 +43,9 @@ components:
3143
$ref: '#/components/schemas/Status'
3244
priority:
3345
$ref: '#/components/schemas/Priority'
46+
userStatus:
47+
$ref: '#/components/schemas/user-status'
48+
taskPriority:
49+
$ref: '#/components/schemas/task.priority'
3450
name:
3551
type: string

packages/create-schemas/tests/e2e.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,21 @@ describe.concurrent("e2e", () => {
149149
const typesFile = result.find(file => file.filename === openapiTypeScriptFilename);
150150
assert(typesFile);
151151

152-
// Verify enums are generated
152+
// Verify enums are generated (including those with special characters)
153153
expect(typesFile.code).toContain("export enum Status");
154154
expect(typesFile.code).toContain("export enum Priority");
155+
expect(typesFile.code).toContain("export enum UserStatus");
156+
expect(typesFile.code).toContain("export enum TaskPriority");
155157

156158
// Verify NO duplicate type aliases for enums
157159
const statusTypeAliasRegex = /export type Status = components\["schemas"\]\["Status"\];/;
158160
const priorityTypeAliasRegex = /export type Priority = components\["schemas"\]\["Priority"\];/;
161+
const userStatusTypeAliasRegex = /export type UserStatus = components\["schemas"\]\["user-status"\];/;
162+
const taskPriorityTypeAliasRegex = /export type TaskPriority = components\["schemas"\]\["task\.priority"\];/;
159163
expect(typesFile.code).not.toMatch(statusTypeAliasRegex);
160164
expect(typesFile.code).not.toMatch(priorityTypeAliasRegex);
165+
expect(typesFile.code).not.toMatch(userStatusTypeAliasRegex);
166+
expect(typesFile.code).not.toMatch(taskPriorityTypeAliasRegex);
161167

162168
// Verify non-enum types still get type aliases
163169
expect(typesFile.code).toContain('export type Task = components["schemas"]["Task"];');

packages/create-schemas/tests/generate.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ describe.concurrent("generate", () => {
1919
expect(typesFile.code).toMatch("export type User");
2020
expect(typesFile.code).toMatch("export type User_1");
2121
expect(typesFile.code).toMatch("export type User_Name");
22-
expect(typesFile.code).toMatch("export type username");
23-
expect(typesFile.code).toMatch("export type myorgUser");
22+
expect(typesFile.code).toMatch("export type UserName");
23+
expect(typesFile.code).toMatch("export type MyOrgUser");
2424
});
2525

2626
test("reject ambiguous names", async ({ expect }) => {

packages/create-schemas/tests/plugins.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ describe.concurrent("plugins", () => {
6565
export type User = components["schemas"]["User"];
6666
export type User_1 = components["schemas"]["User_1"];
6767
export type User_Name = components["schemas"]["User_Name"];
68-
export type username = components["schemas"]["user-name"];
69-
export type myorgUser = components["schemas"]["my.org.User"];
68+
export type UserName = components["schemas"]["user-name"];
69+
export type MyOrgUser = components["schemas"]["my.org.User"];
7070
export type Endpoints = keyof paths;
7171
"
7272
`);
@@ -149,4 +149,31 @@ describe.concurrent("plugins", () => {
149149

150150
expect(emittedFile).toBeUndefined();
151151
});
152+
153+
test("typesPlugin with enums", async ({ expect }) => {
154+
const plugin = typesPlugin();
155+
assert(plugin.transform);
156+
157+
const result = await plugin.transform({
158+
config: await resolveConfig({ input: "openapi.json" }),
159+
id: openapiTypeScriptId,
160+
code: `export interface components {
161+
schemas: {
162+
Status: string;
163+
Task: { name: string };
164+
}
165+
}
166+
export enum Status {
167+
Active = "active",
168+
Inactive = "inactive"
169+
}`,
170+
filename: openapiTypeScriptFilename,
171+
emitFile: () => void 0
172+
});
173+
174+
assert(result);
175+
176+
expect(result.code).toContain('export type Task = components["schemas"]["Task"]');
177+
expect(result.code).not.toContain('export type Status = components["schemas"]["Status"]');
178+
});
152179
});

0 commit comments

Comments
 (0)