Skip to content

Commit ad21cb2

Browse files
committed
Fix empty string TypeScript enum members
1 parent 0cc7ee7 commit ad21cb2

4 files changed

Lines changed: 122 additions & 13 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Generate valid TypeScript enum members for empty string enum values when using `--enum`.

packages/openapi-typescript/src/lib/ts.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -408,20 +408,24 @@ function sanitizeMemberName(name: string) {
408408
export function tsEnumMember(value: string | number, metadata: { name?: string; description?: string | null } = {}) {
409409
let name = metadata.name ?? String(value);
410410
if (!JS_PROPERTY_INDEX_RE.test(name)) {
411-
if (Number(name[0]) >= 0) {
412-
name = `Value${name}`.replace(".", "_"); // don't forged decimals;
413-
} else if (name[0] === "-") {
414-
name = `ValueMinus${name.slice(1)}`;
415-
}
411+
if (name === "") {
412+
name = `"${name}"`;
413+
} else {
414+
if (Number(name[0]) >= 0) {
415+
name = `Value${name}`.replace(".", "_"); // don't forged decimals;
416+
} else if (name[0] === "-") {
417+
name = `ValueMinus${name.slice(1)}`;
418+
}
416419

417-
const invalidCharMatch = name.match(JS_PROPERTY_INDEX_INVALID_CHARS_RE);
418-
if (invalidCharMatch) {
419-
if (invalidCharMatch[0] === name) {
420-
name = `"${name}"`;
421-
} else {
422-
name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, (s) => {
423-
return s in SPECIAL_CHARACTER_MAP ? SPECIAL_CHARACTER_MAP[s] : "_";
424-
});
420+
const invalidCharMatch = name.match(JS_PROPERTY_INDEX_INVALID_CHARS_RE);
421+
if (invalidCharMatch) {
422+
if (invalidCharMatch[0] === name) {
423+
name = `"${name}"`;
424+
} else {
425+
name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, (s) => {
426+
return s in SPECIAL_CHARACTER_MAP ? SPECIAL_CHARACTER_MAP[s] : "_";
427+
});
428+
}
425429
}
426430
}
427431
}

packages/openapi-typescript/test/lib/ts.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ describe("tsEnum", () => {
218218
}`);
219219
});
220220

221+
test("empty string member", () => {
222+
expect(astToString(tsEnum("type", ["", "foo", "bar"])).trim()).toBe(`enum Type {
223+
"" = "",
224+
foo = "foo",
225+
bar = "bar"
226+
}`);
227+
});
228+
221229
test("number members", () => {
222230
expect(astToString(tsEnum(".Error.code.", [100, 101, 102, -100])).trim()).toBe(`enum ErrorCode {
223231
Value100 = 100,

packages/openapi-typescript/test/transform/schema-object/enum.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,62 @@ export type operations = Record<string, never>;`,
237237
options: { ctx: createTestContext({ enum: true, conditionalEnums: true }) },
238238
},
239239
],
240+
[
241+
"options > enum: true with empty string enum member",
242+
{
243+
given: mockEmptyStringEnumSchema(),
244+
want: `export interface paths {
245+
"/test": {
246+
parameters: {
247+
query?: never;
248+
header?: never;
249+
path?: never;
250+
cookie?: never;
251+
};
252+
/** Test */
253+
get: operations["test"];
254+
put?: never;
255+
post?: never;
256+
delete?: never;
257+
options?: never;
258+
head?: never;
259+
patch?: never;
260+
trace?: never;
261+
};
262+
}
263+
export type webhooks = Record<string, never>;
264+
export type components = Record<string, never>;
265+
export type $defs = Record<string, never>;
266+
export interface operations {
267+
test: {
268+
parameters: {
269+
query?: {
270+
type?: PathsTestGetParametersQueryType;
271+
};
272+
header?: never;
273+
path?: never;
274+
cookie?: never;
275+
};
276+
requestBody?: never;
277+
responses: {
278+
/** @description OK */
279+
200: {
280+
headers: {
281+
[name: string]: unknown;
282+
};
283+
content?: never;
284+
};
285+
};
286+
};
287+
}
288+
export enum PathsTestGetParametersQueryType {
289+
"" = "",
290+
foo = "foo",
291+
bar = "bar"
292+
}`,
293+
options: { ctx: createTestContext({ enum: true }) },
294+
},
295+
],
240296
];
241297

242298
describe("transformComponentsObject", () => {
@@ -324,6 +380,42 @@ function mockSchema() {
324380
};
325381
}
326382

383+
function mockEmptyStringEnumSchema() {
384+
return {
385+
openapi: "3.1.0",
386+
info: {
387+
title: "Test",
388+
version: "0.1.0",
389+
},
390+
paths: {
391+
"/test": {
392+
get: {
393+
summary: "Test",
394+
operationId: "test",
395+
parameters: [
396+
{
397+
name: "type",
398+
in: "query",
399+
required: false,
400+
schema: {
401+
enum: ["", "foo", "bar"],
402+
type: "string",
403+
default: "",
404+
title: "Type",
405+
},
406+
},
407+
],
408+
responses: {
409+
200: {
410+
description: "OK",
411+
},
412+
},
413+
},
414+
},
415+
},
416+
};
417+
}
418+
327419
function createTestContext(overrides: Partial<typeof DEFAULT_CTX> = {}) {
328420
return {
329421
...DEFAULT_CTX,

0 commit comments

Comments
 (0)