Skip to content

Support single operation parameter #1535

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 2 commits into from
Feb 15, 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/breezy-trees-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

Support single operation parameter
5 changes: 3 additions & 2 deletions packages/openapi-typescript/src/transform/operation-object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, OperationObject, ParameterObject } from "../types.js";
import { escObjKey, getEntries, getSchemaObjectComment, indent, tsOptionalProperty, tsReadonly } from "../utils.js";
import { escObjKey, getEntries, getParametersArray, getSchemaObjectComment, indent, tsOptionalProperty, tsReadonly } from "../utils.js";
import transformParameterObject from "./parameter-object.js";
import transformRequestBodyObject from "./request-body-object.js";
import transformResponseObject from "./response-object.js";
Expand All @@ -19,13 +19,14 @@ export default function transformOperationObject(operationObject: OperationObjec
// parameters
{
if (operationObject.parameters) {
const parametersArray = getParametersArray(operationObject.parameters);
const parameterOutput: string[] = [];
indentLv++;
for (const paramIn of ["query", "header", "path", "cookie"] as ParameterObject["in"][]) {
const paramInternalOutput: string[] = [];
indentLv++;
let paramInOptional = true;
for (const param of operationObject.parameters ?? []) {
for (const param of parametersArray) {
const node: ParameterObject | undefined = "$ref" in param ? ctx.parameters[param.$ref] : param;
if (node?.in !== paramIn) continue;
let key = escObjKey(node.name);
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-typescript/src/transform/path-item-object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, ParameterObject, PathItemObject, ReferenceObject } from "../types.js";
import { escStr, getSchemaObjectComment, indent } from "../utils.js";
import { escStr, getParametersArray, getSchemaObjectComment, indent } from "../utils.js";
import transformOperationObject from "./operation-object.js";

export interface TransformPathItemObjectOptions {
Expand All @@ -26,7 +26,7 @@ export default function transformPathItemObject(pathItem: PathItemObject, { path
const keyedParameters: Record<string, ParameterObject | ReferenceObject> = {};
if (!("$ref" in operationObject)) {
// important: OperationObject parameters come last, and will override any conflicts with PathItem parameters
for (const parameter of [...(pathItem.parameters ?? []), ...(operationObject.parameters ?? [])]) {
for (const parameter of [...(pathItem.parameters ?? []), ...getParametersArray(operationObject.parameters)]) {
// note: the actual key doesn’t matter here, as long as it can match between PathItem and OperationObject
keyedParameters["$ref" in parameter ? parameter.$ref : `${parameter.in}/${parameter.name}`] = parameter;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-typescript/src/transform/paths-object.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, PathsObject, PathItemObject, ParameterObject, ReferenceObject, OperationObject } from "../types.js";
import { escStr, getEntries, getSchemaObjectComment, indent } from "../utils.js";
import { escStr, getEntries, getParametersArray, getSchemaObjectComment, indent } from "../utils.js";
import transformParameterObject from "./parameter-object.js";
import transformPathItemObject from "./path-item-object.js";

Expand All @@ -8,7 +8,7 @@ const OPERATIONS = ["get", "post", "put", "delete", "options", "head", "patch",
function extractPathParams(obj?: ReferenceObject | PathItemObject | OperationObject | undefined): Map<string, ParameterObject> {
const params = new Map<string, ParameterObject>();
if (obj && "parameters" in obj) {
for (const p of obj.parameters ?? []) {
for (const p of getParametersArray(obj.parameters)) {
if ("in" in p && p.in === "path") params.set(p.name, p);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export interface OperationObject extends Extensable {
/** Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions. */
operationId?: string;
/** A list of parameters that are applicable for this operation. If a parameter is already defined at the Path Item, the new definition will override it but can never remove it. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object’s components/parameters. */
parameters?: (ParameterObject | ReferenceObject)[];
parameters?: ParameterObject | (ParameterObject | ReferenceObject)[];
/** The request body applicable for this operation. The requestBody is fully supported in HTTP methods where the HTTP 1.1 specification [RFC7231] has explicitly defined semantics for request bodies. In other cases where the HTTP spec is vague (such as GET, HEAD and DELETE), requestBody is permitted but does not have well-defined semantics and SHOULD be avoided if possible. */
requestBody?: RequestBodyObject | ReferenceObject;
/** The list of possible responses as they are returned from executing this operation. */
Expand Down
6 changes: 5 additions & 1 deletion packages/openapi-typescript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import c from "ansi-colors";
import { isAbsolute } from "node:path";
import supportsColor from "supports-color";
import { fetch as unidiciFetch } from "undici";
import type { Fetch } from "./types.js";
import type { Fetch, ParameterObject, ReferenceObject } from "./types.js";

// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (!supportsColor.stdout || supportsColor.stdout.hasBasic === false) c.enabled = false;
Expand Down Expand Up @@ -322,3 +322,7 @@ export function getDefaultFetch(): Fetch {
}
return globalFetch;
}

export function getParametersArray(parameters: ParameterObject | (ParameterObject | ReferenceObject)[] = []): (ParameterObject | ReferenceObject)[] {
return Array.isArray(parameters) ? parameters : [parameters];
}
36 changes: 36 additions & 0 deletions packages/openapi-typescript/test/operation-object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ describe("Operation Object", () => {
};
};
};
}`);
});

it("handles non-array parameters", () => {
Copy link
Contributor

@drwpow drwpow Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great tests 🎉

const schema: OperationObject = {
parameters: {
in: "query",
name: "search",
schema: { type: "string" },
},
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: { type: "string" },
},
},
},
},
};
const generated = transformOperationObject(schema, options);
expect(generated).toBe(`{
parameters: {
query?: {
search?: string;
};
};
responses: {
/** @description OK */
200: {
content: {
"application/json": string;
};
};
};
}`);
});
});
24 changes: 23 additions & 1 deletion packages/openapi-typescript/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { comment, escObjKey, getSchemaObjectComment, parseRef, tsIntersectionOf, tsUnionOf } from "../src/utils.js";
import { ParameterObject, ReferenceObject } from "../src/types.js";
import { comment, escObjKey, getParametersArray, getSchemaObjectComment, parseRef, tsIntersectionOf, tsUnionOf } from "../src/utils.js";

describe("utils", () => {
describe("tsUnionOf", () => {
Expand Down Expand Up @@ -167,4 +168,25 @@ describe("utils", () => {
);
});
});

describe('getParametersArray', () => {
it('should return an empty array if no parameters are passed', () => {
expect(getParametersArray()).toEqual([]);
});

it('should return an array if a single parameter is passed', () => {
const parameter: ParameterObject = { name: 'test', in: 'query' };
expect(getParametersArray(parameter)).toEqual([parameter]);
});

it('should return an array if an array of parameters is passed', () => {
const parameters: ParameterObject[] = [{ name: 'test', in: 'query' }, { name: 'test2', in: 'query' }];
expect(getParametersArray(parameters)).toEqual(parameters);
});

it('should return an array if an array of references is passed', () => {
const references: ReferenceObject[] = [{ $ref: 'test' }, { $ref: 'test2' }];
expect(getParametersArray(references)).toEqual(references);
});
});
});