Skip to content

Commit

Permalink
Fix binary request & response openapi (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin committed Jan 5, 2022
1 parent 5713554 commit a6831d1
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/openapi3",
"comment": "**Fix** issue with @body body: bytes producing `type: string, format: bytes` instead of `type: string, format: binary` for requests and responses",
"type": "patch"
}
],
"packageName": "@cadl-lang/openapi3"
}
26 changes: 21 additions & 5 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,26 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
}
}

function isBinaryPayload(body: Type, contentType: string) {
return (
body.kind === "Model" &&
body.name === "bytes" &&
contentType !== "application/json" &&
contentType !== "text/plain"
);
}

function emitResponseObject(responseModel: Type, statusCode: string = "200") {
let contentType = "application/json";
let contentType: string = "application/json";
if (
responseModel.kind === "Model" &&
!responseModel.baseModel &&
responseModel.properties.size === 0
) {
const schema = mapCadlTypeToOpenAPI(responseModel);
const isBinary = isBinaryPayload(responseModel, contentType);
const schema = isBinary
? { type: "string", format: "binary" }
: mapCadlTypeToOpenAPI(responseModel);
if (schema) {
currentEndpoint.responses[statusCode] = {
description: getResponseDescription(responseModel, statusCode),
Expand Down Expand Up @@ -390,12 +402,15 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
}
}

contentEntry.schema = getSchemaOrRef(bodyModel);
const isBinary = isBinaryPayload(bodyModel, contentType);
contentEntry.schema = isBinary
? { type: "string", format: "binary" }
: getSchemaOrRef(bodyModel);

const response: any = {
description: getResponseDescription(responseModel, statusCode),
content: {
[contentType]: contentEntry,
[contentType ?? "application/json"]: contentEntry,
},
};
if (Object.keys(headers).length > 0) {
Expand Down Expand Up @@ -553,7 +568,6 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
}
const bodyParam = bodyParams[0];
const bodyType = bodyParam.type;
const bodySchema = getSchemaOrRef(bodyType);

const requestBody: any = {
description: getDoc(program, bodyParam),
Expand All @@ -567,6 +581,8 @@ function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) {
? getContentTypes(contentTypeParam)
: ["application/json"];
for (let contentType of contentTypes) {
const isBinary = isBinaryPayload(bodyType, contentType);
const bodySchema = isBinary ? { type: "string", format: "binary" } : getSchemaOrRef(bodyType);
const contentEntry: any = {
schema: bodySchema,
};
Expand Down
106 changes: 106 additions & 0 deletions packages/openapi3/test/test-openapi-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,112 @@ describe("openapi3: responses", () => {
"string"
);
});

describe("binary responses", () => {
it("bytes responses should default to application/json with byte format", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@get op read(): bytes;
}
`
);

const response = res.paths["/"].get.responses["200"];
ok(response);
ok(response.content);
strictEqual(response.content["application/json"].schema.type, "string");
strictEqual(response.content["application/json"].schema.format, "byte");
});

it("@body body: bytes responses default to application/json with bytes format", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@get op read(): {@body body: bytes};
}
`
);

const response = res.paths["/"].get.responses["200"];
ok(response);
ok(response.content);
strictEqual(response.content["application/json"].schema.type, "string");
strictEqual(response.content["application/json"].schema.format, "byte");
});

it("@header contentType text/plain should keep format to byte", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@get op read(): {@header contentType: "text/plain", @body body: bytes};
}
`
);

const response = res.paths["/"].get.responses["200"];
ok(response);
ok(response.content);
strictEqual(response.content["text/plain"].schema.type, "string");
strictEqual(response.content["text/plain"].schema.format, "byte");
});

it("@header contentType not json or text should set format to binary", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@get op read(): {@header contentType: "image/png", @body body: bytes};
}
`
);

const response = res.paths["/"].get.responses["200"];
ok(response);
ok(response.content);
strictEqual(response.content["image/png"].schema.type, "string");
strictEqual(response.content["image/png"].schema.format, "binary");
});
});
});

describe("openapi3: request", () => {
describe("binary request", () => {
it("bytes request should default to application/json byte", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@post op read(@body body: bytes): {};
}
`
);

const requestBody = res.paths["/"].post.requestBody;
ok(requestBody);
strictEqual(requestBody.content["application/json"].schema.type, "string");
strictEqual(requestBody.content["application/json"].schema.format, "byte");
});

it("bytes request should respect @header contentType and use binary format when not json or text", async () => {
const res = await openApiFor(
`
@route("/")
namespace root {
@post op read(@header contentType: "image/png", @body body: bytes): {};
}
`
);

const requestBody = res.paths["/"].post.requestBody;
ok(requestBody);
strictEqual(requestBody.content["image/png"].schema.type, "string");
strictEqual(requestBody.content["image/png"].schema.format, "binary");
});
});
});

describe("openapi3: extension decorator", () => {
Expand Down
33 changes: 33 additions & 0 deletions packages/samples/binary/binary.cadl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import "@cadl-lang/rest";

using Cadl.Http;

model HasBytes {
bytes: bytes;
}

model BytesBody<ContentType> {
@body body: bytes;
@header contentType: ContentType;
}

@route("/test")
namespace BytesMethod {
@route("base64")
@post
op jsonWithBase64(@body body: HasBytes): HasBytes;

@route("textPlain")
@post
op textPlain(...BytesBody<"text/plain">): BytesBody<"text/plain">;

@route("binaryFile")
@post
op genericBinaryFile(
...BytesBody<"application/octect-stream">
): BytesBody<"application/octect-stream">;

@route("imagePng")
@post
op image(...BytesBody<"image/png">): BytesBody<"image/png">;
}
1 change: 1 addition & 0 deletions packages/samples/binary/main.cadl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./binary.cadl";
140 changes: 140 additions & 0 deletions packages/samples/test/output/binary/openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{
"openapi": "3.0.0",
"info": {
"title": "(title)",
"version": "0000-00-00"
},
"tags": [],
"paths": {
"/test/base64": {
"post": {
"operationId": "BytesMethod_jsonWithBase64",
"parameters": [],
"responses": {
"200": {
"description": "A successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HasBytes"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HasBytes"
}
}
}
}
}
},
"/test/textPlain": {
"post": {
"operationId": "BytesMethod_textPlain",
"parameters": [],
"responses": {
"200": {
"description": "A successful response",
"content": {
"text/plain": {
"schema": {
"type": "string",
"format": "byte"
}
}
}
}
},
"requestBody": {
"content": {
"text/plain": {
"schema": {
"type": "string",
"format": "byte"
}
}
}
}
}
},
"/test/binaryFile": {
"post": {
"operationId": "BytesMethod_genericBinaryFile",
"parameters": [],
"responses": {
"200": {
"description": "A successful response",
"content": {
"application/octect-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
},
"requestBody": {
"content": {
"application/octect-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
},
"/test/imagePng": {
"post": {
"operationId": "BytesMethod_image",
"parameters": [],
"responses": {
"200": {
"description": "A successful response",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
},
"requestBody": {
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
},
"components": {
"schemas": {
"HasBytes": {
"type": "object",
"properties": {
"bytes": {
"type": "string",
"format": "byte"
}
},
"required": [
"bytes"
]
}
}
}
}

0 comments on commit a6831d1

Please sign in to comment.