Skip to content
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

feat(openapi-typescript): generate path params flag #2102

Merged
merged 1 commit into from
Jan 25, 2025
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/purple-walls-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

Support generating path params for flaky schemas using --generate-path-params option
9 changes: 8 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ The following flags are supported in the CLI:
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |
| `--root-types` | | `false` | Exports types from `components` as root level type aliases |
| `--root-types-no-schema-prefix` | | `false` | Do not add "Schema" prefix to types at the root level (should only be used with --root-types) |
| `--make-paths-enum ` | | `false` | Generate ApiPaths enum for all paths |
| `--make-paths-enum` | | `false` | Generate ApiPaths enum for all paths |
| `--generate-path-params` | | `false` | Generate path parameters for all paths where they are undefined by schema |

### pathParamsAsTypes

Expand Down Expand Up @@ -227,3 +228,9 @@ export enum ApiPaths {
```

:::

### generatePathParams

This option is useful for generating path params optimistically when the schema has flaky path parameter definitions.
Checks the path for opening and closing brackets and extracts them as path parameters.
Does not override already defined by schema path parameters.
2 changes: 2 additions & 0 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const flags = parser(args, {
"rootTypes",
"rootTypesNoSchemaPrefix",
"makePathsEnum",
"generatePathParams",
],
string: ["output", "redocly"],
alias: {
Expand Down Expand Up @@ -146,6 +147,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
rootTypes: flags.rootTypes,
rootTypesNoSchemaPrefix: flags.rootTypesNoSchemaPrefix,
makePathsEnum: flags.makePathsEnum,
generatePathParams: flags.generatePathParams,
redocly,
silent,
}),
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 @@ -90,6 +90,7 @@ export default async function openapiTS(
inject: options.inject ?? undefined,
transform: typeof options.transform === "function" ? options.transform : undefined,
makePathsEnum: options.makePathsEnum ?? false,
generatePathParams: options.generatePathParams ?? false,
resolve($ref) {
return resolveRef(schema, $ref, { silent: options.silent ?? false });
},
Expand Down
54 changes: 53 additions & 1 deletion packages/openapi-typescript/src/transform/parameters-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ import { createRef } from "../lib/utils.js";
import type { ParameterObject, ReferenceObject, TransformNodeOptions } from "../types.js";
import transformParameterObject from "./parameter-object.js";

// Regex to match path parameters in URL
const PATH_PARAM_RE = /\{([^}]+)\}/g;

/**
* Create a synthetic path parameter object from a parameter name
*/
function createPathParameter(paramName: string): ParameterObject {
return {
name: paramName,
in: "path",
required: true,
schema: { type: "string" },
};
}

/**
* Extract path parameters from a URL
*/
function extractPathParamsFromUrl(path: string): ParameterObject[] {
const params: ParameterObject[] = [];
const matches = path.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
const paramName = match.slice(1, -1);
params.push(createPathParameter(paramName));
}
}
return params;
}

/**
* Synthetic type. Array of (ParameterObject | ReferenceObject)s found in OperationObject and PathItemObject.
*/
Expand All @@ -13,14 +43,36 @@ export function transformParametersArray(
): ts.TypeElement[] {
const type: ts.TypeElement[] = [];

// Create a working copy of parameters array
const workingParameters = [...parametersArray];

// Generate path parameters if enabled
if (options.ctx.generatePathParams && options.path) {
const pathString = Array.isArray(options.path) ? options.path[0] : options.path;
if (typeof pathString === "string") {
const pathParams = extractPathParamsFromUrl(pathString);
// Only add path parameters that aren't already defined
for (const param of pathParams) {
const exists = workingParameters.some((p) => {
const resolved = "$ref" in p ? options.ctx.resolve<ParameterObject>(p.$ref) : p;
return resolved?.in === "path" && resolved?.name === param.name;
});
if (!exists) {
workingParameters.push(param);
}
}
}
}

// parameters
const paramType: ts.TypeElement[] = [];
for (const paramIn of ["query", "header", "path", "cookie"] as ParameterObject["in"][]) {
const paramLocType: ts.TypeElement[] = [];
let operationParameters = parametersArray.map((param) => ({
let operationParameters = workingParameters.map((param) => ({
original: param,
resolved: "$ref" in param ? options.ctx.resolve<ParameterObject>(param.$ref) : param,
}));

// this is the only array type in the spec, so we have to one-off sort here
if (options.ctx.alphabetize) {
operationParameters.sort((a, b) => (a.resolved?.name ?? "").localeCompare(b.resolved?.name ?? ""));
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 @@ -670,6 +670,8 @@ export interface OpenAPITSOptions {
inject?: string;
/** Generate ApiPaths enum */
makePathsEnum?: boolean;
/** Generate path params based on path even if they are not defiend in the open api schema */
generatePathParams?: boolean;
}

/** Context passed to all submodules */
Expand Down Expand Up @@ -703,6 +705,7 @@ export interface GlobalContext {
resolve<T>($ref: string): T | undefined;
inject?: string;
makePathsEnum: boolean;
generatePathParams: boolean;
}

export type $defs = Record<string, SchemaObject>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: "3.0"
info:
title: Test
version: "1.0"
paths:
/{id}/get-item-undefined-path-param:
description: Remote Ref
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
/{id}/get-item-undefined-nested-path-param/{secondId}:
description: Remote Ref
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
parameters:
-
name: id
in: path
required: true
schema:
type: number
/{id}/get-item-defined-path-param:
description: Remote Ref
$ref: "_path-object-refs-paths.yaml#/GetItemOperation"
parameters:
-
name: id
in: path
required: true
schema:
type: number
146 changes: 146 additions & 0 deletions packages/openapi-typescript/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,152 @@ export enum ApiPaths {
},
},
],
[
"Generates path parameters",
{
given: new URL("./fixtures/generate-params-test.yaml", import.meta.url),
want: `export interface paths {
"/{id}/get-item-undefined-path-param": {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Item"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/{id}/get-item-undefined-nested-path-param/{secondId}": {
parameters: {
query?: never;
header?: never;
path: {
id: number;
secondId: string;
};
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
id: number;
secondId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Item"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/{id}/get-item-defined-path-param": {
parameters: {
query?: never;
header?: never;
path: {
id: number;
};
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path: {
id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Item"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Item: {
id: string;
name: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;`,
options: {
generatePathParams: true,
},
},
],
[
"nullable > 3.0 syntax",
{
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 @@ -32,6 +32,7 @@ export const DEFAULT_CTX: GlobalContext = {
silent: true,
transform: undefined,
makePathsEnum: false,
generatePathParams: false,
};

/** Generic test case */
Expand Down
Loading