Skip to content

Commit 7ac5174

Browse files
authored
Add support for x-enum-varnames and x-enum-descriptions (#1374)
* Add support for x-enum-varnames and x-enum-descriptions * Refactor ts enum members
1 parent 679b954 commit 7ac5174

File tree

5 files changed

+189
-13
lines changed

5 files changed

+189
-13
lines changed

.changeset/chilled-news-design.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
Add support for x-enum-varnames and x-enum-descriptions

docs/src/content/docs/advanced.md

+41
Original file line numberDiff line numberDiff line change
@@ -521,3 +521,44 @@ Cat: { type?: "cat"; } & components["schemas"]["PetCommonProperties"];
521521
_Note: you optionally could provide `discriminator.propertyName: "type"` on `Pet` ([docs](https://spec.openapis.org/oas/v3.1.0#discriminator-object)) to automatically generate the `type` key, but is less explicit._
522522

523523
While the schema permits you to use composition in any way you like, it’s good to always take a look at the generated types and see if there’s a simpler way to express your unions & intersections. Limiting the use of `oneOf` is not the only way to do that, but often yields the greatest benefits.
524+
525+
### Enum with custom names and descriptions
526+
527+
`x-enum-varnames` can be used to have another enum name for the corresponding value. This is used to define names of the enum items.
528+
529+
`x-enum-descriptions` can be used to provide an individual description for each value. This is used for comments in the code (like javadoc if the target language is java).
530+
531+
`x-enum-descriptions` and `x-enum-varnames` are each expected to be list of items containing the same number of items as enum. The order of the items in the list matters: their position is used to group them together.
532+
533+
Example:
534+
535+
```yaml
536+
ErrorCode:
537+
type: integer
538+
format: int32
539+
enum:
540+
- 100
541+
- 200
542+
- 300
543+
x-enum-varnames:
544+
- Unauthorized
545+
- AccessDenied
546+
- Unknown
547+
x-enum-descriptions:
548+
- "User is not authorized"
549+
- "User has no access to this resource"
550+
- "Something went wrong"
551+
```
552+
553+
Will result in:
554+
555+
```ts
556+
enum ErrorCode {
557+
// User is not authorized
558+
Unauthorized = 100
559+
// User has no access to this resource
560+
AccessDenied = 200
561+
// Something went wrong
562+
Unknown = 300
563+
}
564+
```

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

+33-13
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
229229
export function tsEnum(
230230
name: string,
231231
members: (string | number)[],
232+
metadata?: { name?: string; description?: string }[],
232233
options?: { readonly?: boolean; export?: boolean },
233234
) {
234235
let enumName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
@@ -249,28 +250,47 @@ export function tsEnum(
249250
})
250251
: undefined,
251252
/* name */ enumName,
252-
/* members */ members.map(tsEnumMember),
253+
/* members */ members.map((value, i) =>
254+
tsEnumMember(value, metadata?.[i]),
255+
),
253256
);
254257
}
255258

256259
/** Sanitize TS enum member expression */
257-
export function tsEnumMember(value: string | number) {
258-
if (typeof value === "number") {
259-
return ts.factory.createEnumMember(
260-
`Value${String(value)}`.replace(".", "_"), // don’t forget decimals
261-
ts.factory.createNumericLiteral(value),
262-
);
263-
}
264-
let name = value;
260+
export function tsEnumMember(
261+
value: string | number,
262+
metadata: { name?: string; description?: string } = {},
263+
) {
264+
let name = metadata.name ?? String(value);
265265
if (!JS_PROPERTY_INDEX_RE.test(name)) {
266266
if (Number(name[0]) >= 0) {
267-
name = `Value${name}`;
267+
name = `Value${name}`.replace(".", "_"); // don't forged decimals;
268268
}
269269
name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, "_");
270270
}
271-
return ts.factory.createEnumMember(
272-
name,
273-
ts.factory.createStringLiteral(value),
271+
272+
let member;
273+
if (typeof value === "number") {
274+
member = ts.factory.createEnumMember(
275+
name,
276+
ts.factory.createNumericLiteral(value),
277+
);
278+
} else {
279+
member = ts.factory.createEnumMember(
280+
name,
281+
ts.factory.createStringLiteral(value),
282+
);
283+
}
284+
285+
if (metadata.description == undefined) {
286+
return member;
287+
}
288+
289+
return ts.addSyntheticLeadingComment(
290+
member,
291+
ts.SyntaxKind.SingleLineCommentTrivia,
292+
" ".concat(metadata.description.trim()),
293+
true,
274294
);
275295
}
276296

packages/openapi-typescript/src/transform/schema-object.ts

+5
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,14 @@ export function transformSchemaObjectWithComposition(
124124
let enumName = parseRef(options.path ?? "").pointer.join("/");
125125
// allow #/components/schemas to have simpler names
126126
enumName = enumName.replace("components/schemas", "");
127+
const metadata = schemaObject.enum.map((_, i) => ({
128+
name: schemaObject["x-enum-varnames"]?.[i],
129+
description: schemaObject["x-enum-descriptions"]?.[i],
130+
}));
127131
const enumType = tsEnum(
128132
enumName,
129133
schemaObject.enum as (string | number)[],
134+
metadata,
130135
{ export: true, readonly: options.ctx.immutable },
131136
);
132137
options.ctx.injectFooter.push(enumType);

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

+105
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,111 @@ describe("tsEnum", () => {
127127
Value100 = 100,
128128
Value101 = 101,
129129
Value102 = 102
130+
}`);
131+
});
132+
133+
it("number members with x-enum-descriptions", () => {
134+
expect(
135+
astToString(
136+
tsEnum(
137+
".Error.code.",
138+
[100, 101, 102],
139+
[
140+
{ description: "Code 100" },
141+
{ description: "Code 101" },
142+
{ description: "Code 102" },
143+
],
144+
),
145+
).trim(),
146+
).toBe(`enum ErrorCode {
147+
// Code 100
148+
Value100 = 100,
149+
// Code 101
150+
Value101 = 101,
151+
// Code 102
152+
Value102 = 102
153+
}`);
154+
});
155+
156+
it("x-enum-varnames", () => {
157+
expect(
158+
astToString(
159+
tsEnum(
160+
".Error.code.",
161+
[100, 101, 102],
162+
[
163+
{ name: "Unauthorized" },
164+
{ name: "NotFound" },
165+
{ name: "PermissionDenied" },
166+
],
167+
),
168+
).trim(),
169+
).toBe(`enum ErrorCode {
170+
Unauthorized = 100,
171+
NotFound = 101,
172+
PermissionDenied = 102
173+
}`);
174+
});
175+
176+
it("x-enum-varnames with numeric prefix", () => {
177+
expect(
178+
astToString(
179+
tsEnum(
180+
".Error.code.",
181+
[100, 101, 102],
182+
[{ name: "0a" }, { name: "1b" }, { name: "2c" }],
183+
),
184+
).trim(),
185+
).toBe(`enum ErrorCode {
186+
Value0a = 100,
187+
Value1b = 101,
188+
Value2c = 102
189+
}`);
190+
});
191+
192+
it("partial x-enum-varnames and x-enum-descriptions", () => {
193+
expect(
194+
astToString(
195+
tsEnum(
196+
".Error.code.",
197+
[100, 101, 102],
198+
[
199+
{ name: "Unauthorized", description: "User is unauthorized" },
200+
{ name: "NotFound" },
201+
],
202+
),
203+
).trim(),
204+
).toBe(`enum ErrorCode {
205+
// User is unauthorized
206+
Unauthorized = 100,
207+
NotFound = 101,
208+
Value102 = 102
209+
}`);
210+
});
211+
212+
it("x-enum-descriptions with x-enum-varnames", () => {
213+
expect(
214+
astToString(
215+
tsEnum(
216+
".Error.code.",
217+
[100, 101, 102],
218+
[
219+
{ name: "Unauthorized", description: "User is unauthorized" },
220+
{ name: "NotFound", description: "Item not found" },
221+
{
222+
name: "PermissionDenied",
223+
description: "User doesn't have permissions",
224+
},
225+
],
226+
),
227+
).trim(),
228+
).toBe(`enum ErrorCode {
229+
// User is unauthorized
230+
Unauthorized = 100,
231+
// Item not found
232+
NotFound = 101,
233+
// User doesn't have permissions
234+
PermissionDenied = 102
130235
}`);
131236
});
132237
});

0 commit comments

Comments
 (0)