From a63d103c2841ea40b5c8564dfbdb5e7515636f8c Mon Sep 17 00:00:00 2001 From: jasonblanchard Date: Thu, 20 Mar 2025 22:23:34 -0400 Subject: [PATCH 1/2] Working without an operation id #1 --- examples/petstore/gen/server.ts | 150 +++++++++--------- examples/simple/gen/server.ts | 26 +-- .../openapi-typescript-server/bin/index.cjs | 38 +++-- .../src/cli/generate.test.ts | 118 +++++++++----- .../src/cli/generate.ts | 64 +++++--- 5 files changed, 236 insertions(+), 160 deletions(-) diff --git a/examples/petstore/gen/server.ts b/examples/petstore/gen/server.ts index 4181385..7180170 100644 --- a/examples/petstore/gen/server.ts +++ b/examples/petstore/gen/server.ts @@ -3,34 +3,34 @@ * Do not make direct changes to the file. */ -import type { operations } from "./schema.d.ts"; +import type { paths } from "./schema.d.ts"; import type { Route } from "openapi-typescript-server"; import { NotImplementedError } from "openapi-typescript-server"; export interface UpdatePetArgs { - parameters: operations['updatePet']['parameters']; - requestBody: operations['updatePet']['requestBody']; + parameters: paths['/pet']['put']['parameters']; + requestBody: paths['/pet']['put']['requestBody']; req: Req; res: Res; } interface UpdatePetResult_200 { - content: { 200: operations['updatePet']['responses']['200']['content'] }; + content: { 200: paths['/pet']['put']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface UpdatePetResult_400 { - content: { 400: operations['updatePet']['responses']['400']['content'] }; + content: { 400: paths['/pet']['put']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface UpdatePetResult_404 { - content: { 404: operations['updatePet']['responses']['404']['content'] }; + content: { 404: paths['/pet']['put']['responses']['404']['content'] }; headers?: { [name: string]: any }; } interface UpdatePetResult_405 { - content: { 405: operations['updatePet']['responses']['405']['content'] }; + content: { 405: paths['/pet']['put']['responses']['405']['content'] }; headers?: { [name: string]: any }; } @@ -41,19 +41,19 @@ export async function updatePet_unimplemented(): UpdatePetResult { } export interface AddPetArgs { - parameters: operations['addPet']['parameters']; - requestBody: operations['addPet']['requestBody']; + parameters: paths['/pet']['post']['parameters']; + requestBody: paths['/pet']['post']['requestBody']; req: Req; res: Res; } interface AddPetResult_200 { - content: { 200: operations['addPet']['responses']['200']['content'] }; + content: { 200: paths['/pet']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface AddPetResult_405 { - content: { 405: operations['addPet']['responses']['405']['content'] }; + content: { 405: paths['/pet']['post']['responses']['405']['content'] }; headers?: { [name: string]: any }; } @@ -64,19 +64,19 @@ export async function addPet_unimplemented(): AddPetResult { } export interface FindPetsByStatusArgs { - parameters: operations['findPetsByStatus']['parameters']; - requestBody: operations['findPetsByStatus']['requestBody']; + parameters: paths['/pet/findByStatus']['get']['parameters']; + requestBody: paths['/pet/findByStatus']['get']['requestBody']; req: Req; res: Res; } interface FindPetsByStatusResult_200 { - content: { 200: operations['findPetsByStatus']['responses']['200']['content'] }; + content: { 200: paths['/pet/findByStatus']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface FindPetsByStatusResult_400 { - content: { 400: operations['findPetsByStatus']['responses']['400']['content'] }; + content: { 400: paths['/pet/findByStatus']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } @@ -87,19 +87,19 @@ export async function findPetsByStatus_unimplemented(): FindPetsByStatusResult { } export interface FindPetsByTagsArgs { - parameters: operations['findPetsByTags']['parameters']; - requestBody: operations['findPetsByTags']['requestBody']; + parameters: paths['/pet/findByTags']['get']['parameters']; + requestBody: paths['/pet/findByTags']['get']['requestBody']; req: Req; res: Res; } interface FindPetsByTagsResult_200 { - content: { 200: operations['findPetsByTags']['responses']['200']['content'] }; + content: { 200: paths['/pet/findByTags']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface FindPetsByTagsResult_400 { - content: { 400: operations['findPetsByTags']['responses']['400']['content'] }; + content: { 400: paths['/pet/findByTags']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } @@ -110,24 +110,24 @@ export async function findPetsByTags_unimplemented(): FindPetsByTagsResult { } export interface GetPetByIdArgs { - parameters: operations['getPetById']['parameters']; - requestBody: operations['getPetById']['requestBody']; + parameters: paths['/pet/{petId}']['get']['parameters']; + requestBody: paths['/pet/{petId}']['get']['requestBody']; req: Req; res: Res; } interface GetPetByIdResult_200 { - content: { 200: operations['getPetById']['responses']['200']['content'] }; + content: { 200: paths['/pet/{petId}']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface GetPetByIdResult_400 { - content: { 400: operations['getPetById']['responses']['400']['content'] }; + content: { 400: paths['/pet/{petId}']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface GetPetByIdResult_404 { - content: { 404: operations['getPetById']['responses']['404']['content'] }; + content: { 404: paths['/pet/{petId}']['get']['responses']['404']['content'] }; headers?: { [name: string]: any }; } @@ -138,14 +138,14 @@ export async function getPetById_unimplemented(): GetPetByIdResult { } export interface UpdatePetWithFormArgs { - parameters: operations['updatePetWithForm']['parameters']; - requestBody: operations['updatePetWithForm']['requestBody']; + parameters: paths['/pet/{petId}']['post']['parameters']; + requestBody: paths['/pet/{petId}']['post']['requestBody']; req: Req; res: Res; } interface UpdatePetWithFormResult_405 { - content: { 405: operations['updatePetWithForm']['responses']['405']['content'] }; + content: { 405: paths['/pet/{petId}']['post']['responses']['405']['content'] }; headers?: { [name: string]: any }; } @@ -156,14 +156,14 @@ export async function updatePetWithForm_unimplemented(): UpdatePetWithFormResult } export interface DeletePetArgs { - parameters: operations['deletePet']['parameters']; - requestBody: operations['deletePet']['requestBody']; + parameters: paths['/pet/{petId}']['delete']['parameters']; + requestBody: paths['/pet/{petId}']['delete']['requestBody']; req: Req; res: Res; } interface DeletePetResult_400 { - content: { 400: operations['deletePet']['responses']['400']['content'] }; + content: { 400: paths['/pet/{petId}']['delete']['responses']['400']['content'] }; headers?: { [name: string]: any }; } @@ -174,14 +174,14 @@ export async function deletePet_unimplemented(): DeletePetResult { } export interface UploadFileArgs { - parameters: operations['uploadFile']['parameters']; - requestBody: operations['uploadFile']['requestBody']; + parameters: paths['/pet/{petId}/uploadImage']['post']['parameters']; + requestBody: paths['/pet/{petId}/uploadImage']['post']['requestBody']; req: Req; res: Res; } interface UploadFileResult_200 { - content: { 200: operations['uploadFile']['responses']['200']['content'] }; + content: { 200: paths['/pet/{petId}/uploadImage']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } @@ -192,14 +192,14 @@ export async function uploadFile_unimplemented(): UploadFileResult { } export interface GetInventoryArgs { - parameters: operations['getInventory']['parameters']; - requestBody: operations['getInventory']['requestBody']; + parameters: paths['/store/inventory']['get']['parameters']; + requestBody: paths['/store/inventory']['get']['requestBody']; req: Req; res: Res; } interface GetInventoryResult_200 { - content: { 200: operations['getInventory']['responses']['200']['content'] }; + content: { 200: paths['/store/inventory']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } @@ -210,19 +210,19 @@ export async function getInventory_unimplemented(): GetInventoryResult { } export interface PlaceOrderArgs { - parameters: operations['placeOrder']['parameters']; - requestBody: operations['placeOrder']['requestBody']; + parameters: paths['/store/order']['post']['parameters']; + requestBody: paths['/store/order']['post']['requestBody']; req: Req; res: Res; } interface PlaceOrderResult_200 { - content: { 200: operations['placeOrder']['responses']['200']['content'] }; + content: { 200: paths['/store/order']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface PlaceOrderResult_405 { - content: { 405: operations['placeOrder']['responses']['405']['content'] }; + content: { 405: paths['/store/order']['post']['responses']['405']['content'] }; headers?: { [name: string]: any }; } @@ -233,24 +233,24 @@ export async function placeOrder_unimplemented(): PlaceOrderResult { } export interface GetOrderByIdArgs { - parameters: operations['getOrderById']['parameters']; - requestBody: operations['getOrderById']['requestBody']; + parameters: paths['/store/order/{orderId}']['get']['parameters']; + requestBody: paths['/store/order/{orderId}']['get']['requestBody']; req: Req; res: Res; } interface GetOrderByIdResult_200 { - content: { 200: operations['getOrderById']['responses']['200']['content'] }; + content: { 200: paths['/store/order/{orderId}']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface GetOrderByIdResult_400 { - content: { 400: operations['getOrderById']['responses']['400']['content'] }; + content: { 400: paths['/store/order/{orderId}']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface GetOrderByIdResult_404 { - content: { 404: operations['getOrderById']['responses']['404']['content'] }; + content: { 404: paths['/store/order/{orderId}']['get']['responses']['404']['content'] }; headers?: { [name: string]: any }; } @@ -261,19 +261,19 @@ export async function getOrderById_unimplemented(): GetOrderByIdResult { } export interface DeleteOrderArgs { - parameters: operations['deleteOrder']['parameters']; - requestBody: operations['deleteOrder']['requestBody']; + parameters: paths['/store/order/{orderId}']['delete']['parameters']; + requestBody: paths['/store/order/{orderId}']['delete']['requestBody']; req: Req; res: Res; } interface DeleteOrderResult_400 { - content: { 400: operations['deleteOrder']['responses']['400']['content'] }; + content: { 400: paths['/store/order/{orderId}']['delete']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface DeleteOrderResult_404 { - content: { 404: operations['deleteOrder']['responses']['404']['content'] }; + content: { 404: paths['/store/order/{orderId}']['delete']['responses']['404']['content'] }; headers?: { [name: string]: any }; } @@ -284,14 +284,14 @@ export async function deleteOrder_unimplemented(): DeleteOrderResult { } export interface CreateUserArgs { - parameters: operations['createUser']['parameters']; - requestBody: operations['createUser']['requestBody']; + parameters: paths['/user']['post']['parameters']; + requestBody: paths['/user']['post']['requestBody']; req: Req; res: Res; } interface CreateUserResult_default { - content: { default: operations['createUser']['responses']['default']['content'] }; + content: { default: paths['/user']['post']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -303,19 +303,19 @@ export async function createUser_unimplemented(): CreateUserResult { } export interface CreateUsersWithListInputArgs { - parameters: operations['createUsersWithListInput']['parameters']; - requestBody: operations['createUsersWithListInput']['requestBody']; + parameters: paths['/user/createWithList']['post']['parameters']; + requestBody: paths['/user/createWithList']['post']['requestBody']; req: Req; res: Res; } interface CreateUsersWithListInputResult_200 { - content: { 200: operations['createUsersWithListInput']['responses']['200']['content'] }; + content: { 200: paths['/user/createWithList']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface CreateUsersWithListInputResult_default { - content: { default: operations['createUsersWithListInput']['responses']['default']['content'] }; + content: { default: paths['/user/createWithList']['post']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -327,19 +327,19 @@ export async function createUsersWithListInput_unimplemented(): CreateUsersWithL } export interface LoginUserArgs { - parameters: operations['loginUser']['parameters']; - requestBody: operations['loginUser']['requestBody']; + parameters: paths['/user/login']['get']['parameters']; + requestBody: paths['/user/login']['get']['requestBody']; req: Req; res: Res; } interface LoginUserResult_200 { - content: { 200: operations['loginUser']['responses']['200']['content'] }; + content: { 200: paths['/user/login']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface LoginUserResult_400 { - content: { 400: operations['loginUser']['responses']['400']['content'] }; + content: { 400: paths['/user/login']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } @@ -350,14 +350,14 @@ export async function loginUser_unimplemented(): LoginUserResult { } export interface LogoutUserArgs { - parameters: operations['logoutUser']['parameters']; - requestBody: operations['logoutUser']['requestBody']; + parameters: paths['/user/logout']['get']['parameters']; + requestBody: paths['/user/logout']['get']['requestBody']; req: Req; res: Res; } interface LogoutUserResult_default { - content: { default: operations['logoutUser']['responses']['default']['content'] }; + content: { default: paths['/user/logout']['get']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -369,24 +369,24 @@ export async function logoutUser_unimplemented(): LogoutUserResult { } export interface GetUserByNameArgs { - parameters: operations['getUserByName']['parameters']; - requestBody: operations['getUserByName']['requestBody']; + parameters: paths['/user/{username}']['get']['parameters']; + requestBody: paths['/user/{username}']['get']['requestBody']; req: Req; res: Res; } interface GetUserByNameResult_200 { - content: { 200: operations['getUserByName']['responses']['200']['content'] }; + content: { 200: paths['/user/{username}']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface GetUserByNameResult_400 { - content: { 400: operations['getUserByName']['responses']['400']['content'] }; + content: { 400: paths['/user/{username}']['get']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface GetUserByNameResult_404 { - content: { 404: operations['getUserByName']['responses']['404']['content'] }; + content: { 404: paths['/user/{username}']['get']['responses']['404']['content'] }; headers?: { [name: string]: any }; } @@ -397,14 +397,14 @@ export async function getUserByName_unimplemented(): GetUserByNameResult { } export interface UpdateUserArgs { - parameters: operations['updateUser']['parameters']; - requestBody: operations['updateUser']['requestBody']; + parameters: paths['/user/{username}']['put']['parameters']; + requestBody: paths['/user/{username}']['put']['requestBody']; req: Req; res: Res; } interface UpdateUserResult_default { - content: { default: operations['updateUser']['responses']['default']['content'] }; + content: { default: paths['/user/{username}']['put']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -416,19 +416,19 @@ export async function updateUser_unimplemented(): UpdateUserResult { } export interface DeleteUserArgs { - parameters: operations['deleteUser']['parameters']; - requestBody: operations['deleteUser']['requestBody']; + parameters: paths['/user/{username}']['delete']['parameters']; + requestBody: paths['/user/{username}']['delete']['requestBody']; req: Req; res: Res; } interface DeleteUserResult_400 { - content: { 400: operations['deleteUser']['responses']['400']['content'] }; + content: { 400: paths['/user/{username}']['delete']['responses']['400']['content'] }; headers?: { [name: string]: any }; } interface DeleteUserResult_404 { - content: { 404: operations['deleteUser']['responses']['404']['content'] }; + content: { 404: paths['/user/{username}']['delete']['responses']['404']['content'] }; headers?: { [name: string]: any }; } diff --git a/examples/simple/gen/server.ts b/examples/simple/gen/server.ts index 21eeddb..ba800f0 100644 --- a/examples/simple/gen/server.ts +++ b/examples/simple/gen/server.ts @@ -3,24 +3,24 @@ * Do not make direct changes to the file. */ -import type { operations } from "./schema.d.ts"; +import type { paths } from "./schema.d.ts"; import type { Route } from "openapi-typescript-server"; import { NotImplementedError } from "openapi-typescript-server"; export interface ListPetsArgs { - parameters: operations['listPets']['parameters']; - requestBody: operations['listPets']['requestBody']; + parameters: paths['/pets']['get']['parameters']; + requestBody: paths['/pets']['get']['requestBody']; req: Req; res: Res; } interface ListPetsResult_200 { - content: { 200: operations['listPets']['responses']['200']['content'] }; + content: { 200: paths['/pets']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface ListPetsResult_default { - content: { default: operations['listPets']['responses']['default']['content'] }; + content: { default: paths['/pets']['get']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -32,19 +32,19 @@ export async function listPets_unimplemented(): ListPetsResult { } export interface GetPetByIdArgs { - parameters: operations['getPetById']['parameters']; - requestBody: operations['getPetById']['requestBody']; + parameters: paths['/pet/{petId}']['get']['parameters']; + requestBody: paths['/pet/{petId}']['get']['requestBody']; req: Req; res: Res; } interface GetPetByIdResult_200 { - content: { 200: operations['getPetById']['responses']['200']['content'] }; + content: { 200: paths['/pet/{petId}']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface GetPetByIdResult_default { - content: { default: operations['getPetById']['responses']['default']['content'] }; + content: { default: paths['/pet/{petId}']['get']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } @@ -56,19 +56,19 @@ export async function getPetById_unimplemented(): GetPetByIdResult { } export interface UpdatePetWithFormArgs { - parameters: operations['updatePetWithForm']['parameters']; - requestBody: operations['updatePetWithForm']['requestBody']; + parameters: paths['/pet/{petId}']['post']['parameters']; + requestBody: paths['/pet/{petId}']['post']['requestBody']; req: Req; res: Res; } interface UpdatePetWithFormResult_200 { - content: { 200: operations['updatePetWithForm']['responses']['200']['content'] }; + content: { 200: paths['/pet/{petId}']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } interface UpdatePetWithFormResult_default { - content: { default: operations['updatePetWithForm']['responses']['default']['content'] }; + content: { default: paths['/pet/{petId}']['post']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } diff --git a/packages/openapi-typescript-server/bin/index.cjs b/packages/openapi-typescript-server/bin/index.cjs index 2b11aec..c301e24 100755 --- a/packages/openapi-typescript-server/bin/index.cjs +++ b/packages/openapi-typescript-server/bin/index.cjs @@ -113,7 +113,7 @@ function generate(spec, types, outdir) { overwrite: true }); sourceFile.addImportDeclaration({ - namedImports: ["operations"], + namedImports: ["paths"], moduleSpecifier: types, isTypeOnly: true }); @@ -131,21 +131,26 @@ function generate(spec, types, outdir) { const pathSpec = spec.paths[path]; for (const method in pathSpec) { const operation = pathSpec[method]; - if (!(operation == null ? void 0 : operation.operationId)) { - throw new Error("Operation without operationId not implemented"); + if (!operation) { + throw new Error("no operation"); } + const operationId = getOperationId({ + operationId: operation.operationId, + path, + method + }); const argsInterface = sourceFile.addInterface({ - name: `${capitalize(operation.operationId)}Args`, + name: `${capitalize(operationId)}Args`, isExported: true, typeParameters: [{ name: "Req" }, { name: "Res" }], properties: [ { name: "parameters", - type: `operations['${operation.operationId}']['parameters']` + type: `paths['${path}']['${method}']['parameters']` }, { name: "requestBody", - type: `operations['${operation.operationId}']['requestBody']` + type: `paths['${path}']['${method}']['requestBody']` }, { name: "req", @@ -162,7 +167,7 @@ function generate(spec, types, outdir) { const responseVariantProperties = [ { name: "content", - type: `{${responseVariant}: operations['${operation.operationId}']['responses']['${responseVariant}']['content']}` + type: `{${responseVariant}: paths['${path}']['${method}']['responses']['${responseVariant}']['content']}` }, { name: "headers", @@ -177,24 +182,24 @@ function generate(spec, types, outdir) { }); } const responseVariantInterface = sourceFile.addInterface({ - name: `${capitalize(operation.operationId)}Result_${responseVariant}`, + name: `${capitalize(operationId)}Result_${responseVariant}`, properties: responseVariantProperties }); responseVariantInterfaceNames.push(responseVariantInterface.getName()); } const resultType = sourceFile.addTypeAlias({ - name: `${capitalize(operation.operationId)}Result`, + name: `${capitalize(operationId)}Result`, isExported: true, type: `Promise<${responseVariantInterfaceNames.join(" | ")}>` }); - operationsById[operation.operationId] = { + operationsById[operationId] = { path, method, args: argsInterface.getName(), result: resultType.getName() }; sourceFile.addFunction({ - name: `${operation.operationId}_unimplemented`, + name: `${operationId}_unimplemented`, isExported: true, isAsync: true, returnType: resultType.getName(), @@ -264,6 +269,17 @@ function generate(spec, types, outdir) { function capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } +function getOperationId({ + operationId, + path, + method +}) { + if (operationId) { + return operationId; + } + const pathParts = path.replace("{", "").replace("}", "").split("/").map((part) => capitalize(part)).join(""); + return `${method}${pathParts}`; +} // src/cli/index.ts var program = new import_commander.Command(); diff --git a/packages/openapi-typescript-server/src/cli/generate.test.ts b/packages/openapi-typescript-server/src/cli/generate.test.ts index 78dd7be..61ea0d6 100644 --- a/packages/openapi-typescript-server/src/cli/generate.test.ts +++ b/packages/openapi-typescript-server/src/cli/generate.test.ts @@ -1,9 +1,8 @@ import { describe, it } from "node:test"; import assert from "node:assert"; import generate from "./generate.ts"; -import type { OpenAPISpec } from "../lib/schema.ts"; -const spec: OpenAPISpec = { +const spec = { openapi: "3.0.0", info: {}, paths: { @@ -49,9 +48,7 @@ const sourceFile = generate(spec, "./schema.d.ts", "outdir"); it("writes imports", () => { const operationsImport = sourceFile.getImportDeclaration("./schema.d.ts"); - assert.equal(operationsImport?.getNamedImports()[0]?.getName(), [ - "operations", - ]); + assert.equal(operationsImport?.getNamedImports()[0]?.getName(), ["paths"]); const serverImport = sourceFile.getImportDeclaration( "openapi-typescript-server", @@ -65,11 +62,11 @@ describe("with operationId", () => { assert.equal(argsInterface?.getTypeParameters().length, 2); assert.equal( argsInterface?.getProperty("parameters")?.getTypeNode()?.getText(), - "operations['getOperation']['parameters']", + "paths['/path']['get']['parameters']", ); assert.equal( argsInterface?.getProperty("requestBody")?.getTypeNode()?.getText(), - "operations['getOperation']['requestBody']", + "paths['/path']['get']['requestBody']", ); const result200Interface = sourceFile.getInterface( @@ -77,7 +74,7 @@ describe("with operationId", () => { ); assert.equal( result200Interface?.getProperty("content")?.getTypeNode()?.getText(), - "{ 200: operations['getOperation']['responses']['200']['content'] }", + "{ 200: paths['/path']['get']['responses']['200']['content'] }", ); const resultDefaultInterface = sourceFile.getInterface( @@ -85,7 +82,7 @@ describe("with operationId", () => { ); assert.equal( resultDefaultInterface?.getProperty("content")?.getTypeNode()?.getText(), - "{ default: operations['getOperation']['responses']['default']['content'] }", + "{ default: paths['/path']['get']['responses']['default']['content'] }", ); }); @@ -144,40 +141,77 @@ it("writes register handler", () => { }); describe("wihout operationId", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/something/{id}": { + get: { + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { + type: "string", + }, + }, + }, + }, + }, + }, + default: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + message: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; const modifiedSpec = structuredClone(spec); - delete modifiedSpec?.paths?.["/path"]?.get?.operationId; - // const sourceFile = generate(modifiedSpec, "./schema.d.ts", "outdir"); + const sourceFile = generate(modifiedSpec, "./schema.d.ts", "outdir"); - it("~writes function inputs and results~ throws an error", () => { - const argsInterface = sourceFile.getInterface("GetOperationArgs"); - assert.equal(argsInterface?.getTypeParameters().length, 2); - assert.throws(() => { - generate(modifiedSpec, "./schema.d.ts", "outdir"); - }); - - // assert.equal( - // argsInterface?.getProperty("parameters")?.getTypeNode()?.getText(), - // "operations['getOperation']['parameters']", - // ); - // assert.equal( - // argsInterface?.getProperty("requestBody")?.getTypeNode()?.getText(), - // "operations['getOperation']['requestBody']", - // ); - - // const result200Interface = sourceFile.getInterface( - // "GetOperationResult_200", - // ); - // assert.equal( - // result200Interface?.getProperty("content")?.getTypeNode()?.getText(), - // "{ 200: operations['getOperation']['responses']['200']['content'] }", - // ); - - // const resultDefaultInterface = sourceFile.getInterface( - // "GetOperationResult_default", - // ); - // assert.equal( - // resultDefaultInterface?.getProperty("content")?.getTypeNode()?.getText(), - // "{ default: operations['getOperation']['responses']['default']['content'] }", - // ); + it("writes function inputs and results", () => { + const argsInterface = sourceFile.getInterface("GetSomethingIdArgs"); + assert(argsInterface); + assert.equal(argsInterface.getTypeParameters().length, 2); + + assert.equal( + argsInterface?.getProperty("parameters")?.getTypeNode()?.getText(), + "paths['/something/{id}']['get']['parameters']", + ); + assert.equal( + argsInterface?.getProperty("requestBody")?.getTypeNode()?.getText(), + "paths['/something/{id}']['get']['requestBody']", + ); + + const result200Interface = sourceFile.getInterface( + "GetSomethingIdResult_200", + ); + assert.equal( + result200Interface?.getProperty("content")?.getTypeNode()?.getText(), + "{ 200: paths['/something/{id}']['get']['responses']['200']['content'] }", + ); + + const resultDefaultInterface = sourceFile.getInterface( + "GetSomethingIdResult_default", + ); + assert.equal( + resultDefaultInterface?.getProperty("content")?.getTypeNode()?.getText(), + "{ default: paths['/something/{id}']['get']['responses']['default']['content'] }", + ); }); }); diff --git a/packages/openapi-typescript-server/src/cli/generate.ts b/packages/openapi-typescript-server/src/cli/generate.ts index 5246805..e5d896a 100644 --- a/packages/openapi-typescript-server/src/cli/generate.ts +++ b/packages/openapi-typescript-server/src/cli/generate.ts @@ -1,11 +1,7 @@ import type { OpenAPISpec } from "../lib/schema"; import { Project } from "ts-morph"; -export default function generate( - spec: OpenAPISpec, - types: string, - outdir: string, -) { +export default function generate(spec: OpenAPISpec, types: string, outdir: string) { const project = new Project(); const sourceFile = project.createSourceFile(`${outdir}/server.ts`, "", { @@ -13,7 +9,7 @@ export default function generate( }); sourceFile.addImportDeclaration({ - namedImports: ["operations"], + namedImports: ["paths"], moduleSpecifier: types, isTypeOnly: true, }); @@ -38,22 +34,29 @@ export default function generate( const pathSpec = spec.paths[path]; for (const method in pathSpec) { const operation = pathSpec[method]; - if (!operation?.operationId) { - throw new Error("Operation without operationId not implemented"); + + if (!operation) { + throw new Error("no operation"); } + const operationId = getOperationId({ + operationId: operation.operationId, + path, + method, + }); + const argsInterface = sourceFile.addInterface({ - name: `${capitalize(operation.operationId)}Args`, + name: `${capitalize(operationId)}Args`, isExported: true, typeParameters: [{ name: "Req" }, { name: "Res" }], properties: [ { name: "parameters", - type: `operations['${operation.operationId}']['parameters']`, + type: `paths['${path}']['${method}']['parameters']`, }, { name: "requestBody", - type: `operations['${operation.operationId}']['requestBody']`, + type: `paths['${path}']['${method}']['requestBody']`, }, { name: "req", @@ -72,7 +75,7 @@ export default function generate( const responseVariantProperties = [ { name: "content", - type: `{${responseVariant}: operations['${operation.operationId}']['responses']['${responseVariant}']['content']}`, + type: `{${responseVariant}: paths['${path}']['${method}']['responses']['${responseVariant}']['content']}`, }, { name: "headers", @@ -88,7 +91,7 @@ export default function generate( }); } const responseVariantInterface = sourceFile.addInterface({ - name: `${capitalize(operation.operationId)}Result_${responseVariant}`, + name: `${capitalize(operationId)}Result_${responseVariant}`, properties: responseVariantProperties, }); @@ -96,12 +99,12 @@ export default function generate( } const resultType = sourceFile.addTypeAlias({ - name: `${capitalize(operation.operationId)}Result`, + name: `${capitalize(operationId)}Result`, isExported: true, type: `Promise<${responseVariantInterfaceNames.join(" | ")}>`, }); - operationsById[operation.operationId] = { + operationsById[operationId] = { path: path, method: method, args: argsInterface.getName(), @@ -109,7 +112,7 @@ export default function generate( }; sourceFile.addFunction({ - name: `${operation.operationId}_unimplemented`, + name: `${operationId}_unimplemented`, isExported: true, isAsync: true, returnType: resultType.getName(), @@ -128,7 +131,7 @@ export default function generate( args: ${args} ) => ${result}`, }; - }, + } ); const serverInterface = sourceFile.addInterface({ @@ -159,7 +162,7 @@ export default function generate( writer.writeLine(`path: "${path}",`); writer.writeLine(`handler: server.${operationId},`); writer.writeLine("},"); - }, + } ); writer.writeLine("]"); @@ -173,7 +176,7 @@ export default function generate( * Do not make direct changes to the file. */ - `, + ` ); sourceFile.formatText({ @@ -188,3 +191,26 @@ export default function generate( function capitalize(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } + +function getOperationId({ + operationId, + path, + method, +}: { + operationId?: string; + path: string; + method: string; +}) { + if (operationId) { + return operationId; + } + + const pathParts = path + .replace("{", "") + .replace("}", "") + .split("/") + .map((part) => capitalize(part)) + .join(""); + + return `${method}${pathParts}`; +} \ No newline at end of file From 812decaa473c4a526a7cac34806a43dcfa27c404 Mon Sep 17 00:00:00 2001 From: jasonblanchard Date: Thu, 20 Mar 2025 22:24:54 -0400 Subject: [PATCH 2/2] fix formatting --- .../openapi-typescript-server/src/cli/generate.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/openapi-typescript-server/src/cli/generate.ts b/packages/openapi-typescript-server/src/cli/generate.ts index e5d896a..dfc6e6e 100644 --- a/packages/openapi-typescript-server/src/cli/generate.ts +++ b/packages/openapi-typescript-server/src/cli/generate.ts @@ -1,7 +1,11 @@ import type { OpenAPISpec } from "../lib/schema"; import { Project } from "ts-morph"; -export default function generate(spec: OpenAPISpec, types: string, outdir: string) { +export default function generate( + spec: OpenAPISpec, + types: string, + outdir: string, +) { const project = new Project(); const sourceFile = project.createSourceFile(`${outdir}/server.ts`, "", { @@ -131,7 +135,7 @@ export default function generate(spec: OpenAPISpec, types: string, outdir: strin args: ${args} ) => ${result}`, }; - } + }, ); const serverInterface = sourceFile.addInterface({ @@ -162,7 +166,7 @@ export default function generate(spec: OpenAPISpec, types: string, outdir: strin writer.writeLine(`path: "${path}",`); writer.writeLine(`handler: server.${operationId},`); writer.writeLine("},"); - } + }, ); writer.writeLine("]"); @@ -176,7 +180,7 @@ export default function generate(spec: OpenAPISpec, types: string, outdir: strin * Do not make direct changes to the file. */ - ` + `, ); sourceFile.formatText({ @@ -213,4 +217,4 @@ function getOperationId({ .join(""); return `${method}${pathParts}`; -} \ No newline at end of file +}