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:
Родитель
2bebea06b0
Коммит
68d84e1d38
|
@ -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(
|
||||
`
|
||||
|
|
Загрузка…
Ссылка в новой задаче