Skip to content

Commit d2de5c7

Browse files
feat: brings back --make-paths-enum option to generate ApiPaths enum (#2052)
* feat: brings back --make-paths-enum option to generate ApiPaths enum * chore: adds --make-paths-enum flag to cli docs * chore: adds minor changeset for * tests: adds tests for --make-paths-enum option and paths-enum.ts transformer
1 parent d4689b1 commit d2de5c7

File tree

10 files changed

+276
-1
lines changed

10 files changed

+276
-1
lines changed

Diff for: .changeset/healthy-rabbits-flow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
brings back --make-paths-enum option to generate ApiPaths enum

Diff for: docs/cli.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Refer to the [Redocly docs](https://redocly.com/docs/cli/configuration/#resolve-
100100
The following flags are supported in the CLI:
101101

102102
| Flag | Alias | Default | Description |
103-
| :--------------------------------- | :---- | :------: | :------------------------------------------------------------------------------------------------------------------ |
103+
|:-----------------------------------| :---- | :------: |:--------------------------------------------------------------------------------------------------------------------|
104104
| `--help` | | | Display inline help message and exit |
105105
| `--version` | | | Display this library’s version and exit |
106106
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
@@ -121,6 +121,7 @@ The following flags are supported in the CLI:
121121
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
122122
| `--root-types` | | `false` | Exports types from `components` as root level type aliases |
123123
| `--root-types-no-schema-prefix` | | `false` | Do not add "Schema" prefix to types at the root level (should only be used with --root-types) |
124+
| `--make-paths-enum ` | | `false` | Generate ApiPaths enum for all paths |
124125

125126
### pathParamsAsTypes
126127

@@ -207,3 +208,22 @@ This results in more explicit typechecking of array lengths.
207208
_Note: this has a reasonable limit, so for example `maxItems: 100` would simply flatten back down to `string[];`_
208209

209210
_Thanks, [@kgtkr](https://github.com/kgtkr)!_
211+
212+
### makePathsEnum
213+
214+
This option is useful for generating an enum for all paths in the schema. This can be useful to use the paths from the schema in your code.
215+
216+
Enabling `--make-paths-enum` will add an `ApiPaths` enum like this to the generated types:
217+
218+
::: code-group
219+
220+
```ts [my-openapi-3-schema.d.ts]
221+
export enum ApiPaths {
222+
"/user/{user_id}" = "/user/{user_id}",
223+
"/user" = "/user",
224+
"/user/{user_id}/pets" = "/user/{user_id}/pets",
225+
"/user/{user_id}/pets/{pet_id}" = "/user/{user_id}/pets/{pet_id}",
226+
}
227+
```
228+
229+
:::

Diff for: packages/openapi-typescript/bin/cli.js

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Options
3333
--root-types (optional) Export schemas types at root level
3434
--root-types-no-schema-prefix (optional)
3535
Do not add "Schema" prefix to types at the root level (should only be used with --root-types)
36+
--make-paths-enum Generate ApiPaths enum for all paths
3637
`;
3738

3839
const OUTPUT_FILE = "FILE";
@@ -82,6 +83,7 @@ const flags = parser(args, {
8283
"pathParamsAsTypes",
8384
"rootTypes",
8485
"rootTypesNoSchemaPrefix",
86+
"makePathsEnum",
8587
],
8688
string: ["output", "redocly"],
8789
alias: {
@@ -143,6 +145,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
143145
pathParamsAsTypes: flags.pathParamsAsTypes,
144146
rootTypes: flags.rootTypes,
145147
rootTypesNoSchemaPrefix: flags.rootTypesNoSchemaPrefix,
148+
makePathsEnum: flags.makePathsEnum,
146149
redocly,
147150
silent,
148151
}),

Diff for: packages/openapi-typescript/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default async function openapiTS(
8989
silent: options.silent ?? false,
9090
inject: options.inject ?? undefined,
9191
transform: typeof options.transform === "function" ? options.transform : undefined,
92+
makePathsEnum: options.makePathsEnum ?? false,
9293
resolve($ref) {
9394
return resolveRef(schema, $ref, { silent: options.silent ?? false });
9495
},

Diff for: packages/openapi-typescript/src/transform/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import transformComponentsObject from "./components-object.js";
77
import transformPathsObject from "./paths-object.js";
88
import transformSchemaObject from "./schema-object.js";
99
import transformWebhooksObject from "./webhooks-object.js";
10+
import makeApiPathsEnum from "./paths-enum.js";
1011

1112
type SchemaTransforms = keyof Pick<OpenAPI3, "paths" | "webhooks" | "components" | "$defs">;
1213

@@ -93,5 +94,9 @@ export default function transformSchema(schema: OpenAPI3, ctx: GlobalContext) {
9394
);
9495
}
9596

97+
if (ctx.makePathsEnum && schema.paths) {
98+
type.push(makeApiPathsEnum(schema.paths));
99+
}
100+
96101
return type;
97102
}
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type ts from "typescript";
2+
import { tsEnum } from "../lib/ts.js";
3+
import { getEntries } from "../lib/utils.js";
4+
import type { PathsObject } from "../types.js";
5+
6+
export default function makeApiPathsEnum(pathsObject: PathsObject): ts.EnumDeclaration {
7+
const enumKeys = [];
8+
const enumMetaData = [];
9+
10+
for (const [url, pathItemObject] of getEntries(pathsObject)) {
11+
for (const [method, operation] of Object.entries(pathItemObject)) {
12+
if (!["get", "put", "post", "delete", "options", "head", "patch", "trace"].includes(method)) {
13+
continue;
14+
}
15+
16+
// Generate a name from the operation ID
17+
let pathName: string;
18+
if (operation.operationId) {
19+
pathName = operation.operationId;
20+
} else {
21+
// If the operation ID is not present, construct a name from the method and path
22+
pathName = (method + url)
23+
.split("/")
24+
.map((part) => {
25+
const capitalised = part.charAt(0).toUpperCase() + part.slice(1);
26+
27+
// Remove any characters not allowed as enum keys, and attempt to remove
28+
// named parameters.
29+
return capitalised.replace(/{.*}|:.*|[^a-zA-Z\d_]+/, "");
30+
})
31+
.join("");
32+
}
33+
34+
// Replace {parameters} with :parameters
35+
const adaptedUrl = url.replace(/{(\w+)}/g, ":$1");
36+
37+
enumKeys.push(adaptedUrl);
38+
enumMetaData.push({
39+
name: pathName,
40+
});
41+
}
42+
}
43+
44+
return tsEnum("ApiPaths", enumKeys, enumMetaData, {
45+
export: true,
46+
});
47+
}

Diff for: packages/openapi-typescript/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,8 @@ export interface OpenAPITSOptions {
668668
redocly?: RedoclyConfig;
669669
/** Inject arbitrary TypeScript types into the start of the file */
670670
inject?: string;
671+
/** Generate ApiPaths enum */
672+
makePathsEnum?: boolean;
671673
}
672674

673675
/** Context passed to all submodules */
@@ -700,6 +702,7 @@ export interface GlobalContext {
700702
/** retrieve a node by $ref */
701703
resolve<T>($ref: string): T | undefined;
702704
inject?: string;
705+
makePathsEnum: boolean;
703706
}
704707

705708
export type $defs = Record<string, SchemaObject>;

Diff for: packages/openapi-typescript/test/index.test.ts

+65
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,71 @@ export type operations = Record<string, never>;`,
694694
},
695695
},
696696
],
697+
[
698+
"$refs > path object & paths enum",
699+
{
700+
given: new URL("./fixtures/path-object-refs.yaml", import.meta.url),
701+
want: `export interface paths {
702+
"/get-item": {
703+
parameters: {
704+
query?: never;
705+
header?: never;
706+
path?: never;
707+
cookie?: never;
708+
};
709+
get: {
710+
parameters: {
711+
query?: never;
712+
header?: never;
713+
path?: never;
714+
cookie?: never;
715+
};
716+
requestBody?: never;
717+
responses: {
718+
/** @description OK */
719+
200: {
720+
headers: {
721+
[name: string]: unknown;
722+
};
723+
content: {
724+
"application/json": components["schemas"]["Item"];
725+
};
726+
};
727+
};
728+
};
729+
put?: never;
730+
post?: never;
731+
delete?: never;
732+
options?: never;
733+
head?: never;
734+
patch?: never;
735+
trace?: never;
736+
};
737+
}
738+
export type webhooks = Record<string, never>;
739+
export interface components {
740+
schemas: {
741+
Item: {
742+
id: string;
743+
name: string;
744+
};
745+
};
746+
responses: never;
747+
parameters: never;
748+
requestBodies: never;
749+
headers: never;
750+
pathItems: never;
751+
}
752+
export type $defs = Record<string, never>;
753+
export type operations = Record<string, never>;
754+
export enum ApiPaths {
755+
GetGetitem = "/get-item"
756+
}`,
757+
options: {
758+
makePathsEnum: true,
759+
},
760+
},
761+
],
697762
];
698763

