diff --git a/common/changes/@cadl-lang/rest/rest-RouteUpdate_2022-10-19-17-56.json b/common/changes/@cadl-lang/rest/rest-RouteUpdate_2022-10-19-17-56.json new file mode 100644 index 00000000000..7650b54831f --- /dev/null +++ b/common/changes/@cadl-lang/rest/rest-RouteUpdate_2022-10-19-17-56.json @@ -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" +} \ No newline at end of file diff --git a/packages/rest/src/http/decorators.ts b/packages/rest/src/http/decorators.ts index 631a4a52f87..aae025d5bd1 100644 --- a/packages/rest/src/http/decorators.ts +++ b/packages/rest/src/http/decorators.ts @@ -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 * @@ -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( + context: DecoratorContext, + entity: Type, + path: string, + parameters?: Model +) { setRoute(context, entity, { path, isReset: true, + shared: extractSharedValue(context, parameters), }); } diff --git a/packages/rest/src/http/operations.ts b/packages/rest/src/http/operations.ts index 91aed5809b6..6b0a62d8cba 100644 --- a/packages/rest/src/http/operations.ts +++ b/packages/rest/src/http/operations.ts @@ -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 { @@ -72,7 +73,7 @@ export function getAllHttpServices( }) ); - validateRouteUnique(diagnostics, httpOperations); + validateRouteUnique(program, diagnostics, httpOperations); const service: HttpService = { namespace: serviceNamespace, operations: httpOperations, @@ -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>(); for (const operation of operations) { @@ -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(); diff --git a/packages/rest/src/http/route.ts b/packages/rest/src/http/route.ts index c25a2151d7c..d002de172f2 100644 --- a/packages/rest/src/http/route.ts +++ b/packages/rest/src/http/route.ts @@ -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[] = []; diff --git a/packages/rest/src/http/types.ts b/packages/rest/src/http/types.ts index 5e1c941dae4..76221089385 100644 --- a/packages/rest/src/http/types.ts +++ b/packages/rest/src/http/types.ts @@ -261,6 +261,7 @@ export interface HttpOperation { export interface RoutePath { path: string; isReset: boolean; + shared: boolean; } export type StatusCode = `${number}` | "*"; diff --git a/packages/rest/src/lib.ts b/packages/rest/src/lib.ts index ba977cae78c..1bc21a5eac7 100644 --- a/packages/rest/src/lib.ts +++ b/packages/rest/src/lib.ts @@ -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; diff --git a/packages/rest/test/http-decorators.test.ts b/packages/rest/test/http-decorators.test.ts index dea556bcc8b..1b29d89fa7e 100644 --- a/packages/rest/test/http-decorators.test.ts +++ b/packages/rest/test/http-decorators.test.ts @@ -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(`