diff --git a/docs/post-response-codes.md b/docs/post-response-codes.md index 7d306fd76..c1d129e85 100644 --- a/docs/post-response-codes.md +++ b/docs/post-response-codes.md @@ -10,18 +10,13 @@ ARM OpenAPI (swagger) specs ## Related ARM Guideline Code -- RPC-Async-V1-11, RPC-Async-V1-14 - -## Output Message - -Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes. -Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes. +- RPC-Async-V1-11, RPC-Async-V1-14, RPC-POST-V1-02, RPC-POST-V1-03 ## Description -Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes. -Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes. -202 response for a LRO POST operation must not have a response schema specified. +Synchronous POST operations must only use 200 with a default response when a response schema is required, or 204 with a default response when no schema is needed. No other response codes are allowed. + +Long-running POST (LRO) operations must initially return 202 with a default response and no schema. The final response must be 200 with a schema if one is required, or 204 with no schema if not. No other response codes are permitted. ## How to fix the violation @@ -29,7 +24,7 @@ Synchronous POST operations must have one of the following combinations of respo For Long-running POST operations: 1. Add responses with 202 and default return codes. -2. Add 200 response code if only if the final response is intended to have a schema. +2. Add 200 response code if only if the final response is intended to have a schema if not add 204 response code. 3. Ensure no other response codes are specified. 4. Make sure to define "x-ms-long-running-operation". 5. 202 response for a LRO POST operation must not have a response schema specified. @@ -62,9 +57,6 @@ The following would be a valid SYNC POST: "responses": { "204": { "description": "No Content", - "schema": { - "$ref": "#/definitions/FooResource" - } }, "default": { "description": "Error response describing why the operation failed.", @@ -107,6 +99,9 @@ The following would be a valid ASYNC POST: "202": { "description": "Operation accepted", }, + "204": { + "description": "No Content", + }, "default": { "description": "Error response describing why the operation failed.", "schema": { diff --git a/docs/rules.md b/docs/rules.md index c3af0afc9..89c357a49 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -938,9 +938,9 @@ Please refer to [post-operation-id-contains-url-verb.md](./post-operation-id-con ### PostResponseCodes -Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes. -Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes. -202 response for a LRO POST operation must not have a response schema specified. +Synchronous POST operations must only use 200 with a default response when a response schema is required, or 204 with a default response when no schema is needed. No other response codes are allowed. + +Long-running POST (LRO) operations must initially return 202 with a default response and no schema. The final response must be 200 with a schema if one is required, or 204 with no schema if not. No other response codes are permitted. Please refer to [post-response-codes.md](./post-response-codes.md) for details. diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index b60651366..ee79f1248 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -2298,14 +2298,19 @@ const pathSegmentCasing = (apiPaths, _opts, paths) => { const SYNC_POST_RESPONSES_OK = ["200", "default"]; const SYNC_POST_RESPONSES_NO_CONTENT = ["204", "default"]; -const LR_POST_RESPONSES_WITH_FINAL_SCHEMA = ["200", "202", "default"]; -const LR_POST_RESPONSES_NO_FINAL_SCHEMA = ["202", "default"]; -const SYNC_ERROR = "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes."; -const LR_ERROR = "Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes."; -const LR_NO_SCHEMA_ERROR = "200 return code does not have a schema specified. LRO POST must have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified."; +const LR_POST_RESPONSES_OK = ["202", "200", "default"]; +const LR_POST_RESPONSES_NO_CONTENT = ["202", "204", "default"]; +const HTTP_STATUS_CODE_OK = "200"; +const HTTP_STATUS_CODE_ACCEPTED = "202"; +const HTTP_STATUS_CODE_NO_CONTENT = "204"; +const SYNC_ERROR = "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. No other response codes are permitted."; +const LR_ERROR = "Long-running POST operations must initially return 202 with a default response and no schema. The final response must be 200 with a schema if one is required, or 204 with no schema if not. No other response codes are permitted."; +const LR_NO_SCHEMA_ERROR_OK = "200 return code does not have a schema specified. LRO POST must have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified."; +const LR_SCHEMA_ERROR_ACCEPTED = "202 response for a LRO POST operation must not have a response schema specified."; +const LR_SCHEMA_ERROR_NO_CONTENT = "204 response for a Sync/LRO POST operation must not have a response schema specified."; const EmptyResponse_ERROR$1 = "POST operation response codes must be non-empty. Synchronous POST operation must have response codes 200 and default or 204 and default. LRO POST operations must have response codes 202 and default. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified."; const PostResponseCodes = (postOp, _opts, ctx) => { - var _a, _b, _c; + var _a, _b, _c, _d; if (postOp === null || typeof postOp !== "object") { return []; } @@ -2319,12 +2324,10 @@ const PostResponseCodes = (postOp, _opts, ctx) => { }); return errors; } - const isAsyncOperation = postOp.responses["202"] || + const isAsyncOperation = postOp.responses[HTTP_STATUS_CODE_ACCEPTED] || (postOp["x-ms-long-running-operation"] && postOp["x-ms-long-running-operation"] === true) || postOp["x-ms-long-running-operation-options"]; if (isAsyncOperation) { - let wrongResponseCodes = false; - let okResponseCodeNoSchema = false; if (!postOp["x-ms-long-running-operation"] || postOp["x-ms-long-running-operation"] !== true) { errors.push({ message: "An async POST operation must set '\"x-ms-long-running-operation\" : true'.", @@ -2332,48 +2335,47 @@ const PostResponseCodes = (postOp, _opts, ctx) => { }); return errors; } - if (responses.length === LR_POST_RESPONSES_WITH_FINAL_SCHEMA.length) { - if (!LR_POST_RESPONSES_WITH_FINAL_SCHEMA.every((value) => responses.includes(value))) { - wrongResponseCodes = true; - } - else if (!((_b = postOp.responses["200"]) === null || _b === void 0 ? void 0 : _b.schema)) { - okResponseCodeNoSchema = true; - } - } - else if (responses.length !== LR_POST_RESPONSES_NO_FINAL_SCHEMA.length || - !LR_POST_RESPONSES_NO_FINAL_SCHEMA.every((value) => responses.includes(value))) { - wrongResponseCodes = true; - } - if ((_c = postOp.responses["202"]) === null || _c === void 0 ? void 0 : _c.schema) { - errors.push({ - message: "202 response for a LRO POST operation must not have a response schema specified.", - path: path, - }); - } - if (wrongResponseCodes) { + const respSet = new Set(responses); + const setEquals = (target) => target.length === respSet.size && target.every((v) => respSet.has(v)); + const matchesOk = setEquals(LR_POST_RESPONSES_OK); + const matchesNoContent = setEquals(LR_POST_RESPONSES_NO_CONTENT); + if (!(matchesOk || matchesNoContent)) { errors.push({ message: LR_ERROR, path: path, }); } - else if (okResponseCodeNoSchema) { - errors.push({ - message: LR_NO_SCHEMA_ERROR, - path: path, - }); - } - return errors; } else { - if (responses.length !== SYNC_POST_RESPONSES_OK.length || - (!SYNC_POST_RESPONSES_OK.every((value) => responses.includes(value)) && - !SYNC_POST_RESPONSES_NO_CONTENT.every((value) => responses.includes(value)))) { + const responseSet = new Set(responses); + const setEquals = (target) => target.length === responseSet.size && target.every((v) => responseSet.has(v)); + const matchesOk = setEquals(SYNC_POST_RESPONSES_OK); + const matchesNoContent = setEquals(SYNC_POST_RESPONSES_NO_CONTENT); + if (!(matchesOk || matchesNoContent)) { errors.push({ message: SYNC_ERROR, path: path, }); } } + if (postOp.responses[HTTP_STATUS_CODE_OK] && !((_b = postOp.responses[HTTP_STATUS_CODE_OK]) === null || _b === void 0 ? void 0 : _b.schema)) { + errors.push({ + message: LR_NO_SCHEMA_ERROR_OK, + path: path, + }); + } + if ((_c = postOp.responses[HTTP_STATUS_CODE_ACCEPTED]) === null || _c === void 0 ? void 0 : _c.schema) { + errors.push({ + message: LR_SCHEMA_ERROR_ACCEPTED, + path: path, + }); + } + if ((_d = postOp.responses[HTTP_STATUS_CODE_NO_CONTENT]) === null || _d === void 0 ? void 0 : _d.schema) { + errors.push({ + message: LR_SCHEMA_ERROR_NO_CONTENT, + path: path, + }); + } return errors; }; @@ -3333,8 +3335,8 @@ const ruleset = { }, }, PostResponseCodes: { - rpcGuidelineCode: "RPC-Async-V1-11, RPC-Async-V1-14", - description: "Synchronous POST must have either 200 or 204 return codes and LRO POST must have 202 return code. LRO POST should also have a 200 return code only if the final response is intended to have a schema", + rpcGuidelineCode: "RPC-Async-V1-11, RPC-Async-V1-14, RPC-POST-V1-02, RPC-POST-V1-03", + description: "Synchronous POST must return 200 when a response body is required or 204 when no body is needed; LRO POST must initially return 202, with the final response returning 200 if a body is expected or 204 if not.", severity: "error", disableForTypeSpecDataPlane: true, disableForTypeSpecDataPlaneReason: "Covered by TSP's '@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes' rule.", diff --git a/packages/rulesets/src/spectral/az-arm.ts b/packages/rulesets/src/spectral/az-arm.ts index 1eb96b752..21f3d291d 100644 --- a/packages/rulesets/src/spectral/az-arm.ts +++ b/packages/rulesets/src/spectral/az-arm.ts @@ -178,9 +178,8 @@ const ruleset: any = { // RPC Code: RPC-Async-V1-11, RPC-Async-V1-14 PostResponseCodes: { - rpcGuidelineCode: "RPC-Async-V1-11, RPC-Async-V1-14", - description: - "Synchronous POST must have either 200 or 204 return codes and LRO POST must have 202 return code. LRO POST should also have a 200 return code only if the final response is intended to have a schema", + rpcGuidelineCode: "RPC-Async-V1-11, RPC-Async-V1-14, RPC-POST-V1-02, RPC-POST-V1-03", + description: "Synchronous POST must return 200 when a response body is required or 204 when no body is needed; LRO POST must initially return 202, with the final response returning 200 if a body is expected or 204 if not.", severity: "error", disableForTypeSpecDataPlane: true, disableForTypeSpecDataPlaneReason: diff --git a/packages/rulesets/src/spectral/functions/post-response-codes.ts b/packages/rulesets/src/spectral/functions/post-response-codes.ts index eae39695e..94e9a7bc3 100644 --- a/packages/rulesets/src/spectral/functions/post-response-codes.ts +++ b/packages/rulesets/src/spectral/functions/post-response-codes.ts @@ -1,17 +1,21 @@ -// Synchronous POST must have 200 & 204 return codes and LRO POST must have 200 & 202 return codes. -// RPC Code: RPC-Async-V1-11 +// Synchronous POST must have 200 & 204 return codes and LRO POST must have 202 & 200 or 202 & 204 or 202, 200 & 204 return codes. const SYNC_POST_RESPONSES_OK = ["200", "default"] const SYNC_POST_RESPONSES_NO_CONTENT = ["204", "default"] -const LR_POST_RESPONSES_WITH_FINAL_SCHEMA = ["200", "202", "default"] -const LR_POST_RESPONSES_NO_FINAL_SCHEMA = ["202", "default"] +const LR_POST_RESPONSES_OK = ["202", "200", "default"] +const LR_POST_RESPONSES_NO_CONTENT = ["202", "204", "default"] +const HTTP_STATUS_CODE_OK = "200" +const HTTP_STATUS_CODE_ACCEPTED = "202" +const HTTP_STATUS_CODE_NO_CONTENT = "204" const SYNC_ERROR = - "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes." + "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. No other response codes are permitted." const LR_ERROR = - "Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes." -const LR_NO_SCHEMA_ERROR = + "Long-running POST operations must initially return 202 with a default response and no schema. The final response must be 200 with a schema if one is required, or 204 with no schema if not. No other response codes are permitted." +const LR_NO_SCHEMA_ERROR_OK = "200 return code does not have a schema specified. LRO POST must have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified." +const LR_SCHEMA_ERROR_ACCEPTED = "202 response for a LRO POST operation must not have a response schema specified." +const LR_SCHEMA_ERROR_NO_CONTENT = "204 response for a Sync/LRO POST operation must not have a response schema specified." const EmptyResponse_ERROR = "POST operation response codes must be non-empty. Synchronous POST operation must have response codes 200 and default or 204 and default. LRO POST operations must have response codes 202 and default. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified." @@ -33,14 +37,11 @@ export const PostResponseCodes = (postOp: any, _opts: any, ctx: any) => { } const isAsyncOperation = - postOp.responses["202"] || + postOp.responses[HTTP_STATUS_CODE_ACCEPTED] || (postOp["x-ms-long-running-operation"] && postOp["x-ms-long-running-operation"] === true) || postOp["x-ms-long-running-operation-options"] if (isAsyncOperation) { - let wrongResponseCodes = false - let okResponseCodeNoSchema = false - if (!postOp["x-ms-long-running-operation"] || postOp["x-ms-long-running-operation"] !== true) { errors.push({ message: "An async POST operation must set '\"x-ms-long-running-operation\" : true'.", @@ -49,45 +50,25 @@ export const PostResponseCodes = (postOp: any, _opts: any, ctx: any) => { return errors } - if (responses.length === LR_POST_RESPONSES_WITH_FINAL_SCHEMA.length) { - if (!LR_POST_RESPONSES_WITH_FINAL_SCHEMA.every((value) => responses.includes(value))) { - wrongResponseCodes = true - } else if (!postOp.responses["200"]?.schema) { - okResponseCodeNoSchema = true - } - } else if ( - responses.length !== LR_POST_RESPONSES_NO_FINAL_SCHEMA.length || - !LR_POST_RESPONSES_NO_FINAL_SCHEMA.every((value) => responses.includes(value)) - ) { - wrongResponseCodes = true - } - - if (postOp.responses["202"]?.schema) { - errors.push({ - message: "202 response for a LRO POST operation must not have a response schema specified.", - path: path, - }) - } + const respSet = new Set(responses) + const setEquals = (target: string[]) => target.length === respSet.size && target.every((v) => respSet.has(v)) + const matchesOk = setEquals(LR_POST_RESPONSES_OK) + const matchesNoContent = setEquals(LR_POST_RESPONSES_NO_CONTENT) - if (wrongResponseCodes) { + if (!(matchesOk || matchesNoContent)) { errors.push({ message: LR_ERROR, path: path, }) - } else if (okResponseCodeNoSchema) { - errors.push({ - message: LR_NO_SCHEMA_ERROR, - path: path, - }) } - - return errors } else { - if ( - responses.length !== SYNC_POST_RESPONSES_OK.length || - (!SYNC_POST_RESPONSES_OK.every((value) => responses.includes(value)) && - !SYNC_POST_RESPONSES_NO_CONTENT.every((value) => responses.includes(value))) - ) { + const responseSet = new Set(responses) + const setEquals = (target: string[]) => target.length === responseSet.size && target.every((v) => responseSet.has(v)) + + const matchesOk = setEquals(SYNC_POST_RESPONSES_OK) + const matchesNoContent = setEquals(SYNC_POST_RESPONSES_NO_CONTENT) + + if (!(matchesOk || matchesNoContent)) { errors.push({ message: SYNC_ERROR, path: path, @@ -95,5 +76,26 @@ export const PostResponseCodes = (postOp: any, _opts: any, ctx: any) => { } } + if (postOp.responses[HTTP_STATUS_CODE_OK] && !postOp.responses[HTTP_STATUS_CODE_OK]?.schema) { + errors.push({ + message: LR_NO_SCHEMA_ERROR_OK, + path: path, + }) + } + + if (postOp.responses[HTTP_STATUS_CODE_ACCEPTED]?.schema) { + errors.push({ + message: LR_SCHEMA_ERROR_ACCEPTED, + path: path, + }) + } + + if (postOp.responses[HTTP_STATUS_CODE_NO_CONTENT]?.schema) { + errors.push({ + message: LR_SCHEMA_ERROR_NO_CONTENT, + path: path, + }) + } + return errors } diff --git a/packages/rulesets/src/spectral/test/post-response-codes.test.ts b/packages/rulesets/src/spectral/test/post-response-codes.test.ts index a2afa45f1..fb36b50a8 100644 --- a/packages/rulesets/src/spectral/test/post-response-codes.test.ts +++ b/packages/rulesets/src/spectral/test/post-response-codes.test.ts @@ -2,12 +2,13 @@ import { Spectral } from "@stoplight/spectral-core" import linterForRule from "./utils" const SYNC_ERROR = - "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. They also must not have other response codes." + "Synchronous POST operations must have one of the following combinations of responses - 200 and default ; 204 and default. No other response codes are permitted." const LR_ERROR = - "Long-running POST operations must have responses with 202 and default return codes. They must also have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified. They also must not have other response codes." -const LR_NO_SCHEMA_ERROR = + "Long-running POST operations must initially return 202 with a default response and no schema. The final response must be 200 with a schema if one is required, or 204 with no schema if not. No other response codes are permitted." +const LR_NO_SCHEMA_ERROR_OK = "200 return code does not have a schema specified. LRO POST must have a 200 return code if only if the final response is intended to have a schema, if not the 200 return code must not be specified." -const LRO_SCHEMA_ERROR = "202 response for a LRO POST operation must not have a response schema specified." +const LR_SCHEMA_ERROR_ACCEPTED = "202 response for a LRO POST operation must not have a response schema specified." +const LR_SCHEMA_ERROR_NO_CONTENT = "204 response for a Sync/LRO POST operation must not have a response schema specified." let linter: Spectral @@ -105,9 +106,6 @@ test("PostResponseCodes should find errors for sync post with only 204", () => { responses: { "204": { description: "No content", - schema: { - $ref: "#/definitions/FooResource", - }, }, }, }, @@ -220,9 +218,10 @@ test("PostResponseCodes should find errors for sync post without default respons }, } return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(1) + expect(results.length).toBe(2) expect(results[0].path.join(".")).toBe("paths./foo.post") expect(results[0].message).toContain(SYNC_ERROR) + expect(results[1].message).toContain(LR_SCHEMA_ERROR_NO_CONTENT) }) }) @@ -298,9 +297,10 @@ test("PostResponseCodes should find errors for sync post with extra response cod }, } return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(1) + expect(results.length).toBe(2) expect(results[0].path.join(".")).toBe("paths./foo.post") expect(results[0].message).toContain(SYNC_ERROR) + expect(results[1].message).toContain(LR_SCHEMA_ERROR_NO_CONTENT) }) }) @@ -516,9 +516,6 @@ test("PostResponseCodes should find no errors for sync post when 204, default re responses: { "204": { description: "No-Content", - schema: { - $ref: "#/definitions/FooResource", - }, }, default: { description: "Error", @@ -630,9 +627,9 @@ test("PostResponseCodes should find errors for lro post with only 202", () => { return linter.run(myOpenApiDocument).then((results) => { expect(results.length).toBe(2) expect(results[0].path.join(".")).toBe("paths./foo.post") + expect(results[0].message).toContain(LR_ERROR) expect(results[1].path.join(".")).toBe("paths./foo.post") - expect(results[0].message).toContain(LRO_SCHEMA_ERROR) - expect(results[1].message).toContain(LR_ERROR) + expect(results[1].message).toContain(LR_SCHEMA_ERROR_ACCEPTED) }) }) @@ -776,9 +773,9 @@ test("PostResponseCodes should find errors for lro post without default response return linter.run(myOpenApiDocument).then((results) => { expect(results.length).toBe(2) expect(results[0].path.join(".")).toBe("paths./foo.post") + expect(results[0].message).toContain(LR_ERROR) expect(results[1].path.join(".")).toBe("paths./foo.post") - expect(results[0].message).toContain(LRO_SCHEMA_ERROR) - expect(results[1].message).toContain(LR_ERROR) + expect(results[1].message).toContain(LR_SCHEMA_ERROR_ACCEPTED) }) }) @@ -933,9 +930,9 @@ test("PostResponseCodes should find errors for lro post with empty schema in 200 return linter.run(myOpenApiDocument).then((results) => { expect(results.length).toBe(2) expect(results[0].path.join(".")).toBe("paths./foo.post") - expect(results[0].message).toContain(LRO_SCHEMA_ERROR) + expect(results[0].message).toContain(LR_NO_SCHEMA_ERROR_OK) expect(results[1].path.join(".")).toBe("paths./foo.post") - expect(results[1].message).toContain(LR_NO_SCHEMA_ERROR) + expect(results[1].message).toContain(LR_SCHEMA_ERROR_ACCEPTED) }) }) @@ -1091,7 +1088,7 @@ test("PostResponseCodes should find no errors for lro post when 200 with schema, }) }) -test("PostResponseCodes should find no errors for lro post when 202, default codes are provided", () => { +test("PostResponseCodes should find no errors for lro post when 202, 204 no content, default codes are provided", () => { const myOpenApiDocument = { swagger: "2.0", paths: { @@ -1113,6 +1110,9 @@ test("PostResponseCodes should find no errors for lro post when 202, default cod "202": { description: "accepted", }, + "204": { + description: "No content", + }, default: { description: "Error", schema: {},