Skip to content

Commit

Permalink
Add support for x-enum-varnames and x-enum-descriptions (#1374)
Browse files Browse the repository at this point in the history
* Add support for x-enum-varnames and x-enum-descriptions

* Refactor ts enum members
  • Loading branch information
ElForastero committed Oct 11, 2023
1 parent 679b954 commit 7ac5174
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 13 deletions.
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

`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

0 comments on commit 7ac5174

Please sign in to comment.