Skip to content

Commit

Permalink
tsp-openapi3 - handle requestBodies $ref (#5893)
Browse files Browse the repository at this point in the history
Fixes #5485

---------

Co-authored-by: Christopher Radek <[email protected]>
  • Loading branch information
chrisradek and Christopher Radek authored Feb 6, 2025
1 parent 4d00495 commit ccb5c4f
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/openapi3"
---

Updates tsp-openapi3 to support $ref in requestBodies
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ function generateRequestBodyParameters(
).join(" | ");

if (body) {
definitions.push(`@bodyRoot body: ${body}`);
let doc = "";
if (requestBodies[0].doc) {
doc = generateDocs(requestBodies[0].doc);
}
definitions.push(`${doc}@body body: ${body}`);
}

return definitions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TypeSpecOperationParameter,
TypeSpecRequestBody,
} from "../interfaces.js";
import { Context } from "../utils/context.js";
import { getExtensions, getParameterDecorators } from "../utils/decorators.js";
import { getScopeAndName } from "../utils/get-scope-and-name.js";
import { supportedHttpMethods } from "../utils/supported-http-methods.js";
Expand All @@ -20,7 +21,10 @@ import { supportedHttpMethods } from "../utils/supported-http-methods.js";
* @param paths
* @returns
*/
export function transformPaths(paths: Record<string, OpenAPI3PathItem>): TypeSpecOperation[] {
export function transformPaths(
paths: Record<string, OpenAPI3PathItem>,
context: Context,
): TypeSpecOperation[] {
const operations: TypeSpecOperation[] = [];

for (const route of Object.keys(paths)) {
Expand Down Expand Up @@ -51,7 +55,7 @@ export function transformPaths(paths: Record<string, OpenAPI3PathItem>): TypeSpe
parameters: dedupeParameters([...routeParameters, ...parameters]),
doc: operation.description,
operationId: operation.operationId,
requestBodies: transformRequestBodies(operation.requestBody),
requestBodies: transformRequestBodies(operation.requestBody, context),
responses: operationResponses,
tags: tags,
});
Expand Down Expand Up @@ -102,7 +106,20 @@ function transformOperationParameter(
};
}

