Skip to content

Commit 0cdfc47

Browse files
feat(openapi-typescript): generate path params flag (#2102)
generate path params flag
1 parent d508ddc commit 0cdfc47

File tree

9 files changed

+247
-2
lines changed

9 files changed

+247
-2
lines changed

.changeset/purple-walls-repeat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
Support generating path params for flaky schemas using --generate-path-params option

docs/cli.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ 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 |
124+
| `--make-paths-enum` | | `false` | Generate ApiPaths enum for all paths |
125+
| `--generate-path-params` | | `false` | Generate path parameters for all paths where they are undefined by schema |
125126

126127
### pathParamsAsTypes
127128

@@ -227,3 +228,9 @@ export enum ApiPaths {
227228
```
228229

229230
:::
231+
232+
### generatePathParams
233+
234+
This option is useful for generating path params optimistically when the schema has flaky path parameter definitions.
235+
Checks the path for opening and closing brackets and extracts them as path parameters.
236+
Does not override already defined by schema path parameters.

packages/openapi-typescript/bin/cli.js

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const flags = parser(args, {
8484
"rootTypes",
8585
"rootTypesNoSchemaPrefix",
8686
"makePathsEnum",
87+
"generatePathParams",
8788
],
8889
string: ["output", "redocly"],
8990
alias: {
@@ -146,6 +147,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
146147
rootTypes: flags.rootTypes,
147148
rootTypesNoSchemaPrefix: flags.rootTypesNoSchemaPrefix,
148149
makePathsEnum: flags.makePathsEnum,
150+
generatePathParams: flags.generatePathParams,
149151
redocly,
150152
silent,
151153
}),

packages/openapi-typescript/src/index.ts

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

packages/openapi-typescript/src/transform/parameters-array.ts

+53-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ import { createRef } from "../lib/utils.js";
44
import type { ParameterObject, ReferenceObject, TransformNodeOptions } from "../types.js";
55
import transformParameterObject from "./parameter-object.js";
66

7+
// Regex to match path parameters in URL
8+
const PATH_PARAM_RE = /\{([^}]+)\}/g;
9+
10+
/**
11+
* Create a synthetic path parameter object from a parameter name
12+
*/
13+
function createPathParameter(paramName: string): ParameterObject {
14+
return {
15+
name: paramName,
16+
in: "path",
17+
required: true,
18+
schema: { type: "string" },
19+
};
20+
}
21+
22+
/**
23+
* Extract path parameters from a URL
24+
*/
25+
function extractPathParamsFromUrl(path: string): ParameterObject[] {
26+
const params: ParameterObject[] = [];
27+
const matches = path.match(PATH_PARAM_RE);
28+
if (matches) {
29+
for (const match of matches) {
30+
const paramName = match.slice(1, -1);
31+
params.push(createPathParameter(paramName));
32+
}
33+
}
34+
return params;
35+
}
36+
737
/**
838
* Synthetic type. Array of (ParameterObject | ReferenceObject)s found in OperationObject and PathItemObject.
939
*/
@@ -13,14 +43,36 @@ export function transformParametersArray(
1343
): ts.TypeElement[] {
1444
const type: ts.TypeElement[] = [];
1545

46+
// Create a working copy of parameters array
47+
const workingParameters = [...parametersArray];
48+
49+
// Generate path parameters if enabled
50+
if (options.ctx.generatePathParams && options.path) {
51+
const pathString = Array.isArray(options.path) ? options.path[0] : options.path;
52+
if (typeof pathString === "string") {
53+
const pathParams = extractPathParamsFromUrl(pathString);
54+
// Only add path parameters that aren't already defined
55+
for (const param of pathParams) {
56+
const exists = workingParameters.some((p) => {
57+
const resolved = "$ref" in p ? options.ctx.resolve<ParameterObject>(p.$ref) : p;
58+
return resolved?.in === "path" && resolved?.name === param.name;
59+
});
60+
if (!exists) {
61+
workingParameters.push(param);
62+
}
63+
}
64+
}
65+
}
66+
1667
// parameters
1768
const paramType: ts.TypeElement[] = [];
1869
for (const paramIn of ["query", "header", "path", "cookie"] as ParameterObject["in"][]) {
1970
const paramLocType: ts.TypeElement[] = [];
20-
let operationParameters = parametersArray.map((param) => ({
71+
let operationParameters = workingParameters.map((param) => ({
2172
original: param,
2273
resolved: "$ref" in param ? options.ctx.resolve<ParameterObject>(param.$ref) : param,
2374
}));
75+
2476
// this is the only array type in the spec, so we have to one-off sort here
2577
if (options.ctx.alphabetize) {
2678
operationParameters.sort((a, b) => (a.resolved?.name ?? "").localeCompare(b.resolved?.name ?? ""));

packages/openapi-typescript/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,8 @@ export interface OpenAPITSOptions {
670670
inject?: string;
671671
/** Generate ApiPaths enum */
672672
makePathsEnum?: boolean;
673+
/** Generate path params based on path even if they are not defiend in the open api schema */
674+
generatePathParams?: boolean;
673675
}
674676

675677
/** Context passed to all submodules */
@@ -703,6 +705,7 @@ export interface GlobalContext {
703705
resolve<T>($ref: string): T | undefined;
704706
inject?: string;
705707
makePathsEnum: boolean;
708+
generatePathParams: boolean;
706709
}
707710

708711
export type $defs = Record<string, SchemaObject>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
openapi: "3.0"
2+
info:
3+
title: Test
4+
version: "1.0"
5+
paths:
6+
/{id}/get-item-undefined-path-param:
7+
description: Remote Ref
8+
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
9+
/{id}/get-item-undefined-nested-path-param/{secondId}:
10+
description: Remote Ref
11+
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
12+
parameters:
13+
-
14+
name: id
15+
in: path
16+
required: true
17+
schema:
18+
type: number
19+
/{id}/get-item-defined-path-param:
20+
description: Remote Ref
21+
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
22+
parameters:
23+
-
24+
name: id
25+
in: path
26+
required: true
27+
schema:
28+
type: number

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

+146
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,152 @@ export enum ApiPaths {
759759
},
760760
},
761761
],
762+
[
763+
"Generates path parameters",
764+
{
765+
given: new URL("./fixtures/generate-params-test.yaml", import.meta.url),
766+
want: `export interface paths {
767+
"/{id}/get-item-undefined-path-param": {
768+
parameters: {
769+
query?: never;
770+
header?: never;
771+
path: {
772+
id: string;
773+
};
774+
cookie?: never;
775+
};
776+
get: {
777+
parameters: {
778+
query?: never;
779+
header?: never;
780+
path: {
781+
id: string;
782+
};
783+
cookie?: never;
784+
};
785+
requestBody?: never;
786+
responses: {
787+
/** @description OK */
788+
200: {
789+
headers: {
790+
[name: string]: unknown;
791+
};
792+
content: {
793+
"application/json": components["schemas"]["Item"];
794+
};
795+
};
796+
};
797+
};
798+
put?: never;
799+
post?: never;
800+
delete?: never;
801+
options?: never;
802+
head?: never;
803+
patch?: never;
804+
trace?: never;
805+
};
806+
"/{id}/get-item-undefined-nested-path-param/{secondId}": {
807+
parameters: {
808+
query?: never;
809+
header?: never;
810+
path: {
811+
id: number;
812+
secondId: string;
813+
};
814+
cookie?: never;
815+
};
816+
get: {
817+
parameters: {
818+
query?: never;
819+
header?: never;
820+
path: {
821+
id: number;
822+
secondId: string;
823+
};
824+
cookie?: never;
825+
};
826+
requestBody?: never;
827+
responses: {
828+
/** @description OK */
829+
200: {
830+
headers: {
831+
[name: string]: unknown;
832+
};
833+
content: {
834+
"application/json": components["schemas"]["Item"];
835+
};
836+
};
837+
};
838+
};
839+
put?: never;
840+
post?: never;
841+
delete?: never;
842+
options?: never;
843+
head?: never;
844+
patch?: never;
845+
trace?: never;
846+
};
847+
"/{id}/get-item-defined-path-param": {
848+
parameters: {
849+
query?: never;
850+
header?: never;
851+
path: {
852+
id: number;
853+
};
854+
cookie?: never;
855+
};
856+
get: {
857+
parameters: {
858+
query?: never;
859+
header?: never;
860+
path: {
861+
id: number;
862+
};
863+
cookie?: never;
864+
};
865+
requestBody?: never;
866+
responses: {
867+
/** @description OK */
868+
200: {
869+
headers: {
870+
[name: string]: unknown;
871+
};
872+
content: {
873+
"application/json": components["schemas"]["Item"];
874+
};
875+
};
876+
};
877+
};
878+
put?: never;
879+
post?: never;
880+
delete?: never;
881+
options?: never;
882+
head?: never;
883+
patch?: never;
884+
trace?: never;
885+
};
886+
}
887+
export type webhooks = Record<string, never>;
888+
export interface components {
889+
schemas: {
890+
Item: {
891+
id: string;
892+
name: string;
893+
};
894+
};
895+
responses: never;
896+
parameters: never;
897+
requestBodies: never;
898+
headers: never;
899+
pathItems: never;
900+
}
901+
export type $defs = Record<string, never>;
902+
export type operations = Record<string, never>;`,
903+
options: {
904+
generatePathParams: true,
905+
},
906+
},
907+
],
762908
[
763909
"nullable > 3.0 syntax",
764910
{

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

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const DEFAULT_CTX: GlobalContext = {
3232
silent: true,
3333
transform: undefined,
3434
makePathsEnum: false,
35+
generatePathParams: false,
3536
};
3637

3738
/** Generic test case */

0 commit comments

Comments
 (0)