Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 43 deletions.
8 changes: 8 additions & 0 deletions .chronus/changes/allow-override-verb-2024-6-1-19-54-42.md
Original file line number Diff line number Diff line change
@@ -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
70 changes: 33 additions & 37 deletions packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Operation,
Program,
StringLiteral,
SyntaxKind,
Tuple,
Type,
Union,
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
40 changes: 40 additions & 0 deletions packages/http/test/verbs.test.ts
Original file line number Diff line number Diff line change
@@ -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>(): T;
@head op ping is test<void>;
`);
expect(routes[0].verb).toBe("head");
});

0 comments on commit ce10ed9

Please sign in to comment.