diff --git a/src/__tests__/__snapshots__/zodv3.test.ts.snap b/src/__tests__/__snapshots__/zodv3.test.ts.snap index 2fdc4ba..b0158ce 100644 --- a/src/__tests__/__snapshots__/zodv3.test.ts.snap +++ b/src/__tests__/__snapshots__/zodv3.test.ts.snap @@ -146,6 +146,90 @@ exports[`zod v3 > describeResponse 1`] = ` } `; +exports[`zod v3 > operation id for path with dash 1`] = ` +{ + "components": { + "parameters": { + "Param": { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "number", + }, + }, + }, + }, + "info": { + "description": "Development documentation", + "title": "Hono Documentation", + "version": "0.0.0", + }, + "openapi": "3.1.0", + "paths": { + "/some-path": { + "get": { + "description": "This is a test route", + "operationId": "getSomePath", + "parameters": [ + { + "$ref": "#/components/parameters/Param", + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 10, + "type": "number", + }, + }, + { + "in": "query", + "name": "offset", + "schema": { + "default": 0, + "type": "number", + }, + }, + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "count": { + "type": "number", + }, + "result": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "result", + "count", + ], + "type": "object", + }, + }, + }, + "description": "Success", + }, + }, + "summary": "Test route", + "tags": [ + "test", + ], + }, + }, + }, + "tags": undefined, +} +`; + exports[`zod v3 > with metadata 1`] = ` { "components": { diff --git a/src/__tests__/zodv3.test.ts b/src/__tests__/zodv3.test.ts index dfb5864..f07c762 100644 --- a/src/__tests__/zodv3.test.ts +++ b/src/__tests__/zodv3.test.ts @@ -176,4 +176,116 @@ describe("zod v3", () => { expect(specs).toMatchSnapshot(); }); + + it("operation id for path with dash", async () => { + const query = z.object({ + limit: z.coerce.number().default(10), + offset: z.coerce.number().default(0), + }); + + const param = z + .object({ + id: z.coerce.number(), + }) + .openapi({ ref: "Param" }); + + const app = new Hono().get( + "/some-path", + describeRoute({ + tags: ["test"], + summary: "Test route", + description: "This is a test route", + }), + validator("param", param), + validator("query", query), + describeResponse( + async (c) => { + c.req.valid("query"); + c.req.valid("param"); + + return c.json({ + result: [], + count: 0, + }); + }, + { + 200: { + description: "Success", + content: { + "application/json": { + vSchema: z.object({ + result: z.array(z.string()), + count: z.number(), + }), + }, + }, + }, + }, + ), + ); + + const specs = await generateSpecs(app); + + expect(specs).toMatchSnapshot(); + }); + + it("operation id for path with underscore", async () => { + const app = new Hono().get( + "/api_v1/users", + describeRoute({ + tags: ["test"], + summary: "Test route", + description: "This is a test route", + }), + async (c) => { + return c.json({ message: "Hello" }); + }, + ); + + const specs = await generateSpecs(app); + + expect(specs.paths?.["/api_v1/users"]?.get?.operationId).toBe( + "getApiV1Users", + ); + }); + + it("operation id for path param with dash", async () => { + const app = new Hono().get( + "/users/:user-id", + describeRoute({ + tags: ["test"], + summary: "Test route", + description: "This is a test route", + }), + async (c) => { + return c.json({ message: "Hello" }); + }, + ); + + const specs = await generateSpecs(app); + + expect(specs.paths?.["/users/{user-id}"]?.get?.operationId).toBe( + "getUsersByUserId", + ); + }); + + it("operation id for multiple dashed segments", async () => { + const app = new Hono().get( + "/api-v1/user-profile", + describeRoute({ + tags: ["test"], + summary: "Test route", + description: "This is a test route", + }), + async (c) => { + return c.json({ message: "Hello" }); + }, + ); + + const specs = await generateSpecs(app); + + expect(specs.paths?.["/api-v1/user-profile"]?.get?.operationId).toBe( + "getApiV1UserProfile", + ); + }); }); diff --git a/src/utils.ts b/src/utils.ts index 3981de4..1da8ce8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -47,8 +47,12 @@ const toOpenAPIPathSegment = (segment: string) => { const toOpenAPIPath = (path: string) => path.split("/").map(toOpenAPIPathSegment).join("/"); -const capitalize = (word: string) => - word.charAt(0).toUpperCase() + word.slice(1); +const toPascalCase = (text: string) => + text + .split(/[\W_]+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(""); const generateOperationId = (route: RouterRoute) => { let operationId = route.method.toLowerCase(); @@ -58,9 +62,9 @@ const generateOperationId = (route: RouterRoute) => { for (const segment of route.path.split("/")) { const openApiPathSegment = toOpenAPIPathSegment(segment); if (openApiPathSegment.charCodeAt(0) === 123) { - operationId += `By${capitalize(openApiPathSegment.slice(1, -1))}`; + operationId += `By${toPascalCase(openApiPathSegment.slice(1, -1))}`; } else { - operationId += capitalize(openApiPathSegment); + operationId += toPascalCase(openApiPathSegment); } }