Skip to content

Commit 5f2db46

Browse files
matzehechtMatthias HechtMathurAditya724
authored
feat: transform path segements to PascalCase in generateOperationId (#214)
* feat: transform path segements to pascal case in generateOperationId this introduces better handling for paths with dashes * chore: merge changes * fix: minor improvements --------- Co-authored-by: Matthias Hecht <matthias.hecht@boehringer-ingelheim.com> Co-authored-by: mathuraditya724 <mathuraditya724@gmail.com>
1 parent 780d234 commit 5f2db46

File tree

3 files changed

+204
-4
lines changed

3 files changed

+204
-4
lines changed

src/__tests__/__snapshots__/zodv3.test.ts.snap

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,90 @@ exports[`zod v3 > describeResponse 1`] = `
146146
}
147147
`;
148148

149+
exports[`zod v3 > operation id for path with dash 1`] = `
150+
{
151+
"components": {
152+
"parameters": {
153+
"Param": {
154+
"in": "path",
155+
"name": "id",
156+
"required": true,
157+
"schema": {
158+
"type": "number",
159+
},
160+
},
161+
},
162+
},
163+
"info": {
164+
"description": "Development documentation",
165+
"title": "Hono Documentation",
166+
"version": "0.0.0",
167+
},
168+
"openapi": "3.1.0",
169+
"paths": {
170+
"/some-path": {
171+
"get": {
172+
"description": "This is a test route",
173+
"operationId": "getSomePath",
174+
"parameters": [
175+
{
176+
"$ref": "#/components/parameters/Param",
177+
},
178+
{
179+
"in": "query",
180+
"name": "limit",
181+
"schema": {
182+
"default": 10,
183+
"type": "number",
184+
},
185+
},
186+
{
187+
"in": "query",
188+
"name": "offset",
189+
"schema": {
190+
"default": 0,
191+
"type": "number",
192+
},
193+
},
194+
],
195+
"responses": {
196+
"200": {
197+
"content": {
198+
"application/json": {
199+
"schema": {
200+
"properties": {
201+
"count": {
202+
"type": "number",
203+
},
204+
"result": {
205+
"items": {
206+
"type": "string",
207+
},
208+
"type": "array",
209+
},
210+
},
211+
"required": [
212+
"result",
213+
"count",
214+
],
215+
"type": "object",
216+
},
217+
},
218+
},
219+
"description": "Success",
220+
},
221+
},
222+
"summary": "Test route",
223+
"tags": [
224+
"test",
225+
],
226+
},
227+
},
228+
},
229+
"tags": undefined,
230+
}
231+
`;
232+
149233
exports[`zod v3 > with metadata 1`] = `
150234
{
151235
"components": {

src/__tests__/zodv3.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,116 @@ describe("zod v3", () => {
176176

177177
expect(specs).toMatchSnapshot();
178178
});
179+
180+
it("operation id for path with dash", async () => {
181+
const query = z.object({
182+
limit: z.coerce.number().default(10),
183+
offset: z.coerce.number().default(0),
184+
});
185+
186+
const param = z
187+
.object({
188+
id: z.coerce.number(),
189+
})
190+
.openapi({ ref: "Param" });
191+
192+
const app = new Hono().get(
193+
"/some-path",
194+
describeRoute({
195+
tags: ["test"],
196+
summary: "Test route",
197+
description: "This is a test route",
198+
}),
199+
validator("param", param),
200+
validator("query", query),
201+
describeResponse(
202+
async (c) => {
203+
c.req.valid("query");
204+
c.req.valid("param");
205+
206+
return c.json({
207+
result: [],
208+
count: 0,
209+
});
210+
},
211+
{
212+
200: {
213+
description: "Success",
214+
content: {
215+
"application/json": {
216+
vSchema: z.object({
217+
result: z.array(z.string()),
218+
count: z.number(),
219+
}),
220+
},
221+
},
222+
},
223+
},
224+
),
225+
);
226+
227+
const specs = await generateSpecs(app);
228+
229+
expect(specs).toMatchSnapshot();
230+
});
231+
232+
it("operation id for path with underscore", async () => {
233+
const app = new Hono().get(
234+
"/api_v1/users",
235+
describeRoute({
236+
tags: ["test"],
237+
summary: "Test route",
238+
description: "This is a test route",
239+
}),
240+
async (c) => {
241+
return c.json({ message: "Hello" });
242+
},
243+
);
244+
245+
const specs = await generateSpecs(app);
246+
247+
expect(specs.paths?.["/api_v1/users"]?.get?.operationId).toBe(
248+
"getApiV1Users",
249+
);
250+
});
251+
252+
it("operation id for path param with dash", async () => {
253+
const app = new Hono().get(
254+
"/users/:user-id",
255+
describeRoute({
256+
tags: ["test"],
257+
summary: "Test route",
258+
description: "This is a test route",
259+
}),
260+
async (c) => {
261+
return c.json({ message: "Hello" });
262+
},
263+
);
264+
265+
const specs = await generateSpecs(app);
266+
267+
expect(specs.paths?.["/users/{user-id}"]?.get?.operationId).toBe(
268+
"getUsersByUserId",
269+
);
270+
});
271+
272+
it("operation id for multiple dashed segments", async () => {
273+
const app = new Hono().get(
274+
"/api-v1/user-profile",
275+
describeRoute({
276+
tags: ["test"],
277+
summary: "Test route",
278+
description: "This is a test route",
279+
}),
280+
async (c) => {
281+
return c.json({ message: "Hello" });
282+
},
283+
);
284+
285+
const specs = await generateSpecs(app);
286+
287+
expect(specs.paths?.["/api-v1/user-profile"]?.get?.operationId).toBe(
288+
"getApiV1UserProfile",
289+
);
290+
});
179291
});

src/utils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ const toOpenAPIPathSegment = (segment: string) => {
4747
const toOpenAPIPath = (path: string) =>
4848
path.split("/").map(toOpenAPIPathSegment).join("/");
4949

50-
const capitalize = (word: string) =>
51-
word.charAt(0).toUpperCase() + word.slice(1);
50+
const toPascalCase = (text: string) =>
51+
text
52+
.split(/[\W_]+/)
53+
.filter(Boolean)
54+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
55+
.join("");
5256

5357
const generateOperationId = (route: RouterRoute) => {
5458
let operationId = route.method.toLowerCase();
@@ -58,9 +62,9 @@ const generateOperationId = (route: RouterRoute) => {
5862
for (const segment of route.path.split("/")) {
5963
const openApiPathSegment = toOpenAPIPathSegment(segment);
6064
if (openApiPathSegment.charCodeAt(0) === 123) {
61-
operationId += `By${capitalize(openApiPathSegment.slice(1, -1))}`;
65+
operationId += `By${toPascalCase(openApiPathSegment.slice(1, -1))}`;
6266
} else {
63-
operationId += capitalize(openApiPathSegment);
67+
operationId += toPascalCase(openApiPathSegment);
6468
}
6569
}
6670

0 commit comments

Comments
 (0)