Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/rest",
"comment": "Allow `@route` and `@resetRoute` to accept a `shared` property to suppress route uniqueness validation.",
"type": "minor"
}
],
"packageName": "@cadl-lang/rest"
}
28 changes: 26 additions & 2 deletions packages/rest/src/http/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,23 @@ export function getAuthentication(
return program.stateMap(authenticationKey).get(namespace);
}

function extractSharedValue(context: DecoratorContext, parameters?: Model): boolean {
const sharedType = parameters?.properties.get("shared")?.type;
if (sharedType == undefined) {
return false;
}
switch (sharedType.kind) {
case "Boolean":
return sharedType.value;
default:
reportDiagnostic(context.program, {
code: "shared-boolean",
target: sharedType,
});
return false;
}
}

/**
* `@route` defines the relative route URI for the target operation
*
Expand All @@ -563,17 +580,24 @@ export function getAuthentication(
*
* `@route` can only be applied to operations, namespaces, and interfaces.
*/
export function $route(context: DecoratorContext, entity: Type, path: string) {
export function $route(context: DecoratorContext, entity: Type, path: string, parameters?: Model) {
setRoute(context, entity, {
path,
isReset: false,
shared: extractSharedValue(context, parameters),
});
}

export function $routeReset(context: DecoratorContext, entity: Type, path: string) {
export function $routeReset(
Comment thread
tjprescott marked this conversation as resolved.
context: DecoratorContext,
entity: Type,
path: string,
parameters?: Model
) {
setRoute(context, entity, {
path,
isReset: true,
shared: extractSharedValue(context, parameters),
});
}

Expand Down
13 changes: 11 additions & 2 deletions packages/rest/src/http/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Program,
} from "@cadl-lang/compiler";
import { createDiagnostic, reportDiagnostic } from "../lib.js";
import { getRoutePath } from "./decorators.js";
import { getResponsesForOperation } from "./responses.js";
import { resolvePathAndParameters } from "./route.js";
import {
Expand Down Expand Up @@ -72,7 +73,7 @@ export function getAllHttpServices(
})
);

validateRouteUnique(diagnostics, httpOperations);
validateRouteUnique(program, diagnostics, httpOperations);
const service: HttpService = {
namespace: serviceNamespace,
operations: httpOperations,
Expand Down Expand Up @@ -100,7 +101,11 @@ export function reportIfNoRoutes(program: Program, routes: HttpOperation[]) {
}
}

export function validateRouteUnique(diagnostics: DiagnosticCollector, operations: HttpOperation[]) {
export function validateRouteUnique(
program: Program,
diagnostics: DiagnosticCollector,
operations: HttpOperation[]
) {
const grouped = new Map<string, Map<HttpVerb, HttpOperation[]>>();

for (const operation of operations) {
Expand All @@ -109,6 +114,10 @@ export function validateRouteUnique(diagnostics: DiagnosticCollector, operations
if (operation.overloading !== undefined && isOverloadSameEndpoint(operation as any)) {
continue;
}
const pathShared = getRoutePath(program, operation.operation)?.shared ?? false;
if (pathShared) {
continue;
}
let map = grouped.get(path);
if (map === undefined) {
map = new Map<HttpVerb, HttpOperation[]>();
Expand Down
6 changes: 5 additions & 1 deletion packages/rest/src/http/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ export function resolvePathAndParameters(
overloadBase: HttpOperation | undefined,
options: RouteResolutionOptions
): [
{ path: string; pathSegments: string[]; parameters: HttpOperationParameters },
{
path: string;
pathSegments: string[];
parameters: HttpOperationParameters;
},
readonly Diagnostic[]
] {
let segments: string[] = [];
Expand Down
1 change: 1 addition & 0 deletions packages/rest/src/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export interface HttpOperation {
export interface RoutePath {
path: string;
isReset: boolean;
shared: boolean;
}

export type StatusCode = `${number}` | "*";
Expand Down
7 changes: 7 additions & 0 deletions packages/rest/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ const libDefinition = {
default: paramMessage`@useAuth ${"kind"} only accept Auth model, Tuple of auth model or union of auth model.`,
},
},

"shared-boolean": {
severity: "error",
messages: {
default: "shared parameter must be a boolean.",
},
},
},
} as const;

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

describe("@route", () => {
it("emit diagnostics when duplicated unshared routes are applied", async () => {
const diagnostics = await runner.diagnose(`
@route("/test") op test(): string;
@route("/test") op test2(): string;
`);

expectDiagnostics(diagnostics, [
{
code: "@cadl-lang/rest/duplicate-operation",
message: `Duplicate operation "test" routed at "get /test".`,
},
{
code: "@cadl-lang/rest/duplicate-operation",
message: `Duplicate operation "test2" routed at "get /test".`,
},
]);
});

it("do not emit diagnostics when duplicated shared routes are applied", async () => {
const diagnostics = await runner.diagnose(`
@route("/test", {shared: true}) op test(): string;
@route("/test", {shared: true}) op test2(): string;
`);

expectDiagnosticEmpty(diagnostics);
});

it("emit diagnostic when wrong type for shared is provided", async () => {
const diagnostics = await runner.diagnose(`
@route("/test", {shared: "yes"}) op test(): string;
`);
expectDiagnostics(diagnostics, [
{
code: "@cadl-lang/rest/shared-boolean",
message: `shared parameter must be a boolean.`,
},
]);
});
});

describe("@path", () => {
it("emit diagnostics when @path is not used on model property", async () => {
const diagnostics = await runner.diagnose(`
Expand Down