699764
for (const [testName, { given, want, options, ci }] of tests) {

Diff for: packages/openapi-typescript/test/test-helpers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const DEFAULT_CTX: GlobalContext = {
3131
},
3232
silent: true,
3333
transform: undefined,
34+
makePathsEnum: false,
3435
};
3536

3637
/** Generic test case */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { fileURLToPath } from "node:url";
2+
import { astToString } from "../../src/lib/ts.js";
3+
import makeApiPathsEnum from "../../src/transform/paths-enum.js";
4+
import type { GlobalContext } from "../../src/types.js";
5+
import type { TestCase } from "../test-helpers.js";
6+
7+
describe("transformPathsObjectToEnum", () => {
8+
const tests: TestCase<any, GlobalContext>[] = [
9+
[
10+
"basic",
11+
{
12+
given: {
13+
"/api/v1/user": {
14+
get: {},
15+
},
16+
},
17+
want: `export enum ApiPaths {
18+
GetApiV1User = "/api/v1/user"
19+
}`,
20+
},
21+
],
22+
[
23+
"basic with path parameter",
24+
{
25+
given: {
26+
"/api/v1/user/{user_id}": {
27+
parameters: [
28+
{
29+
name: "page",
30+
in: "query",
31+
schema: { type: "number" },
32+
description: "Page number.",
33+
},
34+
],
35+
get: {
36+
parameters: [{ name: "user_id", in: "path", description: "User ID." }],
37+
},
38+
},
39+
},
40+
want: `export enum ApiPaths {
41+
GetApiV1User = "/api/v1/user/:user_id"
42+
}`,
43+
},
44+
],
45+
[
46+
"with operationId",
47+
{
48+
given: {
49+
"/api/v1/user/{user_id}": {
50+
parameters: [
51+
{
52+
name: "page",
53+
in: "query",
54+
schema: { type: "number" },
55+
description: "Page number.",
56+
},
57+
],
58+
get: {
59+
operationId: "GetUserById",
60+
parameters: [{ name: "user_id", in: "path", description: "User ID." }],
61+
},
62+
},
63+
},
64+
want: `export enum ApiPaths {
65+
GetUserById = "/api/v1/user/:user_id"
66+
}`,
67+
},
68+
],
69+
[
70+
"with and without operationId",
71+
{
72+
given: {
73+
"/api/v1/user/{user_id}": {
74+
parameters: [
75+
{
76+
name: "page",
77+
in: "query",
78+
schema: { type: "number" },
79+
description: "Page number.",
80+
},
81+
],
82+
get: {
83+
operationId: "GetUserById",
84+
parameters: [{ name: "user_id", in: "path", description: "User ID." }],
85+
},
86+
post: {
87+
parameters: [{ name: "user_id", in: "path", description: "User ID." }],
88+
},
89+
},
90+
},
91+
want: `export enum ApiPaths {
92+
GetUserById = "/api/v1/user/:user_id",
93+
PostApiV1User = "/api/v1/user/:user_id"
94+
}`,
95+
},
96+
],
97+
[
98+
"invalid method",
99+
{
100+
given: {
101+
"/api/v1/user": {
102+
invalidMethod: {},
103+
},
104+
},
105+
want: `export enum ApiPaths {
106+
}`,
107+
},
108+
],
109+
];
110+
111+
for (const [testName, { given, want, ci }] of tests) {
112+
test.skipIf(ci?.skipIf)(
113+
testName,
114+
async () => {
115+
const result = astToString(makeApiPathsEnum(given));
116+
if (want instanceof URL) {
117+
expect(result).toMatchFileSnapshot(fileURLToPath(want));
118+
} else {
119+
expect(result).toBe(`${want}\n`);
120+
}
121+
},
122+
ci?.timeout,
123+
);
124+
}
125+
});

0 commit comments

Comments
 (0)