Skip to content

Add support for x-enum-varnames and x-enum-descriptions #1374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-news-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

Add support for x-enum-varnames and x-enum-descriptions
41 changes: 41 additions & 0 deletions docs/src/content/docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,44 @@ Cat: { type?: "cat"; } & components["schemas"]["PetCommonProperties"];
_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._

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.

### Enum with custom names and descriptions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't come up with something better than copying the existing description from one of the OpenAPI generators projects :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me!


`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.

`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).

`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.

Example:

```yaml
ErrorCode:
type: integer
format: int32
enum:
- 100
- 200
- 300
x-enum-varnames:
- Unauthorized
- AccessDenied
- Unknown
x-enum-descriptions:
- "User is not authorized"
- "User has no access to this resource"
- "Something went wrong"
```

Will result in:

```ts
enum ErrorCode {
// User is not authorized
Unauthorized = 100
// User has no access to this resource
AccessDenied = 200
// Something went wrong
Unknown = 300
}
```
46 changes: 33 additions & 13 deletions packages/openapi-typescript/src/lib/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
export function tsEnum(
name: string,
members: (string | number)[],
metadata?: { name?: string; description?: string }[],
options?: { readonly?: boolean; export?: boolean },
) {
let enumName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
Expand All @@ -249,28 +250,47 @@ export function tsEnum(
})
: undefined,
/* name */ enumName,
/* members */ members.map(tsEnumMember),
/* members */ members.map((value, i) =>
tsEnumMember(value, metadata?.[i]),
),
);
}

/** Sanitize TS enum member expression */
export function tsEnumMember(value: string | number) {
if (typeof value === "number") {
return ts.factory.createEnumMember(
`Value${String(value)}`.replace(".", "_"), // don’t forget decimals
ts.factory.createNumericLiteral(value),
);
}
let name = value;
export function tsEnumMember(
value: string | number,
metadata: { name?: string; description?: string } = {},
) {
let name = metadata.name ?? String(value);
if (!JS_PROPERTY_INDEX_RE.test(name)) {
if (Number(name[0]) >= 0) {
name = `Value${name}`;
name = `Value${name}`.replace(".", "_"); // don't forged decimals;
}
name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, "_");
}
return ts.factory.createEnumMember(
name,
ts.factory.createStringLiteral(value),

let member;
if (typeof value === "number") {
member = ts.factory.createEnumMember(
name,
ts.factory.createNumericLiteral(value),
);
} else {
member = ts.factory.createEnumMember(
name,
ts.factory.createStringLiteral(value),
);
}

if (metadata.description == undefined) {
return member;
}

return ts.addSyntheticLeadingComment(
member,
ts.SyntaxKind.SingleLineCommentTrivia,
" ".concat(metadata.description.trim()),
true,
);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,14 @@ export function transformSchemaObjectWithComposition(
let enumName = parseRef(options.path ?? "").pointer.join("/");
// allow #/components/schemas to have simpler names
enumName = enumName.replace("components/schemas", "");
const metadata = schemaObject.enum.map((_, i) => ({
name: schemaObject["x-enum-varnames"]?.[i],
description: schemaObject["x-enum-descriptions"]?.[i],
}));
const enumType = tsEnum(
enumName,
schemaObject.enum as (string | number)[],
metadata,
{ export: true, readonly: options.ctx.immutable },
);
options.ctx.injectFooter.push(enumType);
Expand Down
105 changes: 105 additions & 0 deletions packages/openapi-typescript/test/lib/ts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,111 @@ describe("tsEnum", () => {
Value100 = 100,
Value101 = 101,
Value102 = 102
}`);
});

it("number members with x-enum-descriptions", () => {
expect(
astToString(
tsEnum(
".Error.code.",
[100, 101, 102],
[
{ description: "Code 100" },
{ description: "Code 101" },
{ description: "Code 102" },
],
),
).trim(),
).toBe(`enum ErrorCode {
// Code 100
Value100 = 100,
// Code 101
Value101 = 101,
// Code 102
Value102 = 102
}`);
});

it("x-enum-varnames", () => {
expect(
astToString(
tsEnum(
".Error.code.",
[100, 101, 102],
[
{ name: "Unauthorized" },
{ name: "NotFound" },
{ name: "PermissionDenied" },
],
),
).trim(),
).toBe(`enum ErrorCode {
Unauthorized = 100,
NotFound = 101,
PermissionDenied = 102
}`);
});

it("x-enum-varnames with numeric prefix", () => {
expect(
astToString(
tsEnum(
".Error.code.",
[100, 101, 102],
[{ name: "0a" }, { name: "1b" }, { name: "2c" }],
),
).trim(),
).toBe(`enum ErrorCode {
Value0a = 100,
Value1b = 101,
Value2c = 102
}`);
});

it("partial x-enum-varnames and x-enum-descriptions", () => {
expect(
astToString(
tsEnum(
".Error.code.",
[100, 101, 102],
[
{ name: "Unauthorized", description: "User is unauthorized" },
{ name: "NotFound" },
],
),
).trim(),
).toBe(`enum ErrorCode {
// User is unauthorized
Unauthorized = 100,
NotFound = 101,
Value102 = 102
}`);
});

it("x-enum-descriptions with x-enum-varnames", () => {
expect(
astToString(
tsEnum(
".Error.code.",
[100, 101, 102],
[
{ name: "Unauthorized", description: "User is unauthorized" },
{ name: "NotFound", description: "Item not found" },
{
name: "PermissionDenied",
description: "User doesn't have permissions",
},
],
),
).trim(),
).toBe(`enum ErrorCode {
// User is unauthorized
Unauthorized = 100,
// Item not found
NotFound = 101,
// User doesn't have permissions
PermissionDenied = 102
}`);
});
});
Expand Down