From a9832c641737cad47b2aea8d38b2fcd13bcc291f Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 1 Jul 2024 12:51:37 -0700 Subject: [PATCH 1/2] Allow override verb --- packages/http/src/decorators.ts | 70 +++++++++++++++----------------- packages/http/src/lib.ts | 6 --- packages/http/test/verbs.test.ts | 40 ++++++++++++++++++ 3 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 packages/http/test/verbs.test.ts diff --git a/packages/http/src/decorators.ts b/packages/http/src/decorators.ts index a8473ed52b..f0dec1eefc 100644 --- a/packages/http/src/decorators.ts +++ b/packages/http/src/decorators.ts @@ -9,6 +9,7 @@ import { Operation, Program, StringLiteral, + SyntaxKind, Tuple, Type, Union, @@ -362,53 +363,48 @@ function rangeDescription(start: number, end: number) { return undefined; } -function setOperationVerb(program: Program, entity: Type, verb: HttpVerb): void { - if (entity.kind === "Operation") { - if (!program.stateMap(HttpStateKeys.verbs).has(entity)) { - program.stateMap(HttpStateKeys.verbs).set(entity, verb); - } else { - reportDiagnostic(program, { - code: "http-verb-duplicate", - format: { entityName: entity.name }, - target: entity, - }); - } - } else { - reportDiagnostic(program, { - code: "http-verb-wrong-type", - format: { verb, entityKind: entity.kind }, - target: entity, +function setOperationVerb(context: DecoratorContext, entity: Operation, verb: HttpVerb): void { + validateVerbUniqueOnNode(context, entity); + context.program.stateMap(HttpStateKeys.verbs).set(entity, verb); +} + +function validateVerbUniqueOnNode(context: DecoratorContext, type: Operation) { + const verbDecorators = type.decorators.filter( + (x) => + VERB_DECORATORS.includes(x.decorator) && + x.node?.kind === SyntaxKind.DecoratorExpression && + x.node?.parent === type.node + ); + + if (verbDecorators.length > 1) { + reportDiagnostic(context.program, { + code: "http-verb-duplicate", + format: { entityName: type.name }, + target: context.decoratorTarget, }); + return false; } + return true; } export function getOperationVerb(program: Program, entity: Type): HttpVerb | undefined { return program.stateMap(HttpStateKeys.verbs).get(entity); } -export const $get: GetDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "get"); -}; - -export const $put: PutDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "put"); -}; - -export const $post: PostDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "post"); -}; - -export const $patch: PatchDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "patch"); -}; +function createVerbDecorator(verb: HttpVerb) { + return (context: DecoratorContext, entity: Operation) => { + setOperationVerb(context, entity, verb); + }; +} -export const $delete: DeleteDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "delete"); -}; +export const $get: GetDecorator = createVerbDecorator("get"); +export const $put: PutDecorator = createVerbDecorator("put"); +export const $post: PostDecorator = createVerbDecorator("post"); +export const $patch: PatchDecorator = createVerbDecorator("patch"); +export const $delete: DeleteDecorator = createVerbDecorator("delete"); +export const $head: HeadDecorator = createVerbDecorator("head"); -export const $head: HeadDecorator = (context: DecoratorContext, entity: Operation) => { - setOperationVerb(context.program, entity, "head"); -}; +const VERB_DECORATORS = [$get, $head, $post, $put, $patch, $delete]; export interface HttpServer { url: string; diff --git a/packages/http/src/lib.ts b/packages/http/src/lib.ts index a628270b5c..4dea2027ab 100644 --- a/packages/http/src/lib.ts +++ b/packages/http/src/lib.ts @@ -9,12 +9,6 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`HTTP verb already applied to ${"entityName"}`, }, }, - "http-verb-wrong-type": { - severity: "error", - messages: { - default: paramMessage`Cannot use @${"verb"} on a ${"entityKind"}`, - }, - }, "missing-path-param": { severity: "error", messages: { diff --git a/packages/http/test/verbs.test.ts b/packages/http/test/verbs.test.ts new file mode 100644 index 0000000000..48eb235288 --- /dev/null +++ b/packages/http/test/verbs.test.ts @@ -0,0 +1,40 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; +import { diagnoseOperations, getRoutesFor } from "./test-host.js"; + +describe("specify verb with each decorator", () => { + it.each([ + ["@get", "get"], + ["@post", "post"], + ["@put", "put"], + ["@patch", "patch"], + ["@delete", "delete"], + ["@head", "head"], + ])("%s set verb to %s", async (dec, expected) => { + const routes = await getRoutesFor(`${dec} op test(): string;`); + expect(routes[0].verb).toBe(expected); + }); +}); + +describe("emit error when using 2 verb decorator together on the same node", () => { + it.each([ + ["@get", "@post"], + ["@post", "@put"], + ])("%s", async (...decs) => { + const diagnostics = await diagnoseOperations(`${decs.join(" ")} op test(): string;`); + const diag = { + code: "@typespec/http/http-verb-duplicate", + message: "HTTP verb already applied to test", + }; + expectDiagnostics(diagnostics, new Array(decs.length).fill(diag)); + }); +}); + +it("allow overriding the verb specified in a base operation", async () => { + const routes = await getRoutesFor(` + @get op test(): T; + @head op ping is test; + + `); + expect(routes[0].verb).toBe("head"); +}); From 9fad328439d4deeb2a4c1be990832a9997f5d6d5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 1 Jul 2024 12:59:04 -0700 Subject: [PATCH 2/2] Create allow-override-verb-2024-6-1-19-54-42.md --- .chronus/changes/allow-override-verb-2024-6-1-19-54-42.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/allow-override-verb-2024-6-1-19-54-42.md diff --git a/.chronus/changes/allow-override-verb-2024-6-1-19-54-42.md b/.chronus/changes/allow-override-verb-2024-6-1-19-54-42.md new file mode 100644 index 0000000000..eb379ae989 --- /dev/null +++ b/.chronus/changes/allow-override-verb-2024-6-1-19-54-42.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/http" +--- + +Allow overriding base operation verb