Skip to content

File tree

4 files changed

+81
-43
lines changed

4 files changed

+81
-43
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/http"
6+
---
7+
8+
Allow overriding base operation verb

packages/http/src/decorators.ts

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Operation,
1010
Program,
1111
StringLiteral,
12+
SyntaxKind,
1213
Tuple,
1314
Type,
1415
Union,
@@ -362,53 +363,48 @@ function rangeDescription(start: number, end: number) {
362363
return undefined;
363364
}
364365

365-
function setOperationVerb(program: Program, entity: Type, verb: HttpVerb): void {
366-
if (entity.kind === "Operation") {
367-
if (!program.stateMap(HttpStateKeys.verbs).has(entity)) {
368-
program.stateMap(HttpStateKeys.verbs).set(entity, verb);
369-
} else {
370-
reportDiagnostic(program, {
371-
code: "http-verb-duplicate",
372-
format: { entityName: entity.name },
373-
target: entity,
374-
});
375-
}
376-
} else {
377-
reportDiagnostic(program, {
378-
code: "http-verb-wrong-type",
379-
format: { verb, entityKind: entity.kind },
380-
target: entity,
366+
function setOperationVerb(context: DecoratorContext, entity: Operation, verb: HttpVerb): void {
367+
validateVerbUniqueOnNode(context, entity);
368+
context.program.stateMap(HttpStateKeys.verbs).set(entity, verb);
369+
}
370+
371+
function validateVerbUniqueOnNode(context: DecoratorContext, type: Operation) {
372+
const verbDecorators = type.decorators.filter(
373+
(x) =>
374+
VERB_DECORATORS.includes(x.decorator) &&
375+
x.node?.kind === SyntaxKind.DecoratorExpression &&
376+
x.node?.parent === type.node
377+
);
378+
379+
if (verbDecorators.length > 1) {
380+
reportDiagnostic(context.program, {
381+
code: "http-verb-duplicate",
382+
format: { entityName: type.name },
383+
target: context.decoratorTarget,
381384
});
385+
return false;
382386
}
387+
return true;
383388
}
384389

385390
export function getOperationVerb(program: Program, entity: Type): HttpVerb | undefined {
386391
return program.stateMap(HttpStateKeys.verbs).get(entity);
387392
}
388393

389-
export const $get: GetDecorator = (context: DecoratorContext, entity: Operation) => {
390-
setOperationVerb(context.program, entity, "get");
391-
};
392-
393-
export const $put: PutDecorator = (context: DecoratorContext, entity: Operation) => {
394-
setOperationVerb(context.program, entity, "put");
395-
};
396-
397-
export const $post: PostDecorator = (context: DecoratorContext, entity: Operation) => {
398-
setOperationVerb(context.program, entity, "post");
399-
};
400-
401-
export const $patch: PatchDecorator = (context: DecoratorContext, entity: Operation) => {
402-
setOperationVerb(context.program, entity, "patch");
403-
};
394+
function createVerbDecorator(verb: HttpVerb) {
395+
return (context: DecoratorContext, entity: Operation) => {
396+
setOperationVerb(context, entity, verb);
397+
};
398+
}
404399

405-
export const $delete: DeleteDecorator = (context: DecoratorContext, entity: Operation) => {
406-
setOperationVerb(context.program, entity, "delete");
407-
};
400+
export const $get: GetDecorator = createVerbDecorator("get");
401+
export const $put: PutDecorator = createVerbDecorator("put");
402+
export const $post: PostDecorator = createVerbDecorator("post");
403+
export const $patch: PatchDecorator = createVerbDecorator("patch");
404+
export const $delete: DeleteDecorator = createVerbDecorator("delete");
405+
export const $head: HeadDecorator = createVerbDecorator("head");
408406

409-
export const $head: HeadDecorator = (context: DecoratorContext, entity: Operation) => {
410-
setOperationVerb(context.program, entity, "head");
411-
};
407+
const VERB_DECORATORS = [$get, $head, $post, $put, $patch, $delete];
412408

413409
export interface HttpServer {
414410
url: string;

packages/http/src/lib.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ export const $lib = createTypeSpecLibrary({
99
default: paramMessage`HTTP verb already applied to ${"entityName"}`,
1010
},
1111
},
12-
"http-verb-wrong-type": {
13-
severity: "error",
14-
messages: {
15-
default: paramMessage`Cannot use @${"verb"} on a ${"entityKind"}`,
16-
},
17-
},
1812
"missing-path-param": {
1913
severity: "error",
2014
messages: {

packages/http/test/verbs.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expectDiagnostics } from "@typespec/compiler/testing";
2+
import { describe, expect, it } from "vitest";
3+
import { diagnoseOperations, getRoutesFor } from "./test-host.js";
4+
5+
describe("specify verb with each decorator", () => {
6+
it.each([
7+
["@get", "get"],
8+
["@post", "post"],
9+
["@put", "put"],
10+
["@patch", "patch"],
11+
["@delete", "delete"],
12+
["@head", "head"],
13+
])("%s set verb to %s", async (dec, expected) => {
14+
const routes = await getRoutesFor(`${dec} op test(): string;`);
15+
expect(routes[0].verb).toBe(expected);
16+
});
17+
});
18+
19+
describe("emit error when using 2 verb decorator together on the same node", () => {
20+
it.each([
21+
["@get", "@post"],
22+
["@post", "@put"],
23+
])("%s", async (...decs) => {
24+
const diagnostics = await diagnoseOperations(`${decs.join(" ")} op test(): string;`);
25+
const diag = {
26+
code: "@typespec/http/http-verb-duplicate",
27+
message: "HTTP verb already applied to test",
28+
};
29+
expectDiagnostics(diagnostics, new Array(decs.length).fill(diag));
30+
});
31+
});
32+
33+
it("allow overriding the verb specified in a base operation", async () => {
34+
const routes = await getRoutesFor(`
35+
@get op test<T>(): T;
36+
@head op ping is test<void>;
37+
38+
`);
39+
expect(routes[0].verb).toBe("head");
40+
});

0 commit comments

Comments
 (0)