From 9ef259981b939a8dc3c36c64ead70fecb8bf75dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:32:20 +0000 Subject: [PATCH 01/12] Initial plan From e8cc9ba7c3e670f908538c6e922e7f176f3ed720 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:41:52 +0000 Subject: [PATCH 02/12] Add registerRouteHandlersByTag function and Tag type generation Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/docs/gen/server.ts | 27 ++++ examples/kitchensink/gen/server.ts | 55 +++++++ examples/petstore/gen/server.ts | 150 ++++++++++++++++++ .../src/schema.ts | 1 + .../bin/cli/index.cjs | 75 ++++++++- .../src/cli/generate.ts | 96 +++++++++++ 6 files changed, 403 insertions(+), 1 deletion(-) diff --git a/examples/docs/gen/server.ts b/examples/docs/gen/server.ts index b5cab07..f4e7a0b 100644 --- a/examples/docs/gen/server.ts +++ b/examples/docs/gen/server.ts @@ -72,3 +72,30 @@ export function registerRouteHandlers(server: Server): Route }, ] } + +export type Tag = null; + +export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { + const routes: Route[] = []; + + switch (tag) { + case null: + if (server.makePetSpeak) { + routes.push({ + method: "post", + path: "/speak/{petId}", + handler: server.makePetSpeak as Route["handler"], + }); + } + if (server.uhoh) { + routes.push({ + method: "get", + path: "/uhoh", + handler: server.uhoh as Route["handler"], + }); + } + break; + } + + return routes; +} diff --git a/examples/kitchensink/gen/server.ts b/examples/kitchensink/gen/server.ts index 5dac4bc..e0982da 100644 --- a/examples/kitchensink/gen/server.ts +++ b/examples/kitchensink/gen/server.ts @@ -250,3 +250,58 @@ export function registerRouteHandlers(server: Server): Route }, ] } + +export type Tag = null; + +export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { + const routes: Route[] = []; + + switch (tag) { + case null: + if (server.listPets) { + routes.push({ + method: "get", + path: "/pets", + handler: server.listPets as Route["handler"], + }); + } + if (server.getPetById) { + routes.push({ + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }); + } + if (server.updatePetWithForm) { + routes.push({ + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }); + } + if (server.mixedContentTypes) { + routes.push({ + method: "post", + path: "/pet/{petId}/mixed-content-types", + handler: server.mixedContentTypes as Route["handler"], + }); + } + if (server.getPetImage) { + routes.push({ + method: "get", + path: "/pet/{petId}/image", + handler: server.getPetImage as Route["handler"], + }); + } + if (server.getPetWebpage) { + routes.push({ + method: "get", + path: "/pet/{petId}/webpage", + handler: server.getPetWebpage as Route["handler"], + }); + } + break; + } + + return routes; +} diff --git a/examples/petstore/gen/server.ts b/examples/petstore/gen/server.ts index 8ebff13..88712e7 100644 --- a/examples/petstore/gen/server.ts +++ b/examples/petstore/gen/server.ts @@ -743,3 +743,153 @@ export function registerRouteHandlers(server: Server): Route }, ] } + +export type Tag = "pet" | "store" | "user"; + +export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { + const routes: Route[] = []; + + switch (tag) { + case "pet": + if (server.updatePet) { + routes.push({ + method: "put", + path: "/pet", + handler: server.updatePet as Route["handler"], + }); + } + if (server.addPet) { + routes.push({ + method: "post", + path: "/pet", + handler: server.addPet as Route["handler"], + }); + } + if (server.findPetsByStatus) { + routes.push({ + method: "get", + path: "/pet/findByStatus", + handler: server.findPetsByStatus as Route["handler"], + }); + } + if (server.findPetsByTags) { + routes.push({ + method: "get", + path: "/pet/findByTags", + handler: server.findPetsByTags as Route["handler"], + }); + } + if (server.getPetById) { + routes.push({ + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }); + } + if (server.updatePetWithForm) { + routes.push({ + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }); + } + if (server.deletePet) { + routes.push({ + method: "delete", + path: "/pet/{petId}", + handler: server.deletePet as Route["handler"], + }); + } + if (server.uploadFile) { + routes.push({ + method: "post", + path: "/pet/{petId}/uploadImage", + handler: server.uploadFile as Route["handler"], + }); + } + break; + case "store": + if (server.getInventory) { + routes.push({ + method: "get", + path: "/store/inventory", + handler: server.getInventory as Route["handler"], + }); + } + if (server.placeOrder) { + routes.push({ + method: "post", + path: "/store/order", + handler: server.placeOrder as Route["handler"], + }); + } + if (server.getOrderById) { + routes.push({ + method: "get", + path: "/store/order/{orderId}", + handler: server.getOrderById as Route["handler"], + }); + } + if (server.deleteOrder) { + routes.push({ + method: "delete", + path: "/store/order/{orderId}", + handler: server.deleteOrder as Route["handler"], + }); + } + break; + case "user": + if (server.createUser) { + routes.push({ + method: "post", + path: "/user", + handler: server.createUser as Route["handler"], + }); + } + if (server.createUsersWithListInput) { + routes.push({ + method: "post", + path: "/user/createWithList", + handler: server.createUsersWithListInput as Route["handler"], + }); + } + if (server.loginUser) { + routes.push({ + method: "get", + path: "/user/login", + handler: server.loginUser as Route["handler"], + }); + } + if (server.logoutUser) { + routes.push({ + method: "get", + path: "/user/logout", + handler: server.logoutUser as Route["handler"], + }); + } + if (server.getUserByName) { + routes.push({ + method: "get", + path: "/user/{username}", + handler: server.getUserByName as Route["handler"], + }); + } + if (server.updateUser) { + routes.push({ + method: "put", + path: "/user/{username}", + handler: server.updateUser as Route["handler"], + }); + } + if (server.deleteUser) { + routes.push({ + method: "delete", + path: "/user/{username}", + handler: server.deleteUser as Route["handler"], + }); + } + break; + } + + return routes; +} diff --git a/packages/openapi-typescript-server-runtime/src/schema.ts b/packages/openapi-typescript-server-runtime/src/schema.ts index 6b7645e..25bde22 100644 --- a/packages/openapi-typescript-server-runtime/src/schema.ts +++ b/packages/openapi-typescript-server-runtime/src/schema.ts @@ -29,6 +29,7 @@ export const OpenAPISpec = z.object({ summary: z.string().optional(), description: z.string().optional(), operationId: z.string().optional(), + tags: z.array(z.string()).optional(), parameters: z .array( z.object({ diff --git a/packages/openapi-typescript-server/bin/cli/index.cjs b/packages/openapi-typescript-server/bin/cli/index.cjs index b48428a..edcccb4 100755 --- a/packages/openapi-typescript-server/bin/cli/index.cjs +++ b/packages/openapi-typescript-server/bin/cli/index.cjs @@ -319,6 +319,7 @@ function generate(spec, types, outpath, version) { moduleSpecifier: "openapi-typescript-server-runtime" }); const operationsById = {}; + const allTags = /* @__PURE__ */ new Set(); for (const path in spec.paths) { const pathSpec = spec.paths[path]; for (const method in pathSpec) { @@ -408,13 +409,16 @@ function generate(spec, types, outpath, version) { isExported: true, type: `Promise<${responseVariantInterfaceNames.join(" | ")}>` }); + const operationTags = operation.tags || []; + operationTags.forEach((tag) => allTags.add(tag)); operationsById[operationId] = { path, method, args: argsInterface.getName(), result: resultType.getName(), summary: operation.summary, - description: operation.description + description: operation.description, + tags: operationTags }; sourceFile.addFunction({ name: `${operationId}Unimplemented`, @@ -481,6 +485,75 @@ function generate(spec, types, outpath, version) { writer.writeLine("]"); } }); + const tagValues = Array.from(allTags).map((tag) => `"${tag}"`); + const hasUntaggedOperations = Object.values(operationsById).some( + (op) => !op.tags || op.tags.length === 0 + ); + if (hasUntaggedOperations) { + tagValues.push("null"); + } + if (tagValues.length > 0) { + sourceFile.addTypeAlias({ + name: "Tag", + isExported: true, + type: tagValues.join(" | ") + }); + } + const tagToOperations = {}; + Object.entries(operationsById).forEach(([operationId, { tags }]) => { + if (!tags || tags.length === 0) { + if (!tagToOperations["null"]) { + tagToOperations["null"] = []; + } + tagToOperations["null"].push(operationId); + } else { + tags.forEach((tag) => { + if (!tagToOperations[tag]) { + tagToOperations[tag] = []; + } + tagToOperations[tag].push(operationId); + }); + } + }); + sourceFile.addFunction({ + name: "registerRouteHandlersByTag", + isExported: true, + parameters: [ + { name: "tag", type: "Tag" }, + { name: "server", type: "Partial>" } + ], + typeParameters: [{ name: "Req" }, { name: "Res" }], + returnType: "Route[]", + statements: (writer) => { + writer.writeLine("const routes: Route[] = [];"); + writer.writeLine(""); + if (Object.keys(tagToOperations).length > 0) { + writer.writeLine("switch (tag) {"); + Object.entries(tagToOperations).forEach(([tag, operations]) => { + const caseValue = tag === "null" ? "null" : `"${tag}"`; + writer.writeLine(`case ${caseValue}:`); + operations.forEach((operationId) => { + const op = operationsById[operationId]; + if (op) { + writer.writeLine(`if (server.${operationId}) {`); + writer.writeLine("routes.push({"); + writer.writeLine(`method: "${op.method}",`); + writer.writeLine(`path: "${op.path}",`); + writer.writeLine( + `handler: server.${operationId} as Route["handler"],` + ); + writer.writeLine("});"); + writer.writeLine("}"); + } + }); + writer.writeLine("break;"); + }); + writer.writeLine("}"); + } + writer.writeLine(""); + writer.writeLine("return routes;"); + } + }); sourceFile.insertText( 0, `/** diff --git a/packages/openapi-typescript-server/src/cli/generate.ts b/packages/openapi-typescript-server/src/cli/generate.ts index 80b1c38..37aebca 100644 --- a/packages/openapi-typescript-server/src/cli/generate.ts +++ b/packages/openapi-typescript-server/src/cli/generate.ts @@ -39,9 +39,12 @@ export default function generate( result: string; summary?: string; description?: string; + tags?: string[]; } > = {}; + const allTags = new Set(); + for (const path in spec.paths) { const pathSpec = spec.paths[path]; for (const method in pathSpec) { @@ -144,6 +147,10 @@ export default function generate( type: `Promise<${responseVariantInterfaceNames.join(" | ")}>`, }); + // Track tags for this operation + const operationTags = operation.tags || []; + operationTags.forEach((tag) => allTags.add(tag)); + operationsById[operationId] = { path: path, method: method, @@ -151,6 +158,7 @@ export default function generate( result: resultType.getName(), summary: operation.summary, description: operation.description, + tags: operationTags, }; sourceFile.addFunction({ @@ -227,6 +235,94 @@ export default function generate( }, }); + // Generate Tag type union + const tagValues = Array.from(allTags).map((tag) => `"${tag}"`); + // Check if there are any untagged operations + const hasUntaggedOperations = Object.values(operationsById).some( + (op) => !op.tags || op.tags.length === 0, + ); + if (hasUntaggedOperations) { + tagValues.push("null"); + } + + if (tagValues.length > 0) { + sourceFile.addTypeAlias({ + name: "Tag", + isExported: true, + type: tagValues.join(" | "), + }); + } + + // Generate interfaces for partial servers by tag + const tagToOperations: Record = {}; + + // Group operations by tag + Object.entries(operationsById).forEach(([operationId, { tags }]) => { + if (!tags || tags.length === 0) { + // Untagged operations + if (!tagToOperations["null"]) { + tagToOperations["null"] = []; + } + tagToOperations["null"].push(operationId); + } else { + // Add operation to each tag it belongs to + tags.forEach((tag) => { + if (!tagToOperations[tag]) { + tagToOperations[tag] = []; + } + tagToOperations[tag].push(operationId); + }); + } + }); + + // Generate registerRouteHandlersByTag function + sourceFile.addFunction({ + name: "registerRouteHandlersByTag", + isExported: true, + parameters: [ + { name: "tag", type: "Tag" }, + { name: "server", type: "Partial>" }, + ], + typeParameters: [{ name: "Req" }, { name: "Res" }], + returnType: "Route[]", + statements: (writer) => { + writer.writeLine("const routes: Route[] = [];"); + writer.writeLine(""); + + // Generate switch statement for each tag + if (Object.keys(tagToOperations).length > 0) { + writer.writeLine("switch (tag) {"); + + Object.entries(tagToOperations).forEach(([tag, operations]) => { + const caseValue = tag === "null" ? "null" : `"${tag}"`; + writer.writeLine(`case ${caseValue}:`); + + operations.forEach((operationId) => { + const op = operationsById[operationId]; + if (op) { + writer.writeLine(`if (server.${operationId}) {`); + writer.writeLine("routes.push({"); + writer.writeLine(`method: "${op.method}",`); + writer.writeLine(`path: "${op.path}",`); + writer.writeLine( + `handler: server.${operationId} as Route["handler"],`, + ); + writer.writeLine("});"); + writer.writeLine("}"); + } + }); + + writer.writeLine("break;"); + }); + + writer.writeLine("}"); + } + + writer.writeLine(""); + writer.writeLine("return routes;"); + }, + }); + sourceFile.insertText( 0, `/** From 5bf59e02ce54ee81dda681b245ec551066901595 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:45:48 +0000 Subject: [PATCH 03/12] Add comprehensive tests for registerRouteHandlersByTag Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- .../src/cli/generate.test.ts | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) diff --git a/packages/openapi-typescript-server/src/cli/generate.test.ts b/packages/openapi-typescript-server/src/cli/generate.test.ts index 6a46b03..36d9a80 100644 --- a/packages/openapi-typescript-server/src/cli/generate.test.ts +++ b/packages/openapi-typescript-server/src/cli/generate.test.ts @@ -212,6 +212,295 @@ describe("wihout operationId", () => { }); }); +describe("tags", () => { + it("generates Tag type with all tags from spec", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + tags: ["pet"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + "/users": { + get: { + operationId: "listUsers", + tags: ["user"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const tagType = sourceFile.getTypeAlias("Tag"); + assert(tagType); + assert(tagType.isExported()); + const tagTypeText = tagType.getType().getText(); + assert.match(tagTypeText, /"pet"/); + assert.match(tagTypeText, /"user"/); + }); + + it("generates Tag type with null for untagged operations", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const tagType = sourceFile.getTypeAlias("Tag"); + assert(tagType); + assert(tagType.isExported()); + const tagTypeText = tagType.getType().getText(); + assert.equal(tagTypeText, "null"); + }); + + it("generates Tag type with both tags and null for mixed operations", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + tags: ["pet"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + "/status": { + get: { + operationId: "getStatus", + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const tagType = sourceFile.getTypeAlias("Tag"); + assert(tagType); + const tagTypeText = tagType.getType().getText(); + assert.match(tagTypeText, /"pet"/); + assert.match(tagTypeText, /null/); + }); +}); + +describe("registerRouteHandlersByTag", () => { + it("generates function with correct signature", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + tags: ["pet"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const func = sourceFile.getFunction("registerRouteHandlersByTag"); + assert(func); + assert(func.isExported()); + assert.equal(func.getTypeParameters().length, 2); + assert.equal(func.getParameters().length, 2); + + const tagParam = func.getParameters()[0]; + assert(tagParam); + assert.equal(tagParam.getName(), "tag"); + assert.equal(tagParam.getType().getText(), '"pet"'); + + const serverParam = func.getParameters()[1]; + assert(serverParam); + assert.equal(serverParam.getName(), "server"); + assert.match(serverParam.getType().getText(), /Partial>/); + + assert.match(func.getReturnType().getText(), /Route\[\]/); + }); + + it("generates switch statement for each tag", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + tags: ["pet"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + "/users": { + get: { + operationId: "listUsers", + tags: ["user"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const func = sourceFile.getFunction("registerRouteHandlersByTag"); + const bodyText = func?.getBodyText() || ""; + + assert.match(bodyText, /switch \(tag\)/); + assert.match(bodyText, /case "pet":/); + assert.match(bodyText, /case "user":/); + assert.match(bodyText, /if \(server\.listPets\)/); + assert.match(bodyText, /if \(server\.listUsers\)/); + }); + + it("groups operations by tag correctly", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/pets": { + get: { + operationId: "listPets", + tags: ["pet"], + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + post: { + operationId: "createPet", + tags: ["pet"], + responses: { + 201: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const func = sourceFile.getFunction("registerRouteHandlersByTag"); + const bodyText = func?.getBodyText() || ""; + + // Both operations should be in the same case block + const petCaseMatch = bodyText.match(/case "pet":[\s\S]*?break;/); + assert(petCaseMatch); + const petCaseBlock = petCaseMatch[0]; + assert.match(petCaseBlock, /if \(server\.listPets\)/); + assert.match(petCaseBlock, /if \(server\.createPet\)/); + }); + + it("handles untagged operations with null case", () => { + const spec = { + openapi: "3.0.0", + info: {}, + paths: { + "/status": { + get: { + operationId: "getStatus", + responses: { + 200: { + content: { + "application/json": { + schema: { type: "object" }, + }, + }, + }, + }, + }, + }, + }, + }; + const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); + const func = sourceFile.getFunction("registerRouteHandlersByTag"); + const bodyText = func?.getBodyText() || ""; + + assert.match(bodyText, /case null:/); + assert.match(bodyText, /if \(server\.getStatus\)/); + }); +}); + describe("request body", () => { it("writes required request body", () => { const spec = { From 78e3c25f1e4d6e39e5b50b437e4499d2f06f9cd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:18:58 +0000 Subject: [PATCH 04/12] Add strict type checking for registerRouteHandlersByTag Generate ServerForTag interfaces and function overloads to enforce that only operations with the specified tag can be passed to registerRouteHandlersByTag Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/docs/gen/server.ts | 6 +++ examples/kitchensink/gen/server.ts | 10 ++++ examples/petstore/gen/server.ts | 31 +++++++++++++ .../bin/cli/index.cjs | 36 ++++++++++++++- .../src/cli/generate.ts | 46 ++++++++++++++++++- 5 files changed, 126 insertions(+), 3 deletions(-) diff --git a/examples/docs/gen/server.ts b/examples/docs/gen/server.ts index f4e7a0b..65fbb3d 100644 --- a/examples/docs/gen/server.ts +++ b/examples/docs/gen/server.ts @@ -75,6 +75,12 @@ export function registerRouteHandlers(server: Server): Route export type Tag = null; +export interface ServerForUntagged { + makePetSpeak?: (args: MakePetSpeakArgs) => MakePetSpeakResult; + uhoh?: (args: UhohArgs) => UhohResult; +} + +export function registerRouteHandlersByTag(tag: null, server: Partial>): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; diff --git a/examples/kitchensink/gen/server.ts b/examples/kitchensink/gen/server.ts index e0982da..ba23236 100644 --- a/examples/kitchensink/gen/server.ts +++ b/examples/kitchensink/gen/server.ts @@ -253,6 +253,16 @@ export function registerRouteHandlers(server: Server): Route export type Tag = null; +export interface ServerForUntagged { + listPets?: (args: ListPetsArgs) => ListPetsResult; + getPetById?: (args: GetPetByIdArgs) => GetPetByIdResult; + updatePetWithForm?: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; + mixedContentTypes?: (args: MixedContentTypesArgs) => MixedContentTypesResult; + getPetImage?: (args: GetPetImageArgs) => GetPetImageResult; + getPetWebpage?: (args: GetPetWebpageArgs) => GetPetWebpageResult; +} + +export function registerRouteHandlersByTag(tag: null, server: Partial>): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; diff --git a/examples/petstore/gen/server.ts b/examples/petstore/gen/server.ts index 88712e7..a1ceb78 100644 --- a/examples/petstore/gen/server.ts +++ b/examples/petstore/gen/server.ts @@ -746,6 +746,37 @@ export function registerRouteHandlers(server: Server): Route export type Tag = "pet" | "store" | "user"; +export interface ServerForPet { + updatePet?: (args: UpdatePetArgs) => UpdatePetResult; + addPet?: (args: AddPetArgs) => AddPetResult; + findPetsByStatus?: (args: FindPetsByStatusArgs) => FindPetsByStatusResult; + findPetsByTags?: (args: FindPetsByTagsArgs) => FindPetsByTagsResult; + getPetById?: (args: GetPetByIdArgs) => GetPetByIdResult; + updatePetWithForm?: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; + deletePet?: (args: DeletePetArgs) => DeletePetResult; + uploadFile?: (args: UploadFileArgs) => UploadFileResult; +} + +export interface ServerForStore { + getInventory?: (args: GetInventoryArgs) => GetInventoryResult; + placeOrder?: (args: PlaceOrderArgs) => PlaceOrderResult; + getOrderById?: (args: GetOrderByIdArgs) => GetOrderByIdResult; + deleteOrder?: (args: DeleteOrderArgs) => DeleteOrderResult; +} + +export interface ServerForUser { + createUser?: (args: CreateUserArgs) => CreateUserResult; + createUsersWithListInput?: (args: CreateUsersWithListInputArgs) => CreateUsersWithListInputResult; + loginUser?: (args: LoginUserArgs) => LoginUserResult; + logoutUser?: (args: LogoutUserArgs) => LogoutUserResult; + getUserByName?: (args: GetUserByNameArgs) => GetUserByNameResult; + updateUser?: (args: UpdateUserArgs) => UpdateUserResult; + deleteUser?: (args: DeleteUserArgs) => DeleteUserResult; +} + +export function registerRouteHandlersByTag(tag: "pet", server: Partial>): Route[]; +export function registerRouteHandlersByTag(tag: "store", server: Partial>): Route[]; +export function registerRouteHandlersByTag(tag: "user", server: Partial>): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; diff --git a/packages/openapi-typescript-server/bin/cli/index.cjs b/packages/openapi-typescript-server/bin/cli/index.cjs index edcccb4..0557a38 100755 --- a/packages/openapi-typescript-server/bin/cli/index.cjs +++ b/packages/openapi-typescript-server/bin/cli/index.cjs @@ -515,7 +515,29 @@ function generate(spec, types, outpath, version) { }); } }); - sourceFile.addFunction({ + Object.entries(tagToOperations).forEach(([tag, operations]) => { + const interfaceName = tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`; + const tagInterface = sourceFile.addInterface({ + name: interfaceName, + isExported: true, + typeParameters: [ + { name: "Req", default: "unknown" }, + { name: "Res", default: "unknown" } + ] + }); + operations.forEach((operationId) => { + const op = operationsById[operationId]; + if (op) { + tagInterface.addProperty({ + name: operationId, + type: `(args: ${op.args}) => ${op.result}`, + hasQuestionToken: true + // Make it optional since it's a partial implementation + }); + } + }); + }); + const registerByTagFunc = sourceFile.addFunction({ name: "registerRouteHandlersByTag", isExported: true, parameters: [ @@ -554,6 +576,18 @@ function generate(spec, types, outpath, version) { writer.writeLine("return routes;"); } }); + Object.entries(tagToOperations).forEach(([tag, _operations]) => { + const tagValue = tag === "null" ? "null" : `"${tag}"`; + const interfaceName = tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`; + registerByTagFunc.addOverload({ + parameters: [ + { name: "tag", type: tagValue }, + { name: "server", type: `Partial<${interfaceName}>` } + ], + typeParameters: [{ name: "Req" }, { name: "Res" }], + returnType: "Route[]" + }); + }); sourceFile.insertText( 0, `/** diff --git a/packages/openapi-typescript-server/src/cli/generate.ts b/packages/openapi-typescript-server/src/cli/generate.ts index 37aebca..ec6777e 100644 --- a/packages/openapi-typescript-server/src/cli/generate.ts +++ b/packages/openapi-typescript-server/src/cli/generate.ts @@ -275,8 +275,34 @@ export default function generate( } }); - // Generate registerRouteHandlersByTag function - sourceFile.addFunction({ + // Generate a ServerForTag interface for each tag + Object.entries(tagToOperations).forEach(([tag, operations]) => { + const interfaceName = + tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`; + const tagInterface = sourceFile.addInterface({ + name: interfaceName, + isExported: true, + typeParameters: [ + { name: "Req", default: "unknown" }, + { name: "Res", default: "unknown" }, + ], + }); + + // Add only operations associated with this tag + operations.forEach((operationId) => { + const op = operationsById[operationId]; + if (op) { + tagInterface.addProperty({ + name: operationId, + type: `(args: ${op.args}) => ${op.result}`, + hasQuestionToken: true, // Make it optional since it's a partial implementation + }); + } + }); + }); + + // Generate registerRouteHandlersByTag function with overloads + const registerByTagFunc = sourceFile.addFunction({ name: "registerRouteHandlersByTag", isExported: true, parameters: [ @@ -323,6 +349,22 @@ export default function generate( }, }); + // Add overload signatures for each tag + Object.entries(tagToOperations).forEach(([tag, _operations]) => { + const tagValue = tag === "null" ? "null" : `"${tag}"`; + const interfaceName = + tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`; + + registerByTagFunc.addOverload({ + parameters: [ + { name: "tag", type: tagValue }, + { name: "server", type: `Partial<${interfaceName}>` }, + ], + typeParameters: [{ name: "Req" }, { name: "Res" }], + returnType: "Route[]", + }); + }); + sourceFile.insertText( 0, `/** From 24df11fa9c9624a6ac0a2b90147b25f1c84e14f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 04:44:21 +0000 Subject: [PATCH 05/12] Remove Partial wrapper and if statements from registerRouteHandlersByTag - Make ServerForTag interface properties required (remove optional marker) - Remove Partial<> wrapper from overload signatures - Remove if statements from implementation since all methods are now required Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/docs/gen/server.ts | 30 +- examples/kitchensink/gen/server.ts | 86 +++--- examples/petstore/gen/server.ts | 276 ++++++++---------- .../bin/cli/index.cjs | 8 +- .../src/cli/generate.ts | 6 +- 5 files changed, 174 insertions(+), 232 deletions(-) diff --git a/examples/docs/gen/server.ts b/examples/docs/gen/server.ts index 65fbb3d..a7b0f69 100644 --- a/examples/docs/gen/server.ts +++ b/examples/docs/gen/server.ts @@ -76,30 +76,26 @@ export function registerRouteHandlers(server: Server): Route export type Tag = null; export interface ServerForUntagged { - makePetSpeak?: (args: MakePetSpeakArgs) => MakePetSpeakResult; - uhoh?: (args: UhohArgs) => UhohResult; + makePetSpeak: (args: MakePetSpeakArgs) => MakePetSpeakResult; + uhoh: (args: UhohArgs) => UhohResult; } -export function registerRouteHandlersByTag(tag: null, server: Partial>): Route[]; +export function registerRouteHandlersByTag(tag: null, server: ServerForUntagged): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; switch (tag) { case null: - if (server.makePetSpeak) { - routes.push({ - method: "post", - path: "/speak/{petId}", - handler: server.makePetSpeak as Route["handler"], - }); - } - if (server.uhoh) { - routes.push({ - method: "get", - path: "/uhoh", - handler: server.uhoh as Route["handler"], - }); - } + routes.push({ + method: "post", + path: "/speak/{petId}", + handler: server.makePetSpeak as Route["handler"], + }); + routes.push({ + method: "get", + path: "/uhoh", + handler: server.uhoh as Route["handler"], + }); break; } diff --git a/examples/kitchensink/gen/server.ts b/examples/kitchensink/gen/server.ts index ba23236..2e267a9 100644 --- a/examples/kitchensink/gen/server.ts +++ b/examples/kitchensink/gen/server.ts @@ -254,62 +254,50 @@ export function registerRouteHandlers(server: Server): Route export type Tag = null; export interface ServerForUntagged { - listPets?: (args: ListPetsArgs) => ListPetsResult; - getPetById?: (args: GetPetByIdArgs) => GetPetByIdResult; - updatePetWithForm?: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; - mixedContentTypes?: (args: MixedContentTypesArgs) => MixedContentTypesResult; - getPetImage?: (args: GetPetImageArgs) => GetPetImageResult; - getPetWebpage?: (args: GetPetWebpageArgs) => GetPetWebpageResult; + listPets: (args: ListPetsArgs) => ListPetsResult; + getPetById: (args: GetPetByIdArgs) => GetPetByIdResult; + updatePetWithForm: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; + mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; + getPetImage: (args: GetPetImageArgs) => GetPetImageResult; + getPetWebpage: (args: GetPetWebpageArgs) => GetPetWebpageResult; } -export function registerRouteHandlersByTag(tag: null, server: Partial>): Route[]; +export function registerRouteHandlersByTag(tag: null, server: ServerForUntagged): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; switch (tag) { case null: - if (server.listPets) { - routes.push({ - method: "get", - path: "/pets", - handler: server.listPets as Route["handler"], - }); - } - if (server.getPetById) { - routes.push({ - method: "get", - path: "/pet/{petId}", - handler: server.getPetById as Route["handler"], - }); - } - if (server.updatePetWithForm) { - routes.push({ - method: "post", - path: "/pet/{petId}", - handler: server.updatePetWithForm as Route["handler"], - }); - } - if (server.mixedContentTypes) { - routes.push({ - method: "post", - path: "/pet/{petId}/mixed-content-types", - handler: server.mixedContentTypes as Route["handler"], - }); - } - if (server.getPetImage) { - routes.push({ - method: "get", - path: "/pet/{petId}/image", - handler: server.getPetImage as Route["handler"], - }); - } - if (server.getPetWebpage) { - routes.push({ - method: "get", - path: "/pet/{petId}/webpage", - handler: server.getPetWebpage as Route["handler"], - }); - } + routes.push({ + method: "get", + path: "/pets", + handler: server.listPets as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}/mixed-content-types", + handler: server.mixedContentTypes as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}/image", + handler: server.getPetImage as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}/webpage", + handler: server.getPetWebpage as Route["handler"], + }); break; } diff --git a/examples/petstore/gen/server.ts b/examples/petstore/gen/server.ts index a1ceb78..740f301 100644 --- a/examples/petstore/gen/server.ts +++ b/examples/petstore/gen/server.ts @@ -747,178 +747,140 @@ export function registerRouteHandlers(server: Server): Route export type Tag = "pet" | "store" | "user"; export interface ServerForPet { - updatePet?: (args: UpdatePetArgs) => UpdatePetResult; - addPet?: (args: AddPetArgs) => AddPetResult; - findPetsByStatus?: (args: FindPetsByStatusArgs) => FindPetsByStatusResult; - findPetsByTags?: (args: FindPetsByTagsArgs) => FindPetsByTagsResult; - getPetById?: (args: GetPetByIdArgs) => GetPetByIdResult; - updatePetWithForm?: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; - deletePet?: (args: DeletePetArgs) => DeletePetResult; - uploadFile?: (args: UploadFileArgs) => UploadFileResult; + updatePet: (args: UpdatePetArgs) => UpdatePetResult; + addPet: (args: AddPetArgs) => AddPetResult; + findPetsByStatus: (args: FindPetsByStatusArgs) => FindPetsByStatusResult; + findPetsByTags: (args: FindPetsByTagsArgs) => FindPetsByTagsResult; + getPetById: (args: GetPetByIdArgs) => GetPetByIdResult; + updatePetWithForm: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; + deletePet: (args: DeletePetArgs) => DeletePetResult; + uploadFile: (args: UploadFileArgs) => UploadFileResult; } export interface ServerForStore { - getInventory?: (args: GetInventoryArgs) => GetInventoryResult; - placeOrder?: (args: PlaceOrderArgs) => PlaceOrderResult; - getOrderById?: (args: GetOrderByIdArgs) => GetOrderByIdResult; - deleteOrder?: (args: DeleteOrderArgs) => DeleteOrderResult; + getInventory: (args: GetInventoryArgs) => GetInventoryResult; + placeOrder: (args: PlaceOrderArgs) => PlaceOrderResult; + getOrderById: (args: GetOrderByIdArgs) => GetOrderByIdResult; + deleteOrder: (args: DeleteOrderArgs) => DeleteOrderResult; } export interface ServerForUser { - createUser?: (args: CreateUserArgs) => CreateUserResult; - createUsersWithListInput?: (args: CreateUsersWithListInputArgs) => CreateUsersWithListInputResult; - loginUser?: (args: LoginUserArgs) => LoginUserResult; - logoutUser?: (args: LogoutUserArgs) => LogoutUserResult; - getUserByName?: (args: GetUserByNameArgs) => GetUserByNameResult; - updateUser?: (args: UpdateUserArgs) => UpdateUserResult; - deleteUser?: (args: DeleteUserArgs) => DeleteUserResult; -} - -export function registerRouteHandlersByTag(tag: "pet", server: Partial>): Route[]; -export function registerRouteHandlersByTag(tag: "store", server: Partial>): Route[]; -export function registerRouteHandlersByTag(tag: "user", server: Partial>): Route[]; + createUser: (args: CreateUserArgs) => CreateUserResult; + createUsersWithListInput: (args: CreateUsersWithListInputArgs) => CreateUsersWithListInputResult; + loginUser: (args: LoginUserArgs) => LoginUserResult; + logoutUser: (args: LogoutUserArgs) => LogoutUserResult; + getUserByName: (args: GetUserByNameArgs) => GetUserByNameResult; + updateUser: (args: UpdateUserArgs) => UpdateUserResult; + deleteUser: (args: DeleteUserArgs) => DeleteUserResult; +} + +export function registerRouteHandlersByTag(tag: "pet", server: ServerForPet): Route[]; +export function registerRouteHandlersByTag(tag: "store", server: ServerForStore): Route[]; +export function registerRouteHandlersByTag(tag: "user", server: ServerForUser): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; switch (tag) { case "pet": - if (server.updatePet) { - routes.push({ - method: "put", - path: "/pet", - handler: server.updatePet as Route["handler"], - }); - } - if (server.addPet) { - routes.push({ - method: "post", - path: "/pet", - handler: server.addPet as Route["handler"], - }); - } - if (server.findPetsByStatus) { - routes.push({ - method: "get", - path: "/pet/findByStatus", - handler: server.findPetsByStatus as Route["handler"], - }); - } - if (server.findPetsByTags) { - routes.push({ - method: "get", - path: "/pet/findByTags", - handler: server.findPetsByTags as Route["handler"], - }); - } - if (server.getPetById) { - routes.push({ - method: "get", - path: "/pet/{petId}", - handler: server.getPetById as Route["handler"], - }); - } - if (server.updatePetWithForm) { - routes.push({ - method: "post", - path: "/pet/{petId}", - handler: server.updatePetWithForm as Route["handler"], - }); - } - if (server.deletePet) { - routes.push({ - method: "delete", - path: "/pet/{petId}", - handler: server.deletePet as Route["handler"], - }); - } - if (server.uploadFile) { - routes.push({ - method: "post", - path: "/pet/{petId}/uploadImage", - handler: server.uploadFile as Route["handler"], - }); - } + routes.push({ + method: "put", + path: "/pet", + handler: server.updatePet as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet", + handler: server.addPet as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/findByStatus", + handler: server.findPetsByStatus as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/findByTags", + handler: server.findPetsByTags as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }); + routes.push({ + method: "delete", + path: "/pet/{petId}", + handler: server.deletePet as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}/uploadImage", + handler: server.uploadFile as Route["handler"], + }); break; case "store": - if (server.getInventory) { - routes.push({ - method: "get", - path: "/store/inventory", - handler: server.getInventory as Route["handler"], - }); - } - if (server.placeOrder) { - routes.push({ - method: "post", - path: "/store/order", - handler: server.placeOrder as Route["handler"], - }); - } - if (server.getOrderById) { - routes.push({ - method: "get", - path: "/store/order/{orderId}", - handler: server.getOrderById as Route["handler"], - }); - } - if (server.deleteOrder) { - routes.push({ - method: "delete", - path: "/store/order/{orderId}", - handler: server.deleteOrder as Route["handler"], - }); - } + routes.push({ + method: "get", + path: "/store/inventory", + handler: server.getInventory as Route["handler"], + }); + routes.push({ + method: "post", + path: "/store/order", + handler: server.placeOrder as Route["handler"], + }); + routes.push({ + method: "get", + path: "/store/order/{orderId}", + handler: server.getOrderById as Route["handler"], + }); + routes.push({ + method: "delete", + path: "/store/order/{orderId}", + handler: server.deleteOrder as Route["handler"], + }); break; case "user": - if (server.createUser) { - routes.push({ - method: "post", - path: "/user", - handler: server.createUser as Route["handler"], - }); - } - if (server.createUsersWithListInput) { - routes.push({ - method: "post", - path: "/user/createWithList", - handler: server.createUsersWithListInput as Route["handler"], - }); - } - if (server.loginUser) { - routes.push({ - method: "get", - path: "/user/login", - handler: server.loginUser as Route["handler"], - }); - } - if (server.logoutUser) { - routes.push({ - method: "get", - path: "/user/logout", - handler: server.logoutUser as Route["handler"], - }); - } - if (server.getUserByName) { - routes.push({ - method: "get", - path: "/user/{username}", - handler: server.getUserByName as Route["handler"], - }); - } - if (server.updateUser) { - routes.push({ - method: "put", - path: "/user/{username}", - handler: server.updateUser as Route["handler"], - }); - } - if (server.deleteUser) { - routes.push({ - method: "delete", - path: "/user/{username}", - handler: server.deleteUser as Route["handler"], - }); - } + routes.push({ + method: "post", + path: "/user", + handler: server.createUser as Route["handler"], + }); + routes.push({ + method: "post", + path: "/user/createWithList", + handler: server.createUsersWithListInput as Route["handler"], + }); + routes.push({ + method: "get", + path: "/user/login", + handler: server.loginUser as Route["handler"], + }); + routes.push({ + method: "get", + path: "/user/logout", + handler: server.logoutUser as Route["handler"], + }); + routes.push({ + method: "get", + path: "/user/{username}", + handler: server.getUserByName as Route["handler"], + }); + routes.push({ + method: "put", + path: "/user/{username}", + handler: server.updateUser as Route["handler"], + }); + routes.push({ + method: "delete", + path: "/user/{username}", + handler: server.deleteUser as Route["handler"], + }); break; } diff --git a/packages/openapi-typescript-server/bin/cli/index.cjs b/packages/openapi-typescript-server/bin/cli/index.cjs index 0557a38..6000960 100755 --- a/packages/openapi-typescript-server/bin/cli/index.cjs +++ b/packages/openapi-typescript-server/bin/cli/index.cjs @@ -531,8 +531,8 @@ function generate(spec, types, outpath, version) { tagInterface.addProperty({ name: operationId, type: `(args: ${op.args}) => ${op.result}`, - hasQuestionToken: true - // Make it optional since it's a partial implementation + hasQuestionToken: false + // Required - all operations for this tag must be implemented }); } }); @@ -557,7 +557,6 @@ function generate(spec, types, outpath, version) { operations.forEach((operationId) => { const op = operationsById[operationId]; if (op) { - writer.writeLine(`if (server.${operationId}) {`); writer.writeLine("routes.push({"); writer.writeLine(`method: "${op.method}",`); writer.writeLine(`path: "${op.path}",`); @@ -565,7 +564,6 @@ function generate(spec, types, outpath, version) { `handler: server.${operationId} as Route["handler"],` ); writer.writeLine("});"); - writer.writeLine("}"); } }); writer.writeLine("break;"); @@ -582,7 +580,7 @@ function generate(spec, types, outpath, version) { registerByTagFunc.addOverload({ parameters: [ { name: "tag", type: tagValue }, - { name: "server", type: `Partial<${interfaceName}>` } + { name: "server", type: `${interfaceName}` } ], typeParameters: [{ name: "Req" }, { name: "Res" }], returnType: "Route[]" diff --git a/packages/openapi-typescript-server/src/cli/generate.ts b/packages/openapi-typescript-server/src/cli/generate.ts index ec6777e..c5f66e6 100644 --- a/packages/openapi-typescript-server/src/cli/generate.ts +++ b/packages/openapi-typescript-server/src/cli/generate.ts @@ -295,7 +295,7 @@ export default function generate( tagInterface.addProperty({ name: operationId, type: `(args: ${op.args}) => ${op.result}`, - hasQuestionToken: true, // Make it optional since it's a partial implementation + hasQuestionToken: false, // Required - all operations for this tag must be implemented }); } }); @@ -326,7 +326,6 @@ export default function generate( operations.forEach((operationId) => { const op = operationsById[operationId]; if (op) { - writer.writeLine(`if (server.${operationId}) {`); writer.writeLine("routes.push({"); writer.writeLine(`method: "${op.method}",`); writer.writeLine(`path: "${op.path}",`); @@ -334,7 +333,6 @@ export default function generate( `handler: server.${operationId} as Route["handler"],`, ); writer.writeLine("});"); - writer.writeLine("}"); } }); @@ -358,7 +356,7 @@ export default function generate( registerByTagFunc.addOverload({ parameters: [ { name: "tag", type: tagValue }, - { name: "server", type: `Partial<${interfaceName}>` }, + { name: "server", type: `${interfaceName}` }, ], typeParameters: [{ name: "Req" }, { name: "Res" }], returnType: "Route[]", From 17d0bb90d7d6bf8f6f14b499355686c6e0dad674 Mon Sep 17 00:00:00 2001 From: Jason Blanchard Date: Wed, 15 Oct 2025 22:03:46 -0700 Subject: [PATCH 06/12] start example --- .github/workflows/version.yaml | 3 + examples/tags/api.ts | 124 +++++++++++++ examples/tags/app.test.ts | 166 +++++++++++++++++ examples/tags/app.ts | 70 +++++++ examples/tags/cat.jpeg | Bin 0 -> 45357 bytes examples/tags/cmd/index.ts | 5 + examples/tags/gen/schema.d.ts | 330 +++++++++++++++++++++++++++++++++ examples/tags/gen/server.ts | 272 +++++++++++++++++++++++++++ examples/tags/openapi.yaml | 250 +++++++++++++++++++++++++ examples/tags/package.json | 24 +++ 10 files changed, 1244 insertions(+) create mode 100644 examples/tags/api.ts create mode 100644 examples/tags/app.test.ts create mode 100644 examples/tags/app.ts create mode 100644 examples/tags/cat.jpeg create mode 100644 examples/tags/cmd/index.ts create mode 100644 examples/tags/gen/schema.d.ts create mode 100644 examples/tags/gen/server.ts create mode 100644 examples/tags/openapi.yaml create mode 100644 examples/tags/package.json diff --git a/.github/workflows/version.yaml b/.github/workflows/version.yaml index c8be4a9..ff5bc61 100644 --- a/.github/workflows/version.yaml +++ b/.github/workflows/version.yaml @@ -66,6 +66,9 @@ jobs: npm install --save-exact --workspace docs-example openapi-typescript-server@${{ env.PACKAGE_VERSION }} openapi-typescript-server-express@${{ env.PACKAGE_VERSION }} npm install --save-exact --workspace docs-example openapi-typescript-server@${{ env.PACKAGE_VERSION }} openapi-typescript-server-express@${{ env.PACKAGE_VERSION }} + npm install --save-exact --workspace tags-example openapi-typescript-server@${{ env.PACKAGE_VERSION }} openapi-typescript-server-express@${{ env.PACKAGE_VERSION }} + npm install --save-exact --workspace tags-example openapi-typescript-server@${{ env.PACKAGE_VERSION }} openapi-typescript-server-express@${{ env.PACKAGE_VERSION }} + - name: Build the packages run: npm run build:packages diff --git a/examples/tags/api.ts b/examples/tags/api.ts new file mode 100644 index 0000000..fa0803f --- /dev/null +++ b/examples/tags/api.ts @@ -0,0 +1,124 @@ +import type * as ServerTypes from "./gen/server.ts"; +import * as server from "./gen/server.ts"; +import type { Request, Response } from "express"; +import { promises as fs } from "fs"; +import { join } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = join(__filename, ".."); + +const API: ServerTypes.Server = { + getPetById: async ({ parameters, req }): ServerTypes.GetPetByIdResult => { + if (parameters.path.petId === 42) { + return { + content: { + default: { + "application/json": { + message: "Cannot get that pet", + }, + }, + }, + status: 503, + }; + } + + if (parameters.path.petId === 500) { + throw new Error("Cannot get that pet"); + } + + return { + content: { + 200: { + "application/json": { + pet: { id: parameters.path.petId, name: "dog" }, + }, + }, + }, + }; + }, + + updatePetWithForm: async ({ + parameters, + requestBody, + }): ServerTypes.UpdatePetWithFormResult => { + const { petId } = parameters.path; + const { name } = parameters.query ?? {}; + const { status } = requestBody.content; + + return { + content: { + 200: { + "application/json": { + pet: { id: petId, name: name || "dog", status }, + }, + }, + }, + }; + }, + + mixedContentTypes: async ({ + parameters, + requestBody, + contentType, + }): ServerTypes.MixedContentTypesResult => { + const { petId } = parameters.path; + let status: "available" | "pending" | "sold" | undefined; + + // Since each content type has different structures, + // use the request content type and requestBody discriminator to narrow the type in each case. + + if ( + contentType === "application/json" && + requestBody.mediaType === "application/json" + ) { + status = requestBody.content.jsonstatus; + } + + if ( + contentType == "application/xml" && + requestBody.mediaType === "application/xml" + ) { + status = requestBody.content.xmlstatus; + } + + return { + content: { + 200: { + "application/json": { + pet: { id: petId, name: "dog", status }, + }, + }, + }, + }; + }, + + listPets: server.listPetsUnimplemented, + + getPetImage: async (): ServerTypes.GetPetImageResult => { + const image = await fs.readFile(join(__dirname, `./cat.jpeg`), { + encoding: "base64", + }); + + return { + content: { + 200: { + "image/jpeg": image, + }, + }, + }; + }, + + getPetWebpage: async ({ parameters }): ServerTypes.GetPetWebpageResult => { + const { petId } = parameters.path; + return { + content: { + 200: { + "text/html": `

Hello, pet ${petId}!

`, + }, + }, + }; + }, +}; + +export default API; diff --git a/examples/tags/app.test.ts b/examples/tags/app.test.ts new file mode 100644 index 0000000..222597b --- /dev/null +++ b/examples/tags/app.test.ts @@ -0,0 +1,166 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import makeApp from "./app.ts"; +import request from "supertest"; +import { json2xml } from "xml-js"; + +const app = makeApp(); + +describe("getPetById", async () => { + it("returns 200", async () => { + const response = await request(app) + .get("/api/v3/pet/123") + .set("Accept", "application/json"); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "dog", + }, + }); + }); + + it("returns 503 via default response and custom status code", async () => { + const response = await request(app) + .get("/api/v3/pet/42") + .set("Accept", "application/json"); + + assert.equal(response.status, 503); + assert.deepEqual(response.body, { + message: "Cannot get that pet", + }); + }); +}); + +describe("updatePetWithForm", async () => { + it("returns 200", async () => { + const response = await request(app) + .post("/api/v3/pet/123?name=cat") + .set("Accept", "application/json") + .send({ status: "sold" }); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "cat", + status: "sold", + }, + }); + }); + + it("accepts xml input", async () => { + const xmlData = json2xml(JSON.stringify({ status: "sold" }), { + compact: true, + ignoreComment: true, + spaces: 4, + }); + + const response = await request(app) + .post("/api/v3/pet/123?name=cat") + .set("Content-Type", "application/xml") + .send(xmlData); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "cat", + status: "sold", + }, + }); + }); + + it("accepts form-urlencoded input", async () => { + const response = await request(app) + .post("/api/v3/pet/123?name=cat") + .set("Content-Type", "application/x-www-form-urlencoded") + .send("status=sold"); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "cat", + status: "sold", + }, + }); + }); +}); + +describe("mixed content types with different structures", async () => { + it("handles json by default", async () => { + const response = await request(app) + .post("/api/v3/pet/123/mixed-content-types") + .send({ jsonstatus: "sold" }); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "dog", + status: "sold", + }, + }); + }); + + it("handles xml", async () => { + const xmlData = json2xml(JSON.stringify({ xmlstatus: "sold" }), { + compact: true, + ignoreComment: true, + spaces: 4, + }); + + const response = await request(app) + .post("/api/v3/pet/123/mixed-content-types") + .set("Content-Type", "application/xml") + .send(xmlData); + + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pet: { + id: 123, + name: "dog", + status: "sold", + }, + }); + }); +}); + +describe("listPets", async () => { + it("propagates unimplemented error", async () => { + const response = await request(app) + .get("/api/v3/pets") + .set("Accept", "application/json"); + + assert.equal(response.status, 501); + assert.equal(response.body.message, "Not Implemented"); + }); + + describe("getPetImage", async () => { + it("returns 200", async () => { + const response = await request(app) + .get("/api/v3/pet/123/image") + .set("Accept", "image/jpeg"); + + assert.equal(response.status, 200); + assert(response.body); + assert.equal(response.headers["content-type"], "image/jpeg"); + }); + }); + + describe("getPetWebpage", async () => { + it("returns 200", async () => { + const response = await request(app) + .get("/api/v3/pet/123/webpage") + .set("Accept", "text/html"); + + assert.equal(response.status, 200); + assert.equal( + response.text, + "

Hello, pet 123!

", + ); + }); + }); +}); diff --git a/examples/tags/app.ts b/examples/tags/app.ts new file mode 100644 index 0000000..67b5390 --- /dev/null +++ b/examples/tags/app.ts @@ -0,0 +1,70 @@ +import express from "express"; +import type { Request, Response, NextFunction } from "express"; +import { registerRouteHandlers } from "./gen/server.ts"; +import registerRoutes from "openapi-typescript-server-express"; +import API from "./api.ts"; +import OpenApiValidator from "express-openapi-validator"; +import { NotImplementedError } from "openapi-typescript-server-runtime"; +import xmlparser from "express-xml-bodyparser"; + +export default function makeApp() { + const app = express(); + + app.get("/", (_req, res) => { + res.send("Hello World!"); + }); + + app.use(express.json()); + app.use(xmlparser()); + app.use(express.urlencoded({ extended: true })); + + const apiRouter = express(); + + apiRouter.use( + OpenApiValidator.middleware({ + apiSpec: "./openapi.yaml", + validateResponses: false, + }), + ); + registerRoutes(registerRouteHandlers(API), apiRouter, { + serializers: { + "image/jpeg": (content) => { + return Buffer.from(content, "base64"); + }, + }, + }); + + app.use("/api/v3", apiRouter); + + interface ValidationError extends Error { + status?: number; + errors?: Record[]; + } + + app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { + console.error("+++", err); + + const validationError = err as ValidationError; + if (validationError.status && validationError.errors) { + res.status(validationError.status || 500).json({ + message: validationError.message, + errors: validationError.errors, + }); + return; + } + + if (err instanceof NotImplementedError) { + res.status(501).json({ + message: "Not Implemented", + }); + return; + } + + res.status(500).json({ + message: "Internal Server Error", + }); + return; + }); + + return app; +} diff --git a/examples/tags/cat.jpeg b/examples/tags/cat.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dca1e0bf1cd981220402f96610218bd7b5c34458 GIT binary patch literal 45357 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 H(i#8R!Bqms literal 0 HcmV?d00001 diff --git a/examples/tags/cmd/index.ts b/examples/tags/cmd/index.ts new file mode 100644 index 0000000..881517e --- /dev/null +++ b/examples/tags/cmd/index.ts @@ -0,0 +1,5 @@ +import makeApp from "../app.ts"; + +makeApp().listen(8080, () => { + console.log("Server running on port 8080"); +}); diff --git a/examples/tags/gen/schema.d.ts b/examples/tags/gen/schema.d.ts new file mode 100644 index 0000000..3df982f --- /dev/null +++ b/examples/tags/gen/schema.d.ts @@ -0,0 +1,330 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/pets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Returns all pets from the system that the user has access to */ + get: operations["listPets"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pet/{petId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getPetById"]; + put?: never; + post: operations["updatePetWithForm"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pet/{petId}/mixed-content-types": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["mixedContentTypes"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pet/{petId}/image": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getPetImage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/pet/{petId}/webpage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getPetWebpage"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + ErrorResponse: { + message?: string; + }; + Pet: { + /** Format: int64 */ + id: number; + name: string; + category?: components["schemas"]["Category"]; + photoUrls?: string[]; + tags?: components["schemas"]["Tag"][]; + /** + * @description pet status in the store + * @enum {string} + */ + status?: "available" | "pending" | "sold"; + }; + Category: { + /** Format: int64 */ + id?: number; + name?: string; + }; + Tag: { + /** Format: int64 */ + id?: number; + name?: string; + }; + UpdatePetInput: { + /** @enum {string} */ + status: "available" | "pending" | "sold"; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listPets: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of pets. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pets?: components["schemas"]["Pet"][]; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getPetById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of pet to return */ + petId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pet?: components["schemas"]["Pet"]; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + updatePetWithForm: { + parameters: { + query?: { + /** @description Name of pet that needs to be updated */ + name?: string; + }; + header?: never; + path: { + /** @description ID of pet that needs to be updated */ + petId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdatePetInput"]; + "application/xml": components["schemas"]["UpdatePetInput"]; + "application/x-www-form-urlencoded": components["schemas"]["UpdatePetInput"]; + }; + }; + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pet?: components["schemas"]["Pet"]; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + mixedContentTypes: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of pet that needs to be updated */ + petId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @enum {string} */ + jsonstatus: "available" | "pending" | "sold"; + }; + "application/xml": { + /** @enum {string} */ + xmlstatus: "available" | "pending" | "sold"; + }; + }; + }; + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + pet?: components["schemas"]["Pet"]; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getPetImage: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of pet to return */ + petId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "image/jpeg": string; + }; + }; + }; + }; + getPetWebpage: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of pet to return */ + petId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description successful operation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/html": string; + }; + }; + }; + }; +} diff --git a/examples/tags/gen/server.ts b/examples/tags/gen/server.ts new file mode 100644 index 0000000..5158b74 --- /dev/null +++ b/examples/tags/gen/server.ts @@ -0,0 +1,272 @@ +/** + * This file was auto-generated by openapi-typescript-server@0.0.12. + * Do not make direct changes to the file. + */ + +import type { paths } from "./schema"; +import type { Route } from "openapi-typescript-server-runtime"; +import { NotImplementedError } from "openapi-typescript-server-runtime"; + +export interface ListPetsArgs { + parameters: paths['/pets']['get']['parameters']; + contentType: string; + req: Req; + res: Res; +} + +interface ListPetsResult200 { + content: { 200: paths['/pets']['get']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +interface ListPetsResultDefault { + content: { default: paths['/pets']['get']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type ListPetsResult = Promise; + +export async function listPetsUnimplemented(): ListPetsResult { + throw new NotImplementedError() +} + +export interface GetPetByIdArgs { + parameters: paths['/pet/{petId}']['get']['parameters']; + contentType: string; + req: Req; + res: Res; +} + +interface GetPetByIdResult200 { + content: { 200: paths['/pet/{petId}']['get']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +interface GetPetByIdResultDefault { + content: { default: paths['/pet/{petId}']['get']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type GetPetByIdResult = Promise; + +export async function getPetByIdUnimplemented(): GetPetByIdResult { + throw new NotImplementedError() +} + +export interface UpdatePetWithFormArgs { + parameters: paths['/pet/{petId}']['post']['parameters']; + contentType: string; + req: Req; + res: Res; + requestBody: { + mediaType: "application/json"; + content: paths['/pet/{petId}']['post']['requestBody']['content']['application/json'] + } + | { + mediaType: "application/xml"; + content: paths['/pet/{petId}']['post']['requestBody']['content']['application/xml'] + } + | { + mediaType: "application/x-www-form-urlencoded"; + content: paths['/pet/{petId}']['post']['requestBody']['content']['application/x-www-form-urlencoded'] + } + ; +} + +interface UpdatePetWithFormResult200 { + content: { 200: paths['/pet/{petId}']['post']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +interface UpdatePetWithFormResultDefault { + content: { default: paths['/pet/{petId}']['post']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type UpdatePetWithFormResult = Promise; + +export async function updatePetWithFormUnimplemented(): UpdatePetWithFormResult { + throw new NotImplementedError() +} + +export interface MixedContentTypesArgs { + parameters: paths['/pet/{petId}/mixed-content-types']['post']['parameters']; + contentType: string; + req: Req; + res: Res; + requestBody: { + mediaType: "application/json"; + content: paths['/pet/{petId}/mixed-content-types']['post']['requestBody']['content']['application/json'] + } + | { + mediaType: "application/xml"; + content: paths['/pet/{petId}/mixed-content-types']['post']['requestBody']['content']['application/xml'] + } + ; +} + +interface MixedContentTypesResult200 { + content: { 200: paths['/pet/{petId}/mixed-content-types']['post']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +interface MixedContentTypesResultDefault { + content: { default: paths['/pet/{petId}/mixed-content-types']['post']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type MixedContentTypesResult = Promise; + +export async function mixedContentTypesUnimplemented(): MixedContentTypesResult { + throw new NotImplementedError() +} + +export interface GetPetImageArgs { + parameters: paths['/pet/{petId}/image']['get']['parameters']; + contentType: string; + req: Req; + res: Res; +} + +interface GetPetImageResult200 { + content: { 200: paths['/pet/{petId}/image']['get']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +export type GetPetImageResult = Promise; + +export async function getPetImageUnimplemented(): GetPetImageResult { + throw new NotImplementedError() +} + +export interface GetPetWebpageArgs { + parameters: paths['/pet/{petId}/webpage']['get']['parameters']; + contentType: string; + req: Req; + res: Res; +} + +interface GetPetWebpageResult200 { + content: { 200: paths['/pet/{petId}/webpage']['get']['responses']['200']['content'] }; + headers?: { [name: string]: any }; +} + +export type GetPetWebpageResult = Promise; + +export async function getPetWebpageUnimplemented(): GetPetWebpageResult { + throw new NotImplementedError() +} + +export interface Server { + /** Returns all pets from the system that the user has access to */ + listPets: ( + args: ListPetsArgs + ) => ListPetsResult; + getPetById: ( + args: GetPetByIdArgs + ) => GetPetByIdResult; + updatePetWithForm: ( + args: UpdatePetWithFormArgs + ) => UpdatePetWithFormResult; + mixedContentTypes: ( + args: MixedContentTypesArgs + ) => MixedContentTypesResult; + getPetImage: ( + args: GetPetImageArgs + ) => GetPetImageResult; + getPetWebpage: ( + args: GetPetWebpageArgs + ) => GetPetWebpageResult; +} + +export function registerRouteHandlers(server: Server): Route[] { + return [ + { + method: "get", + path: "/pets", + handler: server.listPets as Route["handler"], + }, + { + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }, + { + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }, + { + method: "post", + path: "/pet/{petId}/mixed-content-types", + handler: server.mixedContentTypes as Route["handler"], + }, + { + method: "get", + path: "/pet/{petId}/image", + handler: server.getPetImage as Route["handler"], + }, + { + method: "get", + path: "/pet/{petId}/webpage", + handler: server.getPetWebpage as Route["handler"], + }, + ] +} + +export type Tag = null; + +export interface ServerForUntagged { + listPets: (args: ListPetsArgs) => ListPetsResult; + getPetById: (args: GetPetByIdArgs) => GetPetByIdResult; + updatePetWithForm: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; + mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; + getPetImage: (args: GetPetImageArgs) => GetPetImageResult; + getPetWebpage: (args: GetPetWebpageArgs) => GetPetWebpageResult; +} + +export function registerRouteHandlersByTag(tag: null, server: ServerForUntagged): Route[]; +export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { + const routes: Route[] = []; + + switch (tag) { + case null: + routes.push({ + method: "get", + path: "/pets", + handler: server.listPets as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}", + handler: server.getPetById as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}", + handler: server.updatePetWithForm as Route["handler"], + }); + routes.push({ + method: "post", + path: "/pet/{petId}/mixed-content-types", + handler: server.mixedContentTypes as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}/image", + handler: server.getPetImage as Route["handler"], + }); + routes.push({ + method: "get", + path: "/pet/{petId}/webpage", + handler: server.getPetWebpage as Route["handler"], + }); + break; + } + + return routes; +} diff --git a/examples/tags/openapi.yaml b/examples/tags/openapi.yaml new file mode 100644 index 0000000..9be5f2b --- /dev/null +++ b/examples/tags/openapi.yaml @@ -0,0 +1,250 @@ +openapi: 3.0.2 +info: + title: Simple Petstore + version: 0.0.1 +servers: + - url: /api/v3 +paths: + /pets: + get: + operationId: listPets + summary: Returns all pets from the system that the user has access to + responses: + "200": + description: A list of pets. + content: + application/json: + schema: + type: object + properties: + pets: + type: array + items: + $ref: "#/components/schemas/Pet" + "default": + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /pet/{petId}: + get: + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pet: + $ref: "#/components/schemas/Pet" + "default": + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + post: + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdatePetInput" + application/xml: + schema: + $ref: "#/components/schemas/UpdatePetInput" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/UpdatePetInput" + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pet: + $ref: "#/components/schemas/Pet" + "default": + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /pet/{petId}/mixed-content-types: + post: + operationId: mixedContentTypes + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + jsonstatus: + type: string + enum: + - available + - pending + - sold + required: + - jsonstatus + application/xml: + schema: + type: object + properties: + xmlstatus: + type: string + enum: + - available + - pending + - sold + required: + - xmlstatus + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + properties: + pet: + $ref: "#/components/schemas/Pet" + "default": + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + /pet/{petId}/image: + get: + operationId: getPetImage + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + image/jpeg: + schema: + type: string + format: binary + /pet/{petId}/webpage: + get: + operationId: getPetWebpage + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + text/html: + schema: + type: string +components: + schemas: + ErrorResponse: + type: object + properties: + message: + type: string + Pet: + required: + - id + - name + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + category: + $ref: "#/components/schemas/Category" + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: "#/components/schemas/Tag" + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + UpdatePetInput: + type: object + properties: + status: + type: string + enum: + - available + - pending + - sold + required: + - status diff --git a/examples/tags/package.json b/examples/tags/package.json new file mode 100644 index 0000000..e78b929 --- /dev/null +++ b/examples/tags/package.json @@ -0,0 +1,24 @@ +{ + "name": "tags-example", + "private": true, + "version": "0.0.12", + "type": "module", + "scripts": { + "test": "node --test", + "gen": "openapi-typescript ./openapi.yaml --output ./gen/schema.d.ts && openapi-typescript-server ./openapi.yaml --types ./schema.d.ts --output ./gen/server.ts", + "start": "node --watch cmd/index.ts" + }, + "dependencies": { + "@types/express": "^5.0.1", + "@types/express-xml-bodyparser": "^0.3.5", + "@types/supertest": "^6.0.3", + "express": "^4.21.2", + "express-openapi-validator": "^5.5.8", + "express-xml-bodyparser": "^0.4.1", + "openapi-typescript": "^7.9.1", + "openapi-typescript-server": "0.0.12", + "openapi-typescript-server-express": "0.0.12", + "supertest": "^7.1.4", + "xml-js": "^1.6.11" + } +} \ No newline at end of file From d3383c210ddbed13ae03907e34def65e73d221c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 05:19:29 +0000 Subject: [PATCH 07/12] Complete examples/tags implementation with registerRouteHandlersByTag - Added tags to operations in openapi.yaml (pets, media tags, plus untagged mixedContentTypes) - Updated api.ts to create service implementations per tag (petsService, mediaService, untaggedService) - Updated app.ts to use registerRouteHandlersByTag to mount each service to /api - Updated app.test.ts to test all routes including newly implemented listPets Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/tags/api.ts | 102 +++++++++++++++++++++--------------- examples/tags/app.test.ts | 53 ++++++++++--------- examples/tags/app.ts | 24 ++++++--- examples/tags/gen/server.ts | 20 +++++-- examples/tags/openapi.yaml | 10 ++++ examples/tags/package.json | 2 +- package-lock.json | 21 ++++++++ 7 files changed, 154 insertions(+), 78 deletions(-) diff --git a/examples/tags/api.ts b/examples/tags/api.ts index fa0803f..874d592 100644 --- a/examples/tags/api.ts +++ b/examples/tags/api.ts @@ -1,5 +1,4 @@ import type * as ServerTypes from "./gen/server.ts"; -import * as server from "./gen/server.ts"; import type { Request, Response } from "express"; import { promises as fs } from "fs"; import { join } from "path"; @@ -8,8 +7,24 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = join(__filename, ".."); -const API: ServerTypes.Server = { - getPetById: async ({ parameters, req }): ServerTypes.GetPetByIdResult => { +// Service implementation for "pets" tag +export const petsService: ServerTypes.ServerForPets = { + listPets: async (): ServerTypes.ListPetsResult => { + return { + content: { + 200: { + "application/json": { + pets: [ + { id: 1, name: "dog" }, + { id: 2, name: "cat" }, + ], + }, + }, + }, + }; + }, + + getPetById: async ({ parameters }): ServerTypes.GetPetByIdResult => { if (parameters.path.petId === 42) { return { content: { @@ -56,45 +71,10 @@ const API: ServerTypes.Server = { }, }; }, +}; - mixedContentTypes: async ({ - parameters, - requestBody, - contentType, - }): ServerTypes.MixedContentTypesResult => { - const { petId } = parameters.path; - let status: "available" | "pending" | "sold" | undefined; - - // Since each content type has different structures, - // use the request content type and requestBody discriminator to narrow the type in each case. - - if ( - contentType === "application/json" && - requestBody.mediaType === "application/json" - ) { - status = requestBody.content.jsonstatus; - } - - if ( - contentType == "application/xml" && - requestBody.mediaType === "application/xml" - ) { - status = requestBody.content.xmlstatus; - } - - return { - content: { - 200: { - "application/json": { - pet: { id: petId, name: "dog", status }, - }, - }, - }, - }; - }, - - listPets: server.listPetsUnimplemented, - +// Service implementation for "media" tag +export const mediaService: ServerTypes.ServerForMedia = { getPetImage: async (): ServerTypes.GetPetImageResult => { const image = await fs.readFile(join(__dirname, `./cat.jpeg`), { encoding: "base64", @@ -121,4 +101,42 @@ const API: ServerTypes.Server = { }, }; -export default API; +// Service implementation for untagged operations +export const untaggedService: ServerTypes.ServerForUntagged = + { + mixedContentTypes: async ({ + parameters, + requestBody, + contentType, + }): ServerTypes.MixedContentTypesResult => { + const { petId } = parameters.path; + let status: "available" | "pending" | "sold" | undefined; + + // Since each content type has different structures, + // use the request content type and requestBody discriminator to narrow the type in each case. + + if ( + contentType === "application/json" && + requestBody.mediaType === "application/json" + ) { + status = requestBody.content.jsonstatus; + } + + if ( + contentType == "application/xml" && + requestBody.mediaType === "application/xml" + ) { + status = requestBody.content.xmlstatus; + } + + return { + content: { + 200: { + "application/json": { + pet: { id: petId, name: "dog", status }, + }, + }, + }, + }; + }, + }; diff --git a/examples/tags/app.test.ts b/examples/tags/app.test.ts index 222597b..77ca704 100644 --- a/examples/tags/app.test.ts +++ b/examples/tags/app.test.ts @@ -129,38 +129,43 @@ describe("mixed content types with different structures", async () => { }); describe("listPets", async () => { - it("propagates unimplemented error", async () => { + it("returns 200", async () => { const response = await request(app) .get("/api/v3/pets") .set("Accept", "application/json"); - assert.equal(response.status, 501); - assert.equal(response.body.message, "Not Implemented"); + assert.equal(response.status, 200); + assert.deepEqual(response.body, { + pets: [ + { id: 1, name: "dog" }, + { id: 2, name: "cat" }, + ], + }); }); +}); - describe("getPetImage", async () => { - it("returns 200", async () => { - const response = await request(app) - .get("/api/v3/pet/123/image") - .set("Accept", "image/jpeg"); +describe("getPetImage", async () => { + it("returns 200", async () => { + const response = await request(app) + .get("/api/v3/pet/123/image") + .set("Accept", "image/jpeg"); - assert.equal(response.status, 200); - assert(response.body); - assert.equal(response.headers["content-type"], "image/jpeg"); - }); + assert.equal(response.status, 200); + assert(response.body); + assert.equal(response.headers["content-type"], "image/jpeg"); }); +}); - describe("getPetWebpage", async () => { - it("returns 200", async () => { - const response = await request(app) - .get("/api/v3/pet/123/webpage") - .set("Accept", "text/html"); - - assert.equal(response.status, 200); - assert.equal( - response.text, - "

Hello, pet 123!

", - ); - }); +describe("getPetWebpage", async () => { + it("returns 200", async () => { + const response = await request(app) + .get("/api/v3/pet/123/webpage") + .set("Accept", "text/html"); + + assert.equal(response.status, 200); + assert.equal( + response.text, + "

Hello, pet 123!

", + ); }); }); diff --git a/examples/tags/app.ts b/examples/tags/app.ts index 67b5390..642e25f 100644 --- a/examples/tags/app.ts +++ b/examples/tags/app.ts @@ -1,8 +1,8 @@ import express from "express"; import type { Request, Response, NextFunction } from "express"; -import { registerRouteHandlers } from "./gen/server.ts"; +import { registerRouteHandlersByTag } from "./gen/server.ts"; import registerRoutes from "openapi-typescript-server-express"; -import API from "./api.ts"; +import { petsService, mediaService, untaggedService } from "./api.ts"; import OpenApiValidator from "express-openapi-validator"; import { NotImplementedError } from "openapi-typescript-server-runtime"; import xmlparser from "express-xml-bodyparser"; @@ -26,13 +26,23 @@ export default function makeApp() { validateResponses: false, }), ); - registerRoutes(registerRouteHandlers(API), apiRouter, { - serializers: { - "image/jpeg": (content) => { - return Buffer.from(content, "base64"); + + // Register routes by tag using registerRouteHandlersByTag + const petsRoutes = registerRouteHandlersByTag("pets", petsService); + const mediaRoutes = registerRouteHandlersByTag("media", mediaService); + const untaggedRoutes = registerRouteHandlersByTag(null, untaggedService); + + registerRoutes( + [...petsRoutes, ...mediaRoutes, ...untaggedRoutes], + apiRouter, + { + serializers: { + "image/jpeg": (content) => { + return Buffer.from(content, "base64"); + }, }, }, - }); + ); app.use("/api/v3", apiRouter); diff --git a/examples/tags/gen/server.ts b/examples/tags/gen/server.ts index 5158b74..5cfb53f 100644 --- a/examples/tags/gen/server.ts +++ b/examples/tags/gen/server.ts @@ -3,7 +3,7 @@ * Do not make direct changes to the file. */ -import type { paths } from "./schema"; +import type { paths } from "./schema.d.ts"; import type { Route } from "openapi-typescript-server-runtime"; import { NotImplementedError } from "openapi-typescript-server-runtime"; @@ -218,23 +218,31 @@ export function registerRouteHandlers(server: Server): Route ] } -export type Tag = null; +export type Tag = "pets" | "media" | null; -export interface ServerForUntagged { +export interface ServerForPets { listPets: (args: ListPetsArgs) => ListPetsResult; getPetById: (args: GetPetByIdArgs) => GetPetByIdResult; updatePetWithForm: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; +} + +export interface ServerForUntagged { mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; +} + +export interface ServerForMedia { getPetImage: (args: GetPetImageArgs) => GetPetImageResult; getPetWebpage: (args: GetPetWebpageArgs) => GetPetWebpageResult; } +export function registerRouteHandlersByTag(tag: "pets", server: ServerForPets): Route[]; export function registerRouteHandlersByTag(tag: null, server: ServerForUntagged): Route[]; +export function registerRouteHandlersByTag(tag: "media", server: ServerForMedia): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; switch (tag) { - case null: + case "pets": routes.push({ method: "get", path: "/pets", @@ -250,11 +258,15 @@ export function registerRouteHandlersByTag(tag: Tag, server: Partial Date: Wed, 15 Oct 2025 22:32:36 -0700 Subject: [PATCH 08/12] fix tests --- .../src/cli/generate.test.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/openapi-typescript-server/src/cli/generate.test.ts b/packages/openapi-typescript-server/src/cli/generate.test.ts index 36d9a80..06fb0a0 100644 --- a/packages/openapi-typescript-server/src/cli/generate.test.ts +++ b/packages/openapi-typescript-server/src/cli/generate.test.ts @@ -254,7 +254,7 @@ describe("tags", () => { const tagType = sourceFile.getTypeAlias("Tag"); assert(tagType); assert(tagType.isExported()); - const tagTypeText = tagType.getType().getText(); + const tagTypeText = tagType.getTypeNode()?.getText() || ""; assert.match(tagTypeText, /"pet"/); assert.match(tagTypeText, /"user"/); }); @@ -284,7 +284,7 @@ describe("tags", () => { const tagType = sourceFile.getTypeAlias("Tag"); assert(tagType); assert(tagType.isExported()); - const tagTypeText = tagType.getType().getText(); + const tagTypeText = tagType.getTypeNode()?.getText() || ""; assert.equal(tagTypeText, "null"); }); @@ -327,7 +327,7 @@ describe("tags", () => { const sourceFile = generate(spec, "./schema.d.ts", "outdir.ts"); const tagType = sourceFile.getTypeAlias("Tag"); assert(tagType); - const tagTypeText = tagType.getType().getText(); + const tagTypeText = tagType.getTypeNode()?.getText() || ""; assert.match(tagTypeText, /"pet"/); assert.match(tagTypeText, /null/); }); @@ -371,9 +371,11 @@ describe("registerRouteHandlersByTag", () => { const serverParam = func.getParameters()[1]; assert(serverParam); assert.equal(serverParam.getName(), "server"); - assert.match(serverParam.getType().getText(), /Partial>/); + const serverTypeText = serverParam.getTypeNode()?.getText() || ""; + assert.match(serverTypeText, /Partial>/); - assert.match(func.getReturnType().getText(), /Route\[\]/); + const returnTypeText = func.getReturnTypeNode()?.getText() || ""; + assert.match(returnTypeText, /Route\[\]/); }); it("generates switch statement for each tag", () => { @@ -420,8 +422,8 @@ describe("registerRouteHandlersByTag", () => { assert.match(bodyText, /switch \(tag\)/); assert.match(bodyText, /case "pet":/); assert.match(bodyText, /case "user":/); - assert.match(bodyText, /if \(server\.listPets\)/); - assert.match(bodyText, /if \(server\.listUsers\)/); + assert.match(bodyText, /handler: server\.listPets/); + assert.match(bodyText, /handler: server\.listUsers/); }); it("groups operations by tag correctly", () => { @@ -467,8 +469,8 @@ describe("registerRouteHandlersByTag", () => { const petCaseMatch = bodyText.match(/case "pet":[\s\S]*?break;/); assert(petCaseMatch); const petCaseBlock = petCaseMatch[0]; - assert.match(petCaseBlock, /if \(server\.listPets\)/); - assert.match(petCaseBlock, /if \(server\.createPet\)/); + assert.match(petCaseBlock, /handler: server\.listPets/); + assert.match(petCaseBlock, /handler: server\.createPet/); }); it("handles untagged operations with null case", () => { @@ -497,7 +499,7 @@ describe("registerRouteHandlersByTag", () => { const bodyText = func?.getBodyText() || ""; assert.match(bodyText, /case null:/); - assert.match(bodyText, /if \(server\.getStatus\)/); + assert.match(bodyText, /handler: server\.getStatus/); }); }); From 0ac958f77c8322e5e57cf017c9905574d224bbdd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 05:41:55 +0000 Subject: [PATCH 09/12] Replace media routes with store routes in examples/tags - Removed media tagged routes (getPetImage, getPetWebpage) - Added store tagged routes (getInventory, placeOrder) - Updated api.ts to replace mediaService with storeService - Updated app.ts to use storeService instead of mediaService - Updated app.test.ts with store route tests - Removed unused cat.jpeg file - Added Order schema to openapi.yaml components Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/tags/api.ts | 38 +++++++------ examples/tags/app.test.ts | 31 ++++++----- examples/tags/app.ts | 16 ++---- examples/tags/cat.jpeg | Bin 45357 -> 0 bytes examples/tags/gen/schema.d.ts | 76 +++++++++++++++++++------- examples/tags/gen/server.ts | 85 +++++++++++++++++------------ examples/tags/openapi.yaml | 98 ++++++++++++++++++++++++---------- 7 files changed, 222 insertions(+), 122 deletions(-) delete mode 100644 examples/tags/cat.jpeg diff --git a/examples/tags/api.ts b/examples/tags/api.ts index 874d592..9bedb9c 100644 --- a/examples/tags/api.ts +++ b/examples/tags/api.ts @@ -1,11 +1,5 @@ import type * as ServerTypes from "./gen/server.ts"; import type { Request, Response } from "express"; -import { promises as fs } from "fs"; -import { join } from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = join(__filename, ".."); // Service implementation for "pets" tag export const petsService: ServerTypes.ServerForPets = { @@ -73,28 +67,38 @@ export const petsService: ServerTypes.ServerForPets = { }, }; -// Service implementation for "media" tag -export const mediaService: ServerTypes.ServerForMedia = { - getPetImage: async (): ServerTypes.GetPetImageResult => { - const image = await fs.readFile(join(__dirname, `./cat.jpeg`), { - encoding: "base64", - }); - +// Service implementation for "store" tag +export const storeService: ServerTypes.ServerForStore = { + getInventory: async (): ServerTypes.GetInventoryResult => { return { content: { 200: { - "image/jpeg": image, + "application/json": { + inventory: { + available: 10, + pending: 5, + sold: 3, + }, + }, }, }, }; }, - getPetWebpage: async ({ parameters }): ServerTypes.GetPetWebpageResult => { - const { petId } = parameters.path; + placeOrder: async ({ requestBody }): ServerTypes.PlaceOrderResult => { + const { petId, quantity } = requestBody.content; + return { content: { 200: { - "text/html": `

Hello, pet ${petId}!

`, + "application/json": { + order: { + id: Math.floor(Math.random() * 1000), + petId, + quantity, + status: "placed", + }, + }, }, }, }; diff --git a/examples/tags/app.test.ts b/examples/tags/app.test.ts index 77ca704..6edb73a 100644 --- a/examples/tags/app.test.ts +++ b/examples/tags/app.test.ts @@ -144,28 +144,35 @@ describe("listPets", async () => { }); }); -describe("getPetImage", async () => { +describe("getInventory", async () => { it("returns 200", async () => { const response = await request(app) - .get("/api/v3/pet/123/image") - .set("Accept", "image/jpeg"); + .get("/api/v3/store/inventory") + .set("Accept", "application/json"); assert.equal(response.status, 200); - assert(response.body); - assert.equal(response.headers["content-type"], "image/jpeg"); + assert.deepEqual(response.body, { + inventory: { + available: 10, + pending: 5, + sold: 3, + }, + }); }); }); -describe("getPetWebpage", async () => { +describe("placeOrder", async () => { it("returns 200", async () => { const response = await request(app) - .get("/api/v3/pet/123/webpage") - .set("Accept", "text/html"); + .post("/api/v3/store/order") + .set("Accept", "application/json") + .send({ petId: 123, quantity: 2 }); assert.equal(response.status, 200); - assert.equal( - response.text, - "

Hello, pet 123!

", - ); + assert(response.body.order); + assert.equal(response.body.order.petId, 123); + assert.equal(response.body.order.quantity, 2); + assert.equal(response.body.order.status, "placed"); + assert(typeof response.body.order.id === "number"); }); }); diff --git a/examples/tags/app.ts b/examples/tags/app.ts index 642e25f..838c99f 100644 --- a/examples/tags/app.ts +++ b/examples/tags/app.ts @@ -2,7 +2,7 @@ import express from "express"; import type { Request, Response, NextFunction } from "express"; import { registerRouteHandlersByTag } from "./gen/server.ts"; import registerRoutes from "openapi-typescript-server-express"; -import { petsService, mediaService, untaggedService } from "./api.ts"; +import { petsService, storeService, untaggedService } from "./api.ts"; import OpenApiValidator from "express-openapi-validator"; import { NotImplementedError } from "openapi-typescript-server-runtime"; import xmlparser from "express-xml-bodyparser"; @@ -29,20 +29,10 @@ export default function makeApp() { // Register routes by tag using registerRouteHandlersByTag const petsRoutes = registerRouteHandlersByTag("pets", petsService); - const mediaRoutes = registerRouteHandlersByTag("media", mediaService); + const storeRoutes = registerRouteHandlersByTag("store", storeService); const untaggedRoutes = registerRouteHandlersByTag(null, untaggedService); - registerRoutes( - [...petsRoutes, ...mediaRoutes, ...untaggedRoutes], - apiRouter, - { - serializers: { - "image/jpeg": (content) => { - return Buffer.from(content, "base64"); - }, - }, - }, - ); + registerRoutes([...petsRoutes, ...storeRoutes, ...untaggedRoutes], apiRouter); app.use("/api/v3", apiRouter); diff --git a/examples/tags/cat.jpeg b/examples/tags/cat.jpeg deleted file mode 100644 index dca1e0bf1cd981220402f96610218bd7b5c34458..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45357 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 H(i#8R!Bqms diff --git a/examples/tags/gen/schema.d.ts b/examples/tags/gen/schema.d.ts index 3df982f..24eb63e 100644 --- a/examples/tags/gen/schema.d.ts +++ b/examples/tags/gen/schema.d.ts @@ -53,14 +53,14 @@ export interface paths { patch?: never; trace?: never; }; - "/pet/{petId}/image": { + "/store/inventory": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getPetImage"]; + get: operations["getInventory"]; put?: never; post?: never; delete?: never; @@ -69,16 +69,16 @@ export interface paths { patch?: never; trace?: never; }; - "/pet/{petId}/webpage": { + "/store/order": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getPetWebpage"]; + get?: never; put?: never; - post?: never; + post: operations["placeOrder"]; delete?: never; options?: never; head?: never; @@ -119,6 +119,19 @@ export interface components { /** @enum {string} */ status: "available" | "pending" | "sold"; }; + Order: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + petId: number; + /** Format: int32 */ + quantity: number; + /** + * @description Order Status + * @enum {string} + */ + status?: "placed" | "approved" | "delivered"; + }; }; responses: never; parameters: never; @@ -281,14 +294,11 @@ export interface operations { }; }; }; - getPetImage: { + getInventory: { parameters: { query?: never; header?: never; - path: { - /** @description ID of pet to return */ - petId: number; - }; + path?: never; cookie?: never; }; requestBody?: never; @@ -299,22 +309,41 @@ export interface operations { [name: string]: unknown; }; content: { - "image/jpeg": string; + "application/json": { + inventory?: { + [key: string]: number; + }; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; }; }; - getPetWebpage: { + placeOrder: { parameters: { query?: never; header?: never; - path: { - /** @description ID of pet to return */ - petId: number; - }; + path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": { + /** Format: int64 */ + petId: number; + /** Format: int32 */ + quantity: number; + }; + }; + }; responses: { /** @description successful operation */ 200: { @@ -322,7 +351,18 @@ export interface operations { [name: string]: unknown; }; content: { - "text/html": string; + "application/json": { + order?: components["schemas"]["Order"]; + }; + }; + }; + /** @description unexpected error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; }; }; }; diff --git a/examples/tags/gen/server.ts b/examples/tags/gen/server.ts index 5cfb53f..c367ddb 100644 --- a/examples/tags/gen/server.ts +++ b/examples/tags/gen/server.ts @@ -125,39 +125,56 @@ export async function mixedContentTypesUnimplemented(): MixedContentTypesResult throw new NotImplementedError() } -export interface GetPetImageArgs { - parameters: paths['/pet/{petId}/image']['get']['parameters']; +export interface GetInventoryArgs { + parameters: paths['/store/inventory']['get']['parameters']; contentType: string; req: Req; res: Res; } -interface GetPetImageResult200 { - content: { 200: paths['/pet/{petId}/image']['get']['responses']['200']['content'] }; +interface GetInventoryResult200 { + content: { 200: paths['/store/inventory']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } -export type GetPetImageResult = Promise; +interface GetInventoryResultDefault { + content: { default: paths['/store/inventory']['get']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type GetInventoryResult = Promise; -export async function getPetImageUnimplemented(): GetPetImageResult { +export async function getInventoryUnimplemented(): GetInventoryResult { throw new NotImplementedError() } -export interface GetPetWebpageArgs { - parameters: paths['/pet/{petId}/webpage']['get']['parameters']; +export interface PlaceOrderArgs { + parameters: paths['/store/order']['post']['parameters']; contentType: string; req: Req; res: Res; + requestBody: { + mediaType: "application/json"; + content: paths['/store/order']['post']['requestBody']['content']['application/json'] + } + ; } -interface GetPetWebpageResult200 { - content: { 200: paths['/pet/{petId}/webpage']['get']['responses']['200']['content'] }; +interface PlaceOrderResult200 { + content: { 200: paths['/store/order']['post']['responses']['200']['content'] }; headers?: { [name: string]: any }; } -export type GetPetWebpageResult = Promise; +interface PlaceOrderResultDefault { + content: { default: paths['/store/order']['post']['responses']['default']['content'] }; + headers?: { [name: string]: any }; + status: number; +} + +export type PlaceOrderResult = Promise; -export async function getPetWebpageUnimplemented(): GetPetWebpageResult { +export async function placeOrderUnimplemented(): PlaceOrderResult { throw new NotImplementedError() } @@ -175,12 +192,12 @@ export interface Server { mixedContentTypes: ( args: MixedContentTypesArgs ) => MixedContentTypesResult; - getPetImage: ( - args: GetPetImageArgs - ) => GetPetImageResult; - getPetWebpage: ( - args: GetPetWebpageArgs - ) => GetPetWebpageResult; + getInventory: ( + args: GetInventoryArgs + ) => GetInventoryResult; + placeOrder: ( + args: PlaceOrderArgs + ) => PlaceOrderResult; } export function registerRouteHandlers(server: Server): Route[] { @@ -207,18 +224,18 @@ export function registerRouteHandlers(server: Server): Route }, { method: "get", - path: "/pet/{petId}/image", - handler: server.getPetImage as Route["handler"], + path: "/store/inventory", + handler: server.getInventory as Route["handler"], }, { - method: "get", - path: "/pet/{petId}/webpage", - handler: server.getPetWebpage as Route["handler"], + method: "post", + path: "/store/order", + handler: server.placeOrder as Route["handler"], }, ] } -export type Tag = "pets" | "media" | null; +export type Tag = "pets" | "store" | null; export interface ServerForPets { listPets: (args: ListPetsArgs) => ListPetsResult; @@ -230,14 +247,14 @@ export interface ServerForUntagged { mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; } -export interface ServerForMedia { - getPetImage: (args: GetPetImageArgs) => GetPetImageResult; - getPetWebpage: (args: GetPetWebpageArgs) => GetPetWebpageResult; +export interface ServerForStore { + getInventory: (args: GetInventoryArgs) => GetInventoryResult; + placeOrder: (args: PlaceOrderArgs) => PlaceOrderResult; } export function registerRouteHandlersByTag(tag: "pets", server: ServerForPets): Route[]; export function registerRouteHandlersByTag(tag: null, server: ServerForUntagged): Route[]; -export function registerRouteHandlersByTag(tag: "media", server: ServerForMedia): Route[]; +export function registerRouteHandlersByTag(tag: "store", server: ServerForStore): Route[]; export function registerRouteHandlersByTag(tag: Tag, server: Partial>): Route[] { const routes: Route[] = []; @@ -266,16 +283,16 @@ export function registerRouteHandlersByTag(tag: Tag, server: Partial Date: Fri, 17 Oct 2025 19:22:40 +0000 Subject: [PATCH 10/12] Remove XML and urlencoded support, replace mixedContentTypes with listUsers - Removed application/xml and application/x-www-form-urlencoded content types from updatePetWithForm endpoint - Replaced mixedContentTypes untagged route with listUsers route - Removed XML and urlencoded tests from app.test.ts - Removed json2xml import and xmlparser middleware (no longer needed) - Added User schema to openapi.yaml components - Updated api.ts to implement listUsers instead of mixedContentTypes - Updated tests for new listUsers endpoint Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com> --- examples/tags/api.ts | 31 +++----------- examples/tags/app.test.ts | 78 ++++------------------------------- examples/tags/app.ts | 2 - examples/tags/gen/schema.d.ts | 39 +++++++----------- examples/tags/gen/server.ts | 54 +++++++++--------------- examples/tags/openapi.yaml | 68 ++++++++++-------------------- 6 files changed, 69 insertions(+), 203 deletions(-) diff --git a/examples/tags/api.ts b/examples/tags/api.ts index 9bedb9c..e2bba19 100644 --- a/examples/tags/api.ts +++ b/examples/tags/api.ts @@ -108,36 +108,15 @@ export const storeService: ServerTypes.ServerForStore = { // Service implementation for untagged operations export const untaggedService: ServerTypes.ServerForUntagged = { - mixedContentTypes: async ({ - parameters, - requestBody, - contentType, - }): ServerTypes.MixedContentTypesResult => { - const { petId } = parameters.path; - let status: "available" | "pending" | "sold" | undefined; - - // Since each content type has different structures, - // use the request content type and requestBody discriminator to narrow the type in each case. - - if ( - contentType === "application/json" && - requestBody.mediaType === "application/json" - ) { - status = requestBody.content.jsonstatus; - } - - if ( - contentType == "application/xml" && - requestBody.mediaType === "application/xml" - ) { - status = requestBody.content.xmlstatus; - } - + listUsers: async (): ServerTypes.ListUsersResult => { return { content: { 200: { "application/json": { - pet: { id: petId, name: "dog", status }, + users: [ + { id: 1, username: "john_doe", email: "john@example.com" }, + { id: 2, username: "jane_smith", email: "jane@example.com" }, + ], }, }, }, diff --git a/examples/tags/app.test.ts b/examples/tags/app.test.ts index 6edb73a..8616487 100644 --- a/examples/tags/app.test.ts +++ b/examples/tags/app.test.ts @@ -2,7 +2,6 @@ import { describe, it } from "node:test"; import assert from "node:assert"; import makeApp from "./app.ts"; import request from "supertest"; -import { json2xml } from "xml-js"; const app = makeApp(); @@ -49,81 +48,20 @@ describe("updatePetWithForm", async () => { }, }); }); - - it("accepts xml input", async () => { - const xmlData = json2xml(JSON.stringify({ status: "sold" }), { - compact: true, - ignoreComment: true, - spaces: 4, - }); - - const response = await request(app) - .post("/api/v3/pet/123?name=cat") - .set("Content-Type", "application/xml") - .send(xmlData); - - assert.equal(response.status, 200); - assert.deepEqual(response.body, { - pet: { - id: 123, - name: "cat", - status: "sold", - }, - }); - }); - - it("accepts form-urlencoded input", async () => { - const response = await request(app) - .post("/api/v3/pet/123?name=cat") - .set("Content-Type", "application/x-www-form-urlencoded") - .send("status=sold"); - - assert.equal(response.status, 200); - assert.deepEqual(response.body, { - pet: { - id: 123, - name: "cat", - status: "sold", - }, - }); - }); }); -describe("mixed content types with different structures", async () => { - it("handles json by default", async () => { - const response = await request(app) - .post("/api/v3/pet/123/mixed-content-types") - .send({ jsonstatus: "sold" }); - - assert.equal(response.status, 200); - assert.deepEqual(response.body, { - pet: { - id: 123, - name: "dog", - status: "sold", - }, - }); - }); - - it("handles xml", async () => { - const xmlData = json2xml(JSON.stringify({ xmlstatus: "sold" }), { - compact: true, - ignoreComment: true, - spaces: 4, - }); - +describe("listUsers", async () => { + it("returns 200", async () => { const response = await request(app) - .post("/api/v3/pet/123/mixed-content-types") - .set("Content-Type", "application/xml") - .send(xmlData); + .get("/api/v3/users") + .set("Accept", "application/json"); assert.equal(response.status, 200); assert.deepEqual(response.body, { - pet: { - id: 123, - name: "dog", - status: "sold", - }, + users: [ + { id: 1, username: "john_doe", email: "john@example.com" }, + { id: 2, username: "jane_smith", email: "jane@example.com" }, + ], }); }); }); diff --git a/examples/tags/app.ts b/examples/tags/app.ts index 838c99f..dfb6209 100644 --- a/examples/tags/app.ts +++ b/examples/tags/app.ts @@ -5,7 +5,6 @@ import registerRoutes from "openapi-typescript-server-express"; import { petsService, storeService, untaggedService } from "./api.ts"; import OpenApiValidator from "express-openapi-validator"; import { NotImplementedError } from "openapi-typescript-server-runtime"; -import xmlparser from "express-xml-bodyparser"; export default function makeApp() { const app = express(); @@ -15,7 +14,6 @@ export default function makeApp() { }); app.use(express.json()); - app.use(xmlparser()); app.use(express.urlencoded({ extended: true })); const apiRouter = express(); diff --git a/examples/tags/gen/schema.d.ts b/examples/tags/gen/schema.d.ts index 24eb63e..b7b2f01 100644 --- a/examples/tags/gen/schema.d.ts +++ b/examples/tags/gen/schema.d.ts @@ -37,16 +37,17 @@ export interface paths { patch?: never; trace?: never; }; - "/pet/{petId}/mixed-content-types": { + "/users": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** Returns all users from the system */ + get: operations["listUsers"]; put?: never; - post: operations["mixedContentTypes"]; + post?: never; delete?: never; options?: never; head?: never; @@ -132,6 +133,12 @@ export interface components { */ status?: "placed" | "approved" | "delivered"; }; + User: { + /** Format: int64 */ + id: number; + username: string; + email?: string; + }; }; responses: never; parameters: never; @@ -222,8 +229,6 @@ export interface operations { requestBody: { content: { "application/json": components["schemas"]["UpdatePetInput"]; - "application/xml": components["schemas"]["UpdatePetInput"]; - "application/x-www-form-urlencoded": components["schemas"]["UpdatePetInput"]; }; }; responses: { @@ -249,37 +254,23 @@ export interface operations { }; }; }; - mixedContentTypes: { + listUsers: { parameters: { query?: never; header?: never; - path: { - /** @description ID of pet that needs to be updated */ - petId: number; - }; + path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": { - /** @enum {string} */ - jsonstatus: "available" | "pending" | "sold"; - }; - "application/xml": { - /** @enum {string} */ - xmlstatus: "available" | "pending" | "sold"; - }; - }; - }; + requestBody?: never; responses: { - /** @description successful operation */ + /** @description A list of users. */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": { - pet?: components["schemas"]["Pet"]; + users?: components["schemas"]["User"][]; }; }; }; diff --git a/examples/tags/gen/server.ts b/examples/tags/gen/server.ts index c367ddb..0e94861 100644 --- a/examples/tags/gen/server.ts +++ b/examples/tags/gen/server.ts @@ -64,14 +64,6 @@ export interface UpdatePetWithFormArgs { mediaType: "application/json"; content: paths['/pet/{petId}']['post']['requestBody']['content']['application/json'] } - | { - mediaType: "application/xml"; - content: paths['/pet/{petId}']['post']['requestBody']['content']['application/xml'] - } - | { - mediaType: "application/x-www-form-urlencoded"; - content: paths['/pet/{petId}']['post']['requestBody']['content']['application/x-www-form-urlencoded'] - } ; } @@ -92,36 +84,27 @@ export async function updatePetWithFormUnimplemented(): UpdatePetWithFormResult throw new NotImplementedError() } -export interface MixedContentTypesArgs { - parameters: paths['/pet/{petId}/mixed-content-types']['post']['parameters']; +export interface ListUsersArgs { + parameters: paths['/users']['get']['parameters']; contentType: string; req: Req; res: Res; - requestBody: { - mediaType: "application/json"; - content: paths['/pet/{petId}/mixed-content-types']['post']['requestBody']['content']['application/json'] - } - | { - mediaType: "application/xml"; - content: paths['/pet/{petId}/mixed-content-types']['post']['requestBody']['content']['application/xml'] - } - ; } -interface MixedContentTypesResult200 { - content: { 200: paths['/pet/{petId}/mixed-content-types']['post']['responses']['200']['content'] }; +interface ListUsersResult200 { + content: { 200: paths['/users']['get']['responses']['200']['content'] }; headers?: { [name: string]: any }; } -interface MixedContentTypesResultDefault { - content: { default: paths['/pet/{petId}/mixed-content-types']['post']['responses']['default']['content'] }; +interface ListUsersResultDefault { + content: { default: paths['/users']['get']['responses']['default']['content'] }; headers?: { [name: string]: any }; status: number; } -export type MixedContentTypesResult = Promise; +export type ListUsersResult = Promise; -export async function mixedContentTypesUnimplemented(): MixedContentTypesResult { +export async function listUsersUnimplemented(): ListUsersResult { throw new NotImplementedError() } @@ -189,9 +172,10 @@ export interface Server { updatePetWithForm: ( args: UpdatePetWithFormArgs ) => UpdatePetWithFormResult; - mixedContentTypes: ( - args: MixedContentTypesArgs - ) => MixedContentTypesResult; + /** Returns all users from the system */ + listUsers: ( + args: ListUsersArgs + ) => ListUsersResult; getInventory: ( args: GetInventoryArgs ) => GetInventoryResult; @@ -218,9 +202,9 @@ export function registerRouteHandlers(server: Server): Route handler: server.updatePetWithForm as Route["handler"], }, { - method: "post", - path: "/pet/{petId}/mixed-content-types", - handler: server.mixedContentTypes as Route["handler"], + method: "get", + path: "/users", + handler: server.listUsers as Route["handler"], }, { method: "get", @@ -244,7 +228,7 @@ export interface ServerForPets { } export interface ServerForUntagged { - mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; + listUsers: (args: ListUsersArgs) => ListUsersResult; } export interface ServerForStore { @@ -278,9 +262,9 @@ export function registerRouteHandlersByTag(tag: Tag, server: Partial Date: Sat, 1 Nov 2025 15:00:55 -0400 Subject: [PATCH 11/12] fix packages --- package-lock.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7e431fa..59d274e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,43 @@ "xml-js": "^1.6.11" } }, + "examples/tags/node_modules/openapi-typescript-server": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/openapi-typescript-server/-/openapi-typescript-server-0.0.12.tgz", + "integrity": "sha512-xVHDnUdwGFGolevKYEdb92wBqNl2d+w/+Ee70c7wJdgTjTfoqIYA6X4sB9NEPfynBC08oITEs0sN7SqU+TL08Q==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "js-yaml": "^4.1.0", + "openapi-typescript-server-runtime": "0.0.12", + "ts-morph": "^27.0.0", + "zod": "^4.0.14" + }, + "bin": { + "openapi-typescript-server": "bin/cli/index.cjs" + } + }, + "examples/tags/node_modules/openapi-typescript-server-express": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/openapi-typescript-server-express/-/openapi-typescript-server-express-0.0.12.tgz", + "integrity": "sha512-2InKXog1ALuIgtsBSUq+LYxbKC42tdd+xXpwxQUbUFiKZAgStKMXBB8vLVm1svYxhcDGlCfbsXteBPHMsLckBw==", + "license": "MIT", + "dependencies": { + "openapi-typescript-server-runtime": "0.0.12" + }, + "peerDependencies": { + "express": "^4.21.2 || ^5.0.0" + } + }, + "examples/tags/node_modules/openapi-typescript-server-runtime": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/openapi-typescript-server-runtime/-/openapi-typescript-server-runtime-0.0.12.tgz", + "integrity": "sha512-nVWF9UBJDKXGiA4SKq1wwMDJZ9SgFynC8iL3Bshcr0VIR8aNLQfEZNQMAst0Z8H4XnEiSk2bO8VUKOSrDXF7/Q==", + "license": "MIT", + "dependencies": { + "zod": "^4.0.14" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.1.1.tgz", From 7825ace2eb814211e60987a1ba88e50da47f47a2 Mon Sep 17 00:00:00 2001 From: jasonblanchard Date: Sat, 1 Nov 2025 17:38:17 -0400 Subject: [PATCH 12/12] updated --- examples/kitchensink/gen/server.ts | 6 +++++ examples/tags/gen/server.ts | 2 +- examples/tags/package.json | 6 ++--- package-lock.json | 43 +++--------------------------- 4 files changed, 13 insertions(+), 44 deletions(-) diff --git a/examples/kitchensink/gen/server.ts b/examples/kitchensink/gen/server.ts index 2e267a9..844627a 100644 --- a/examples/kitchensink/gen/server.ts +++ b/examples/kitchensink/gen/server.ts @@ -255,6 +255,7 @@ export type Tag = null; export interface ServerForUntagged { listPets: (args: ListPetsArgs) => ListPetsResult; + listPetsBySize: (args: ListPetsBySizeArgs) => ListPetsBySizeResult; getPetById: (args: GetPetByIdArgs) => GetPetByIdResult; updatePetWithForm: (args: UpdatePetWithFormArgs) => UpdatePetWithFormResult; mixedContentTypes: (args: MixedContentTypesArgs) => MixedContentTypesResult; @@ -273,6 +274,11 @@ export function registerRouteHandlersByTag(tag: Tag, server: Partial