Skip to content

Commit 8b34a06

Browse files
authored
Fix missing parameters in operation object (#1090)
1 parent 667e3e9 commit 8b34a06

File tree

4 files changed

+134
-13
lines changed

4 files changed

+134
-13
lines changed

CONTRIBUTING.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ This will compile the code as you change automatically.
4040

4141
**Please fill out the template!** It’s a very lightweight template 🙂.
4242

43-
### TDD
43+
### Use Test-driven Development!
4444

45-
This library is a great usecase for [test-driven development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development). If you’re new to this, the basic workflow is:
45+
Contributing to this library is hard-bordering-on-impossible without a [test-driven development (TDD)](https://en.wikipedia.org/wiki/Test-driven_development) strategy. If you’re new to this, the basic workflow is:
4646

4747
1. First, write a [test](#testing) that fully outlines what you’d _like_ the output to be.
48-
2. Make sure this test **fails** when you run `npm test` (yes, _fails!_)
48+
2. Next, make sure this test **fails** when you run `npm test` (yes, _fails!_)
4949
3. Then, make changes to `src/` until the tests pass.
5050

51-
_Code generation is hard!_ And for that reason, starting with a very clear expectation of your end-goal can make working easier.
51+
Reasoning about code generation can be quite difficult until you “invert your thinking” and approach it output-first. Adopting TDD can turn very unclear/abstract problems into concrete ones with clear steps to resolution.
52+
53+
✨ When starting any task, **write a failing test first!**
5254

5355
#### Updating snapshot tests
5456

src/transform/path-item-object.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,32 @@ export default function transformPathItemObject(
2424
if (!operationObject) continue;
2525
const c = getSchemaObjectComment(operationObject, indentLv);
2626
if (c) output.push(indent(c, indentLv));
27+
28+
// fold top-level PathItem parameters into method-level, with the latter overriding the former
29+
const keyedParameters: Record<string, ParameterObject | ReferenceObject> = {};
30+
if (!("$ref" in operationObject)) {
31+
// important: OperationObject parameters come last, and will override any conflicts with PathItem parameters
32+
for (const parameter of [...(pathItem.parameters ?? []), ...(operationObject.parameters ?? [])]) {
33+
// note: the actual key doesn’t matter here, as long as it can match between PathItem and OperationObject
34+
keyedParameters["$ref" in parameter ? parameter.$ref : parameter.name] = parameter;
35+
}
36+
}
37+
2738
if ("$ref" in operationObject) {
2839
output.push(indent(`${method}: ${operationObject.$ref}`, indentLv));
2940
}
3041
// if operationId exists, move into an `operations` export and pass the reference in here
3142
else if (operationObject.operationId) {
32-
const operationType = transformOperationObject(operationObject, { path, ctx: { ...ctx, indentLv: 1 } });
43+
const operationType = transformOperationObject(
44+
{ ...operationObject, parameters: Object.values(keyedParameters) },
45+
{ path, ctx: { ...ctx, indentLv: 1 } }
46+
);
3347
ctx.operations[operationObject.operationId] = {
3448
operationType,
3549
comment: getSchemaObjectComment(operationObject, 1),
3650
};
3751
output.push(indent(`${method}: operations[${escStr(operationObject.operationId)}];`, indentLv));
3852
} else {
39-
// fold top-level PathItem parameters into method-level, with the latter overriding the former
40-
const keyedParameters: Record<string, ParameterObject | ReferenceObject> = {};
41-
// important: OperationObject parameters come last, and will override any conflicts with PathItem parameters
42-
for (const parameter of [...(pathItem.parameters ?? []), ...(operationObject.parameters ?? [])]) {
43-
// note: the actual key doesn’t matter here, as long as it can match between PathItem and OperationObject
44-
keyedParameters["$ref" in parameter ? parameter.$ref : parameter.name] = parameter;
45-
}
4653
const operationType = transformOperationObject(
4754
{ ...operationObject, parameters: Object.values(keyedParameters) },
4855
{ path, ctx: { ...ctx, indentLv } }

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export interface RequestBodyObject extends Extensable {
276276
*/
277277
export interface MediaTypeObject extends Extensable {
278278
/** The schema defining the content of the request, response, or parameter. */
279-
schema?: SchemaObject;
279+
schema?: SchemaObject | ReferenceObject;
280280
/** Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. */
281281
example?: any;
282282
/** Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema. */

test/index.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe("openapiTS", () => {
3333
});
3434

3535
describe("3.0", () => {
36+
/** test that custom $refs are ignored rather than throw errors */
3637
test("custom properties", async () => {
3738
const generated = await openapiTS({
3839
openapi: "3.0",
@@ -74,6 +75,7 @@ export type operations = Record<string, never>;
7475
`);
7576
});
7677

78+
/** test that component examples aren’t parsed (they may be invalid / pseudocode) */
7779
test("components.examples are skipped", async () => {
7880
const generated = await openapiTS({
7981
openapi: "3.0",
@@ -124,6 +126,7 @@ export type operations = Record<string, never>;
124126
`);
125127
});
126128

129+
/** test that $ref’d parameters make it into the paths object correctly */
127130
test("parameter $refs", async () => {
128131
const generated = await openapiTS(new URL("./fixtures/parameters-test.yaml", import.meta.url));
129132
expect(generated).toBe(`${BOILERPLATE}
@@ -177,6 +180,115 @@ export type operations = Record<string, never>;
177180
`);
178181
});
179182

183+
/** test that $ref’d parameters, operation parameters, and method parameters all make it into the operation object correctly */
184+
test("operations keep all parameters", async () => {
185+
const schema: OpenAPI3 = {
186+
openapi: "3.0",
187+
info: { title: "Test", version: "1.0" },
188+
paths: {
189+
"/post/{id}": {
190+
get: {
191+
operationId: "getPost",
192+
parameters: [
193+
{ name: "format", in: "query", schema: { type: "string" } },
194+
{ $ref: "#/components/parameters/post_id" },
195+
],
196+
responses: {
197+
200: {
198+
description: "OK",
199+
content: {
200+
"application/json": { schema: { $ref: "#/components/schemas/Post" } },
201+
},
202+
},
203+
},
204+
},
205+
parameters: [{ name: "revision", in: "query", schema: { type: "number" } }],
206+
},
207+
},
208+
components: {
209+
schemas: {
210+
Post: {
211+
type: "object",
212+
properties: {
213+
id: { type: "number" },
214+
title: { type: "string" },
215+
body: { type: "string" },
216+
published_at: { type: "number" },
217+
},
218+
required: ["id", "title", "body"],
219+
},
220+
},
221+
parameters: {
222+
post_id: {
223+
name: "post_id",
224+
in: "path",
225+
schema: { type: "string" },
226+
required: true,
227+
},
228+
},
229+
},
230+
};
231+
const generated = await openapiTS(schema);
232+
expect(generated).toBe(`${BOILERPLATE}
233+
export interface paths {
234+
"/post/{id}": {
235+
get: operations["getPost"];
236+
parameters: {
237+
query: {
238+
revision?: number;
239+
};
240+
};
241+
};
242+
}
243+
244+
export type webhooks = Record<string, never>;
245+
246+
export interface components {
247+
schemas: {
248+
Post: {
249+
id: number;
250+
title: string;
251+
body: string;
252+
published_at?: number;
253+
};
254+
};
255+
responses: never;
256+
parameters: {
257+
post_id: string;
258+
};
259+
requestBodies: never;
260+
headers: never;
261+
pathItems: never;
262+
}
263+
264+
export type external = Record<string, never>;
265+
266+
export interface operations {
267+
268+
getPost: {
269+
parameters: {
270+
query: {
271+
revision?: number;
272+
format?: string;
273+
};
274+
path: {
275+
post_id: components["parameters"]["post_id"];
276+
};
277+
};
278+
responses: {
279+
/** @description OK */
280+
200: {
281+
content: {
282+
"application/json": components["schemas"]["Post"];
283+
};
284+
};
285+
};
286+
};
287+
}
288+
`);
289+
});
290+
291+
/** test that remote $refs are loaded correctly */
180292
test("remote $refs", async () => {
181293
const generated = await openapiTS(new URL("./fixtures/remote-ref-test.yaml", import.meta.url));
182294
expect(generated).toBe(`${BOILERPLATE}

0 commit comments

Comments
 (0)