Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/rest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Defines the separator string that is inserted before the action name in auto-gen

##### Target

`Model | ModelProperty | Operation`
`Operation | Interface | Namespace`

##### Parameters

Expand Down
3 changes: 2 additions & 1 deletion packages/rest/generated-defs/TypeSpec.Rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Interface,
Model,
ModelProperty,
Namespace,
Operation,
} from "@typespec/compiler";

Expand Down Expand Up @@ -58,7 +59,7 @@ export type SegmentOfDecorator = (
*/
export type ActionSeparatorDecorator = (
context: DecoratorContext,
target: Model | ModelProperty | Operation,
target: Operation | Interface | Namespace,
seperator: "/" | ":" | "/:",
) => void;

Expand Down
2 changes: 1 addition & 1 deletion packages/rest/lib/rest-decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extern dec segmentOf(target: Operation, type: Model);
* @param seperator Seperator seperating the action segment from the rest of the url
*/
extern dec actionSeparator(
target: Model | ModelProperty | Operation,
target: Operation | Interface | Namespace,
seperator: valueof "/" | ":" | "/:"
);

Expand Down
5 changes: 3 additions & 2 deletions packages/rest/src/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Interface,
Model,
ModelProperty,
Namespace,
Operation,
Program,
Scalar,
Expand Down Expand Up @@ -273,11 +274,11 @@ const actionSeparatorKey = createStateSymbol("actionSeparator");
* `@actionSeparator` defines the separator string that is used to precede the action name
* in auto-generated actions.
*
* `@actionSeparator` can only be applied to model properties, operation parameters, or operations.
* `@actionSeparator` can only be applied to operations, interfaces, or namespaces.
*/
export function $actionSeparator(
context: DecoratorContext,
entity: Model | ModelProperty | Operation,
entity: Operation | Interface | Namespace,
separator: "/" | ":" | "/:",
) {
context.program.stateMap(actionSeparatorKey).set(entity, separator);
Expand Down
121 changes: 121 additions & 0 deletions packages/rest/test/action-separator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { expectDiagnostics } from "@typespec/compiler/testing";
import { strictEqual } from "assert";
import { describe, it } from "vitest";
import { Tester, getRoutesFor } from "./test-host.js";

describe("rest: @actionSeparator decorator", () => {
describe("valid targets", () => {
it("works on Operation and affects routing", async () => {
const routes = await getRoutesFor(`
@autoRoute
interface Things {
@action
@TypeSpec.Rest.actionSeparator(":")
@put op customAction(@segment("things") @path thingId: string): string;
}
`);

strictEqual(routes.length, 1);
strictEqual(routes[0].path, "/things/{thingId}:customAction");
});

it("accepts Interface as target without compilation errors", async () => {
// This test verifies that @actionSeparator can be applied to interfaces without errors
const diagnostics = await Tester.diagnose(`
@TypeSpec.Rest.actionSeparator(":")
interface TestInterface {
op test(): void;
}
`);

// No diagnostics means the decorator accepts interfaces as valid targets
strictEqual(diagnostics.length, 0);
});

it("accepts Namespace as target without compilation errors", async () => {
// This test verifies that @actionSeparator can be applied to namespaces without errors
const diagnostics = await Tester.diagnose(`
@TypeSpec.Rest.actionSeparator(":")
namespace TestNamespace {
op test(): void;
}
`);

// No diagnostics means the decorator accepts namespaces as valid targets
strictEqual(diagnostics.length, 0);
});

it("supports all separator values in routing", async () => {
const routes = await getRoutesFor(`
@autoRoute
interface Things {
@action
@TypeSpec.Rest.actionSeparator("/")
@put op action1(@segment("things") @path thingId: string): string;

@action
@TypeSpec.Rest.actionSeparator(":")
@put op action2(@segment("things") @path thingId: string): string;

@action
@TypeSpec.Rest.actionSeparator("/:")
@put op action3(@segment("things") @path thingId: string): string;
}
`);

strictEqual(routes.length, 3);
strictEqual(routes[0].path, "/things/{thingId}/action1");
strictEqual(routes[1].path, "/things/{thingId}:action2");
strictEqual(routes[2].path, "/things/{thingId}/:action3");
});
});

describe("invalid targets", () => {
it("rejects Model", async () => {
const diagnostics = await Tester.diagnose(`
@TypeSpec.Rest.actionSeparator(":")
model TestModel {
id: string;
}
`);

expectDiagnostics(diagnostics, [
{
code: "decorator-wrong-target",
message:
"Cannot apply @actionSeparator decorator to TestModel since it is not assignable to Operation | Interface | Namespace",
},
]);
});

it("rejects ModelProperty", async () => {
const diagnostics = await Tester.diagnose(`
model TestModel {
@TypeSpec.Rest.actionSeparator(":")
id: string;
}
`);

expectDiagnostics(diagnostics, [
{
code: "decorator-wrong-target",
message:
/Cannot apply @actionSeparator decorator to .* since it is not assignable to Operation \| Interface \| Namespace/,
},
]);
});

it("rejects invalid separator values", async () => {
const diagnostics = await Tester.diagnose(`
@TypeSpec.Rest.actionSeparator("invalid")
op test(): void;
`);

expectDiagnostics(diagnostics, [
{
code: "invalid-argument",
},
]);
});
});
});
Loading