function transformRequestBodies(requestBodies?: OpenAPI3RequestBody): TypeSpecRequestBody[] {
function transformRequestBodies(
requestBodies: Refable<OpenAPI3RequestBody> | undefined,
context: Context,
): TypeSpecRequestBody[] {
if (!requestBodies) {
return [];
}

const description = requestBodies.description;

if ("$ref" in requestBodies) {
requestBodies = context.getByRef<OpenAPI3RequestBody>(requestBodies.$ref);
}

if (!requestBodies) {
return [];
}
Expand All @@ -113,7 +130,7 @@ function transformRequestBodies(requestBodies?: OpenAPI3RequestBody): TypeSpecRe
typespecBodies.push({
contentType,
isOptional: !requestBodies.required,
doc: requestBodies.description,
doc: description ?? requestBodies.description,
encoding: contentBody.encoding,
schema: contentBody.schema,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { transformServiceInfo } from "./transform-service-info.js";
export function transform(context: Context): TypeSpecProgram {
const openapi = context.openApi3Doc;
const models = collectDataTypes(context);
const operations = transformPaths(openapi.paths);
const operations = transformPaths(openapi.paths, context);

return {
serviceInfo: transformServiceInfo(openapi.info),
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi3/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ export type OpenAPI3Operation = Extensions & {
responses?: any;
tags?: string[];
operationId?: string;
requestBody?: OpenAPI3RequestBody;
requestBody?: Refable<OpenAPI3RequestBody>;
parameters: Refable<OpenAPI3Parameter>[];
deprecated?: boolean;
security?: Record<string, string[]>[];
Expand Down
11 changes: 8 additions & 3 deletions packages/openapi3/test/examples.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { OpenAPI3Document } from "../src/types.js";
import { OpenAPI3Document, OpenAPI3RequestBody } from "../src/types.js";
import { openApiFor } from "./test-host.js";
import { worksFor } from "./works-for.js";

Expand Down Expand Up @@ -67,7 +67,9 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
`,
);
expect(res.paths["/"].post?.requestBody?.content["application/json"].example).toEqual({
expect(
(res.paths["/"].post?.requestBody as OpenAPI3RequestBody).content["application/json"].example,
).toEqual({
name: "Fluffy",
age: 2,
});
Expand All @@ -87,7 +89,10 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
`,
);
expect(res.paths["/"].post?.requestBody?.content["application/json"].examples).toEqual({
expect(
(res.paths["/"].post?.requestBody as OpenAPI3RequestBody).content["application/json"]
.examples,
).toEqual({
MyExample: {
summary: "MyExample",
value: {
Expand Down
12 changes: 7 additions & 5 deletions packages/openapi3/test/overloads.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { deepStrictEqual, ok, strictEqual } from "assert";
import { beforeEach, describe, it } from "vitest";
import { OpenAPI3Document } from "../src/types.js";
import { OpenAPI3Document, OpenAPI3RequestBody } from "../src/types.js";
import { worksFor } from "./works-for.js";

worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
Expand All @@ -24,7 +24,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
const operation = res.paths["/upload"].post;
ok(operation);
strictEqual(operation.operationId, "upload");
deepStrictEqual(Object.keys(operation.requestBody!.content), [
deepStrictEqual(Object.keys((operation.requestBody as OpenAPI3RequestBody).content), [
"text/plain",
"application/octet-stream",
]);
Expand Down Expand Up @@ -56,8 +56,10 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
strictEqual(stringOperation.operationId, "uploadString");
strictEqual(bytesOperation.operationId, "uploadBytes");

deepStrictEqual(Object.keys(stringOperation.requestBody!.content), ["text/plain"]);
deepStrictEqual(Object.keys(bytesOperation.requestBody!.content), [
deepStrictEqual(Object.keys((stringOperation.requestBody as OpenAPI3RequestBody).content), [
"text/plain",
]);
deepStrictEqual(Object.keys((bytesOperation.requestBody as OpenAPI3RequestBody).content), [
"application/octet-stream",
]);
});
Expand All @@ -67,7 +69,7 @@ worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
ok(baseOperation);
strictEqual(baseOperation.operationId, "upload");

deepStrictEqual(Object.keys(baseOperation.requestBody!.content), [
deepStrictEqual(Object.keys((baseOperation.requestBody as OpenAPI3RequestBody).content), [
"text/plain",
"application/octet-stream",
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ model `Escaped-Model` {
@route("/{escaped-property}") @get op `get-thing`(
@query(#{ explode: true }) `weird@param`?: `Foo-Bar`,
...Parameters.`Escaped-Model`.`escaped-property`,
@bodyRoot body: `Escaped-Model`,
@body body: `Escaped-Model`,
): GeneratedHelpers.DefaultResponse<Description = "Success">;

namespace Parameters {
Expand Down
6 changes: 3 additions & 3 deletions packages/openapi3/test/tsp-openapi3/output/nested/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ namespace SubA {
model Thing {
name: string;
}
@route("/sub/a/subsub") @post op doSomething(@bodyRoot body: SubA.SubSubA.Thing): Body<string>;
@route("/sub/a/subsub") @post op doSomething(@body body: SubA.SubSubA.Thing): Body<string>;
}
}

namespace SubB {
model Thing {
id: int64;
}
@route("/sub/b") @post op doSomething(@bodyRoot body: SubB.Thing): Body<string>;
@route("/sub/b") @post op doSomething(@body body: SubB.Thing): Body<string>;
}

namespace SubC {
@route("/") @post op anotherOp(
@bodyRoot body: {
@body body: {
thing: SubA.Thing;
thing2: SubA.Thing;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ model Pet {
}

@route("/any") @post op putAny(
@bodyRoot body: {
@body body: {
pet: Dog | Cat;
},
): NoContentResponse;

@route("/one") @post op putOne(
@bodyRoot body: {
@body body: {
@oneOf
pet: Dog | Cat;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ model Thing {

@route("/thing/{name}") @put op Operations_putThing(
...Parameters.NameParameter,
@bodyRoot body: Thing,
@body body: Thing,
): Thing;

namespace Parameters {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ model ApiResponse {
@summary("Add a new pet to the store")
op addPet(
@header contentType: "application/json" | "application/xml" | "application/x-www-form-urlencoded",
@bodyRoot body: Pet,

/**
* Create a new pet in the store
*/
@body body: Pet,
): {
@header contentType: "application/xml";
@body body: Pet;
Expand All @@ -133,7 +137,11 @@ op addPet(
@summary("Update an existing pet")
op updatePet(
@header contentType: "application/json" | "application/xml" | "application/x-www-form-urlencoded",
@bodyRoot body: Pet,

/**
* Update an existent pet in the store
*/
@body body: Pet,
):
| {
@header contentType: "application/xml";
Expand Down Expand Up @@ -253,7 +261,7 @@ op uploadFile(
@query(#{ explode: true }) additionalMetadata?: string,

@header contentType: "application/octet-stream",
@bodyRoot body: bytes,
@body body: bytes,
): ApiResponse;

/**
Expand All @@ -276,7 +284,7 @@ op getInventory(): Body<Record<int32>>;
@summary("Place an order for a pet")
op placeOrder(
@header contentType: "application/json" | "application/xml" | "application/x-www-form-urlencoded",
@bodyRoot body: Order,
@body body: Order,
): Order | {
@statusCode statusCode: 405;
};
Expand Down Expand Up @@ -327,7 +335,11 @@ op getOrderById(
@summary("Create user")
op createUser(
@header contentType: "application/json" | "application/xml" | "application/x-www-form-urlencoded",
@bodyRoot body: User,

/**
* Created user object
*/
@body body: User,
): GeneratedHelpers.DefaultResponse<
Description = "successful operation",
Body = User
Expand All @@ -347,7 +359,7 @@ op createUser(
@route("/user/createWithList")
@post
@summary("Creates list of users with given input array")
op createUsersWithListInput(@bodyRoot body: User[]): {
op createUsersWithListInput(@body body: User[]): {
@header contentType: "application/xml";
@body body: User;
} | User | GeneratedHelpers.DefaultResponse<Description = "successful operation">;
Expand Down Expand Up @@ -446,7 +458,11 @@ op updateUser(
@path username: string,

@header contentType: "application/json" | "application/xml" | "application/x-www-form-urlencoded",
@bodyRoot body: User,

/**
* Update an existent user in the store
*/
@body body: User,
): GeneratedHelpers.DefaultResponse<Description = "successful operation">;

namespace GeneratedHelpers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ op Widgets_list(): Body<Widget[]> | GeneratedHelpers.DefaultResponse<
@tag("Widgets")
@route("/widgets")
@post
op Widgets_create(@bodyRoot body: WidgetCreate): Widget | GeneratedHelpers.DefaultResponse<
op Widgets_create(@body body: WidgetCreate): Widget | GeneratedHelpers.DefaultResponse<
Description = "An unexpected error response.",
Body = Error
>;
Expand All @@ -71,7 +71,7 @@ op Widgets_read(@path id: string): Widget | GeneratedHelpers.DefaultResponse<
@patch
op Widgets_update(
...Parameters.Widget.id,
@bodyRoot body: WidgetUpdate,
@body body: WidgetUpdate,
): Widget | GeneratedHelpers.DefaultResponse<
Description = "An unexpected error response.",
Body = Error
Expand Down
Loading

0 comments on commit ccb5c4f

Please sign in to comment.