Make @route and @autoRoute work together (#1576)

* Add tests.

* Allow overriding route on operations and interfaces.

* Fix #1335.

* Update docs.
This commit is contained in:
Travis Prescott 2023-01-26 10:25:18 -08:00 коммит произвёл GitHub
Родитель 2bebea06b0
Коммит 68d84e1d38
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 190 добавлений и 14 удалений

Просмотреть файл

@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@cadl-lang/rest",
"comment": "Allow @route and @autoRoute to be used together.",
"type": "none"
}
],
"packageName": "@cadl-lang/rest"
}

Просмотреть файл

@ -27,6 +27,21 @@ namespace Pets {
}
```
If `@route` is applied to an interface, that route is not "portable". It will be applied to that interface but will not carry over if another interface extends it.
```cadl
// Operations prepended with /pets
@route("/pets")
interface PetOps {
list(): Pet[]
}
// Operations will *not* be prepended with /pets
interface MyPetOps extends PetOps {
...
}
```
### Automatic route generation
Instead of manually specifying routes using the `@route` decorator, you automatically generate
@ -66,3 +81,24 @@ This will result in the following route for both operations
```text
/tenants/{tenantId}/users/{userName}
```
If `@autoRoute` is applied to an interface, it is not "portable". It will be applied to that interface but will not carry over if another interface extends it.
```cadl
// Operations prepended with /pets
@autoRoute
interface PetOps {
action(@path @segment("pets") id: string): void;
}
// Operations will *not* be prepended with /pets
interface MyPetOps extends PetOps {
...
}
```
### Customizing Automatic Route Generation
Instead of manually specifying routes using the `@route` decorator, you automatically generate
routes from operation parameters by applying the `@autoRoute` decorator to an operation, namespace,
or interface containing operations.

Просмотреть файл

@ -631,16 +631,14 @@ function setRoute(context: DecoratorContext, entity: Type, details: RoutePath) {
const state = context.program.stateMap(routesKey);
if (state.has(entity)) {
if (entity.kind === "Namespace") {
const existingValue: RoutePath = state.get(entity);
if (existingValue.path !== details.path) {
reportDiagnostic(context.program, {
code: "duplicate-route-decorator",
messageId: "namespace",
target: entity,
});
}
if (state.has(entity) && entity.kind === "Namespace") {
const existingValue: RoutePath = state.get(entity);
if (existingValue.path !== details.path) {
reportDiagnostic(context.program, {
code: "duplicate-route-decorator",
messageId: "namespace",
target: entity,
});
}
} else {
state.set(entity, details);

Просмотреть файл

@ -126,12 +126,12 @@ export function resolvePathAndParameters(
},
readonly Diagnostic[]
] {
let segments: string[] = [];
let segments = getOperationRouteSegments(program, operation, overloadBase);
let parameters: HttpOperationParameters;
const diagnostics = createDiagnosticCollector();
if (isAutoRoute(program, operation)) {
let parentOptions;
[segments, parentOptions] = getParentSegments(program, operation);
const [parentSegments, parentOptions] = getParentSegments(program, operation);
segments = parentSegments.length ? parentSegments : segments;
parameters = diagnostics.pipe(getOperationParameters(program, operation));
// The operation exists within an @autoRoute scope, generate the path. This
@ -141,7 +141,6 @@ export function resolvePathAndParameters(
...options,
});
} else {
segments = getOperationRouteSegments(program, operation, overloadBase);
const declaredPathParams = segments.flatMap(extractParamsFromPath);
parameters = diagnostics.pipe(
getOperationParameters(program, operation, overloadBase, declaredPathParams)

Просмотреть файл

@ -667,6 +667,139 @@ describe("rest: routes", () => {
});
});
describe("use of @route with @autoRoute", () => {
it("can override library operation route in service", async () => {
const ops = await getOperations(`
namespace Lib {
@route("one")
op action(): void;
}
@service({title: "Test"})
namespace Test {
op my is Lib.action;
@route("my")
op my2 is Lib.action;
}
`);
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/one");
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my");
});
it("can override library interface route in service", async () => {
const ops = await getOperations(`
namespace Lib {
@route("one")
interface Ops {
action(): void;
}
}
@service({title: "Test"})
namespace Test {
interface Mys extends Lib.Ops {
}
@route("my") interface Mys2 extends Lib.Ops {}
}
`);
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/");
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my");
});
it("can override library interface route in service without changing library", async () => {
const ops = await getOperations(`
namespace Lib {
@route("one")
interface Ops {
action(): void;
}
}
@service({title: "Test"})
namespace Test {
@route("my") interface Mys2 extends Lib.Ops {}
op op2 is Lib.Ops.action;
}
`);
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my");
strictEqual(ops[1].container.kind, "Interface");
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/");
strictEqual(ops[0].container.kind, "Namespace");
});
it("prepends @route in service when library operation uses @autoRoute", async () => {
const ops = await getOperations(`
namespace Lib {
@autoRoute
op action(@path @segment("pets") id: string): void;
}
@service({title: "Test"})
namespace Test {
op my is Lib.action;
@route("my")
op my2 is Lib.action;
}
`);
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/pets/{id}");
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my/pets/{id}");
});
it("prepends @route in service when library interface operation uses @autoRoute", async () => {
const ops = await getOperations(`
namespace Lib {
interface Ops {
@autoRoute
action(@path @segment("pets") id: string): void;
}
}
@service({title: "Test"})
namespace Test {
interface Mys extends Lib.Ops {}
@route("my")
interface Mys2 extends Lib.Ops {};
}
`);
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/pets/{id}");
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my/pets/{id}");
});
it("prepends @route in service when library interface uses @autoRoute", async () => {
const ops = await getOperations(`
namespace Lib {
@autoRoute
interface Ops {
action(@path @segment("pets") id: string): void;
}
}
@service({title: "Test"})
namespace Test {
interface Mys extends Lib.Ops {}
@route("my")
interface Mys2 extends Lib.Ops {};
}
`);
strictEqual(ops[0].verb, "get");
strictEqual(ops[0].path, "/{id}");
strictEqual(ops[1].verb, "get");
strictEqual(ops[1].path, "/my/{id}");
});
});
it("allows customization of path parameters in generated routes", async () => {
const routes = await getRoutesFor(
`