Skip to content

Commit

Permalink
Support single operation parameter (#1535)
Browse files Browse the repository at this point in the history
* support single operation parameter

* changeset
  • Loading branch information
tvanier committed Feb 15, 2024
1 parent 0b50c1e commit 48ca069
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 9 deletions.
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", () => {
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);
});
});
});

0 comments on commit 48ca069

Please sign in to comment.