diff --git a/.chronus/changes/rest-prop-filtering-2025-2-7-9-50-32.md b/.chronus/changes/rest-prop-filtering-2025-2-7-9-50-32.md new file mode 100644 index 00000000000..d89168d8054 --- /dev/null +++ b/.chronus/changes/rest-prop-filtering-2025-2-7-9-50-32.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/rest" +--- + +Updates `@autoRoute` behavior to apply same HttpOperationParameter filtering to HttpProperty \ No newline at end of file diff --git a/packages/rest/src/rest.ts b/packages/rest/src/rest.ts index 24e60c56331..faa95659d2f 100644 --- a/packages/rest/src/rest.ts +++ b/packages/rest/src/rest.ts @@ -111,6 +111,7 @@ function autoRouteProducer( const routePath = getRoutePath(program, operation)?.path; const segments = [...parentSegments, ...(routePath ? [routePath] : [])]; const filteredParameters: HttpOperationParameter[] = []; + const filteredParamProperties = new Set(); const paramOptions = { ...(options?.paramOptions ?? {}), verbSelector: getResourceOperationHttpVerb, @@ -146,10 +147,21 @@ function autoRouteProducer( // Push all usable parameters to the filtered list filteredParameters.push(httpParam); + filteredParamProperties.add(httpParam.param); } // Replace the original parameters with filtered set parameters.parameters = filteredParameters; + // Remove any header/query/path/cookie properties that aren't in the filtered list + for (let i = parameters.properties.length - 1; i >= 0; i--) { + const httpProp = parameters.properties[i]; + if (!["header", "query", "path", "cookie"].includes(httpProp.kind)) { + continue; + } + if (!filteredParamProperties.has(httpProp.property)) { + parameters.properties.splice(i, 1); + } + } // Add the operation's own segment if present addSegmentFragment(program, operation, segments); diff --git a/packages/rest/test/routes.test.ts b/packages/rest/test/routes.test.ts index ee43ebc1530..1cd860f07dc 100644 --- a/packages/rest/test/routes.test.ts +++ b/packages/rest/test/routes.test.ts @@ -143,7 +143,7 @@ describe("rest: routes", () => { }); it("autoRoute operations filter out path parameters with a string literal type", async () => { - const routes = await getRoutesFor( + const operations = await getOperations( ` model ThingId { @path @@ -167,16 +167,60 @@ describe("rest: routes", () => { } `, ); - - deepStrictEqual(routes, [ - { - verb: "get", - path: "/subscriptions/{subscriptionId}/providers/Microsoft.Things/things/{thingId}", - params: ["subscriptionId", "thingId"], - }, + expect(operations.length).toBe(1); + const operation = operations[0]; + expect(operation.verb).toBe("get"); + expect(operation.path).toBe( + "/subscriptions/{subscriptionId}/providers/Microsoft.Things/things/{thingId}", + ); + const operationParams = operation.parameters.parameters; + const operationProps = operation.parameters.properties; + expect(operationParams.map((p) => p.name)).toEqual(["subscriptionId", "thingId"]); + expect(operationProps.map((p) => (p as any).options.name)).toEqual([ + "subscriptionId", + "thingId", ]); }); + it("autoRoute operations does not filter out non-param http properties", async () => { + const operations = await getOperations( + ` + model ThingId { + @path + @segment("things") + thingId: string; + } + + @autoRoute + interface Things { + @put + op WithFilteredParam( + @path + @segment("subscriptions") + subscriptionId: string, + + @path + @segment("providers") + provider: "Microsoft.Things", + ...ThingId, + + @header contentType: "text/plain", + + @body body: "Test Content" + ): string; + } + `, + ); + expect(operations.length).toBe(1); + const operation = operations[0]; + expect(operation.verb).toBe("put"); + expect(operation.path).toBe( + "/subscriptions/{subscriptionId}/providers/Microsoft.Things/things/{thingId}", + ); + const operationProps = operation.parameters.properties; + expect(operationProps.map((p) => p.kind)).toEqual(["path", "path", "contentType", "body"]); + }); + it("emit diagnostic if passing arguments to autoroute decorators", async () => { const [_, diagnostics] = await compileOperations(` @autoRoute("/test") op test(): string;