Skip to content

feat: add dedupeEnums option #1775

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 6 commits into from
Jul 31, 2024
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/wet-foxes-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

feat: add dedupeEnums option
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ The following flags are supported in the CLI:
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
| `--enum` | | `false` | Generate true [TS enums](https://www.typescriptlang.org/docs/handbook/enums.html) rather than string unions. |
| `--enum-values` | | `false` | Export enum values as arrays. |
| `--dedupe-enums` | | `false` | Dedupe enum types when `--enum=true` is set |
| `--check` | | `false` | Check that the generated types are up-to-date. |
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
Expand Down
3 changes: 2 additions & 1 deletion docs/zh/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ CLI 支持以下参数:
| `--empty-objects-unknown` | | `false` | 允许在未指定属性和未指定 `additionalProperties` 的情况下,为模式对象设置任意属性 |
| `--enum` | | `false` | 生成真实的 [TS 枚举](https://www.typescriptlang.org/docs/handbook/enums.html),而不是字符串联合。 |
| `--enum-values` | | `false` | 将 enum 值导出为数组 |
| `--check` | | `false` | 检查生成的类型是否是最新的 |
| `--dedupe-enums` | | `false` | 将 enum 值导出为数组 |
| `--check` | | `false` | 当 --enum=true 时,去除重复定义的枚举类型 |
| `--exclude-deprecated` | | `false` | 从类型中排除已弃用的字段 |
| `--export-type` | `-t` | `false` | 导出 `type` 而不是 `interface` |
| `--immutable` | | `false` | 生成不可变类型(只读属性和只读数组) |
Expand Down
7 changes: 1 addition & 6 deletions packages/openapi-typescript-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@
},
"./*": "./*"
},
"files": [
"index.js",
"index.cjs",
"index.d.ts",
"index.d.cts"
],
"files": ["index.js", "index.cjs", "index.d.ts", "index.d.cts"],
"homepage": "https://openapi-ts.dev",
"repository": {
"type": "git",
Expand Down
3 changes: 3 additions & 0 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Options
--output, -o Specify output file (if not specified in redocly.yaml)
--enum Export true TS enums instead of unions
--enum-values Export enum values as arrays
--dedupe-enums Dedupe enum types when \`--enum=true\` is set
--check Check that the generated types are up-to-date. (default: false)
--export-type, -t Export top-level \`type\` instead of \`interface\`
--immutable Generate readonly types
Expand Down Expand Up @@ -66,6 +67,7 @@ const flags = parser(args, {
"emptyObjectsUnknown",
"enum",
"enumValues",
"dedupeEnums",
"check",
"excludeDeprecated",
"exportType",
Expand Down Expand Up @@ -126,6 +128,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
emptyObjectsUnknown: flags.emptyObjectsUnknown,
enum: flags.enum,
enumValues: flags.enumValues,
dedupeEnums: flags.dedupeEnums,
excludeDeprecated: flags.excludeDeprecated,
exportType: flags.exportType,
immutable: flags.immutable,
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export default async function openapiTS(
emptyObjectsUnknown: options.emptyObjectsUnknown ?? false,
enum: options.enum ?? false,
enumValues: options.enumValues ?? false,
dedupeEnums: options.dedupeEnums ?? false,
excludeDeprecated: options.excludeDeprecated ?? false,
exportType: options.exportType ?? false,
immutable: options.immutable ?? false,
Expand Down
21 changes: 19 additions & 2 deletions packages/openapi-typescript/src/lib/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,37 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
return filteredTypes;
}

export const enumCache = new Map<string, ts.EnumDeclaration>();

/** Create a TS enum (with sanitized name and members) */
export function tsEnum(
name: string,
members: (string | number)[],
metadata?: { name?: string; description?: string }[],
options?: { export?: boolean },
options?: { export?: boolean; shouldCache?: boolean },
) {
let enumName = sanitizeMemberName(name);
enumName = `${enumName[0].toUpperCase()}${enumName.substring(1)}`;
return ts.factory.createEnumDeclaration(
let key = "";
if (options?.shouldCache) {
key = `${members
.slice(0)
.sort()
.map((v, i) => {
return `${metadata?.[i]?.name ?? String(v)}:${metadata?.[i]?.description || ""}`;
})
.join(",")}`;
if (enumCache.has(key)) {
return enumCache.get(key) as ts.EnumDeclaration;
}
}
const enumDeclaration = ts.factory.createEnumDeclaration(
/* modifiers */ options ? tsModifiers({ export: options.export ?? false }) : undefined,
/* name */ enumName,
/* members */ members.map((value, i) => tsEnumMember(value, metadata?.[i])),
);
options?.shouldCache && enumCache.set(key, enumDeclaration);
return enumDeclaration;
}

/** Create an exported TS array literal expression */
Expand Down
19 changes: 8 additions & 11 deletions packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,14 @@ export function transformSchemaObjectWithComposition(
name: schemaObject["x-enum-varnames"]?.[i] ?? schemaObject["x-enumNames"]?.[i],
description: schemaObject["x-enum-descriptions"]?.[i] ?? schemaObject["x-enumDescriptions"]?.[i],
}));
const enumType = tsEnum(
enumName,
schemaObject.enum as (string | number)[],
metadata,

{
export: true,
// readonly: TS enum do not support the readonly modifier
},
);
options.ctx.injectFooter.push(enumType);
const enumType = tsEnum(enumName, schemaObject.enum as (string | number)[], metadata, {
shouldCache: options.ctx.dedupeEnums,
export: true,
// readonly: TS enum do not support the readonly modifier
});
if (!options.ctx.injectFooter.includes(enumType)) {
options.ctx.injectFooter.push(enumType);
}
return ts.factory.createTypeReferenceNode(enumType.name);
}
const enumType = schemaObject.enum.map(tsLiteral);
Expand Down
3 changes: 3 additions & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,8 @@ export interface OpenAPITSOptions {
enum?: boolean;
/** Export union values as arrays */
enumValues?: boolean;
/** Dedupe enum values */
dedupeEnums?: boolean;
/** (optional) Substitute path parameter names with their respective types */
pathParamsAsTypes?: boolean;
/** Treat all objects as if they have \`required\` set to all properties by default (default: false) */
Expand Down Expand Up @@ -678,6 +680,7 @@ export interface GlobalContext {
emptyObjectsUnknown: boolean;
enum: boolean;
enumValues: boolean;
dedupeEnums: boolean;
excludeDeprecated: boolean;
exportType: boolean;
immutable: boolean;
Expand Down
87 changes: 87 additions & 0 deletions packages/openapi-typescript/test/node-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,93 @@ export type operations = Record<string, never>;`,
options: { enumValues: true },
},
],
[
"options > dedupeEnums",
{
given: {
openapi: "3.1",
info: { title: "Test", version: "1.0" },
paths: {
"/url": {
get: {
parameters: [
{
name: "status",
in: "query",
schema: {
type: "string",
enum: ["active", "inactive"],
},
},
],
},
},
},
components: {
schemas: {
Status: {
type: "string",
enum: ["active", "inactive"],
},
StatusReverse: {
type: "string",
enum: ["inactive", "active"],
},
},
},
},
want: `export interface paths {
"/url": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: {
status?: PathsUrlGetParametersQueryStatus;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: never;
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** @enum {string} */
Status: PathsUrlGetParametersQueryStatus;
/** @enum {string} */
StatusReverse: PathsUrlGetParametersQueryStatus;
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export enum PathsUrlGetParametersQueryStatus {
active = "active",
inactive = "inactive"
}
export type operations = Record<string, never>;`,
options: { enum: true, dedupeEnums: true },
},
],
[
"snapshot > GitHub",
{
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/test/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const DEFAULT_CTX: GlobalContext = {
emptyObjectsUnknown: false,
enum: false,
enumValues: false,
dedupeEnums: false,
excludeDeprecated: false,
exportType: false,
immutable: false,
Expand Down