Skip to content

Commit

Permalink
[HTTP] Don't allow multiple properties in a response to be annotated …
Browse files Browse the repository at this point in the history
…with `@statusCode` (#2454)

Fix #2347.

The fix was surprisingly less involved than I expected. 

**BREAKING CHANGE** since this will now fail instead of outputting
nonsense.
  • Loading branch information
tjprescott authored Sep 27, 2023
1 parent b9452d0 commit a096823
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/http",
"comment": "Emit error when multiple properties on a response model have the `@statusCode` decorator.",
"type": "none"
}
],
"packageName": "@typespec/http"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/openapi3",
"comment": "",
"type": "none"
}
],
"packageName": "@typespec/openapi3"
}
6 changes: 6 additions & 0 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Duplicate operation "${"operationName"}" routed at "${"verb"} ${"path"}".`,
},
},
"multiple-status-codes": {
severity: "error",
messages: {
default: "Multiple `@statusCode` decorators defined for this operation response.",
},
},
"status-code-invalid": {
severity: "error",
messages: {
Expand Down
13 changes: 11 additions & 2 deletions packages/http/src/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
isHeader,
isStatusCode,
} from "./decorators.js";
import { createDiagnostic } from "./lib.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
import { gatherMetadata, isApplicableMetadata, Visibility } from "./metadata.js";
import { HttpOperationResponse } from "./types.js";

Expand Down Expand Up @@ -131,9 +131,18 @@ function getResponseStatusCodes(
): string[] {
const codes: string[] = [];

let statusFound = false;
for (const prop of metadata) {
if (isStatusCode(program, prop)) {
codes.push(...getStatusCodes(program, prop));
if (statusFound) {
reportDiagnostic(program, {
code: "multiple-status-codes",
target: responseType,
});
}
statusFound = true;
const propCodes = getStatusCodes(program, prop);
codes.push(...propCodes);
}
}

Expand Down
42 changes: 42 additions & 0 deletions packages/http/test/http-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,48 @@ describe("http: decorators", () => {
]);
});

it("emits error if multiple properties are decorated with `@statusCode` in return type", async () => {
const diagnostics = await runner.diagnose(
`
model CreatedOrUpdatedResponse {
@statusCode ok: "200";
@statusCode created: "201";
}
model DateHeader {
@header date: utcDateTime;
}
model Key {
key: string;
}
@put op create(): CreatedOrUpdatedResponse & DateHeader & Key;
`
);
expectDiagnostics(diagnostics, [{ code: "@typespec/http/multiple-status-codes" }]);
});

it("emits error if multiple `@statusCode` decorators are composed together", async () => {
const diagnostics = await runner.diagnose(
`
model CustomUnauthorizedResponse {
@statusCode _: 401;
@body body: UnauthorizedResponse;
}
model Pet {
name: string;
}
model PetList {
@statusCode _: 200;
@body body: Pet[];
}
op list(): PetList | CustomUnauthorizedResponse;
`
);
expectDiagnostics(diagnostics, [{ code: "@typespec/http/multiple-status-codes" }]);
});

it("set the statusCode with @statusCode", async () => {
const { code } = await runner.compile(`
op test(): {
Expand Down
29 changes: 0 additions & 29 deletions packages/openapi3/test/return-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,35 +159,6 @@ describe("openapi3: return types", () => {
);
});

it("defines separate responses for each status code property in return type", async () => {
const res = await openApiFor(
`
model CreatedOrUpdatedResponse {
@statusCode ok: "200";
@statusCode created: "201";
}
model DateHeader {
@header date: utcDateTime;
}
model Key {
key: string;
}
@put op create(): CreatedOrUpdatedResponse & DateHeader & Key;
`
);
ok(res.paths["/"].put.responses["200"]);
ok(res.paths["/"].put.responses["201"]);
// Note: 200 and 201 response should be equal except for description
deepStrictEqual(
res.paths["/"].put.responses["200"].headers,
res.paths["/"].put.responses["201"].headers
);
deepStrictEqual(
res.paths["/"].put.responses["200"].content,
res.paths["/"].put.responses["201"].content
);
});

it("defines separate responses for each variant of a union return type", async () => {
const res = await openApiFor(
`
Expand Down

0 comments on commit a096823

Please sign in to comment.