Skip to content

Commit

Permalink
[OpenAPI 3 Emitter] Reuse Operation IDs With Identical Values In @ope…
Browse files Browse the repository at this point in the history
…rationid Decorators For Shared Routes (#6157)

Fix: #5513

This pull request introduces changes to reuse operation IDs when all
`@operationId` decorators share the same value, which improve the
usability for client code generation to leverage the shared operation
ID.

---------

Co-authored-by: albertxavier100 <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: albertxavier100 <[email protected]>
Co-authored-by: Timothee Guerin <[email protected]>
  • Loading branch information
5 people authored Feb 28, 2025
1 parent ceec016 commit 134f7e7
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .chronus/changes/wanl-opid-2025-1-26-11-56-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/openapi3"
---

Shared operations operationId can now be set if they all share the same value provided by `@operationId`
5 changes: 4 additions & 1 deletion packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,10 @@ function createOAPIEmitter(
}

function computeSharedOperationId(shared: SharedHttpOperation) {
return shared.operations.map((op) => resolveOperationId(program, op.operation)).join("_");
const operationIds = shared.operations.map((op) => resolveOperationId(program, op.operation));
const uniqueOpIds = new Set<string>(operationIds);
if (uniqueOpIds.size === 1) return uniqueOpIds.values().next().value;
return operationIds.join("_");
}

function getOperationOrSharedOperation(operation: HttpOperation | SharedHttpOperation):
Expand Down
61 changes: 61 additions & 0 deletions packages/openapi3/test/operation-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { expect, it } from "vitest";
import { OpenAPI3Document } from "../src/types.js";
import { worksFor } from "./works-for.js";

interface Case {
description: string;
code: string;
expectedOperationId: string;
}

const testCases: Case[] = [
{
description: "should reuse on the same operation IDs",
code: createCode("op1", "op1", "op1"),
expectedOperationId: "op1",
},
{
description: "should concat with different operation IDs",
code: createCode("op1", "op1", "op2"),
expectedOperationId: "op1_op1_op2",
},
{
description: "should concat with partial operation IDs",
code: createCode(undefined, "op1", "op1"),
expectedOperationId: "getWidget1_op1_op1",
},
{
description: "should concat with no operation IDs",
code: createCode(),
expectedOperationId: "getWidget1_getWidget2_getWidget3",
},
];

worksFor(["3.0.0", "3.1.0"], ({ openApiFor }) => {
it.each(testCases)("$description", async (c: Case) => {
const res: OpenAPI3Document = await openApiFor(c.code);
expect(res.paths["/{id}"].get?.operationId).toBe(c.expectedOperationId);
});
});

function createCode(id1?: string, id2?: string, id3?: string) {
return `
@service
namespace DemoService;
${createOperationCodeBlock(1, id1)}
${createOperationCodeBlock(2, id2)}
${createOperationCodeBlock(3, id3)}
`;
}

function createOperationCodeBlock(operationIndex: number, id?: string) {
return `
@sharedRoute
${createOperationIdDecorator(id)}
op getWidget${operationIndex}(@path id: string): void;
`;
}

function createOperationIdDecorator(id?: string) {
return id ? `@operationId("${id}")` : "";
}

0 comments on commit 134f7e7

Please sign in to comment.