Skip to content

Commit 78e3c25

Browse files
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>
1 parent 5bf59e0 commit 78e3c25

File tree

5 files changed

+126
-3
lines changed

5 files changed

+126
-3
lines changed

examples/docs/gen/server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ export function registerRouteHandlers<Req, Res>(server: Server<Req, Res>): Route
7575

7676
export type Tag = null;
7777

78+
export interface ServerForUntagged<Req = unknown, Res = unknown> {
79+
makePetSpeak?: (args: MakePetSpeakArgs<Req, Res>) => MakePetSpeakResult;
80+
uhoh?: (args: UhohArgs<Req, Res>) => UhohResult;
81+
}
82+
83+
export function registerRouteHandlersByTag<Req, Res>(tag: null, server: Partial<ServerForUntagged<Req, Res>>): Route[];
7884
export function registerRouteHandlersByTag<Req, Res>(tag: Tag, server: Partial<Server<Req, Res>>): Route[] {
7985
const routes: Route[] = [];
8086

examples/kitchensink/gen/server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,16 @@ export function registerRouteHandlers<Req, Res>(server: Server<Req, Res>): Route
253253

254254
export type Tag = null;
255255

256+
export interface ServerForUntagged<Req = unknown, Res = unknown> {
257+
listPets?: (args: ListPetsArgs<Req, Res>) => ListPetsResult;
258+
getPetById?: (args: GetPetByIdArgs<Req, Res>) => GetPetByIdResult;
259+
updatePetWithForm?: (args: UpdatePetWithFormArgs<Req, Res>) => UpdatePetWithFormResult;
260+
mixedContentTypes?: (args: MixedContentTypesArgs<Req, Res>) => MixedContentTypesResult;
261+
getPetImage?: (args: GetPetImageArgs<Req, Res>) => GetPetImageResult;
262+
getPetWebpage?: (args: GetPetWebpageArgs<Req, Res>) => GetPetWebpageResult;
263+
}
264+
265+
export function registerRouteHandlersByTag<Req, Res>(tag: null, server: Partial<ServerForUntagged<Req, Res>>): Route[];
256266
export function registerRouteHandlersByTag<Req, Res>(tag: Tag, server: Partial<Server<Req, Res>>): Route[] {
257267
const routes: Route[] = [];
258268

examples/petstore/gen/server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,37 @@ export function registerRouteHandlers<Req, Res>(server: Server<Req, Res>): Route
746746

747747
export type Tag = "pet" | "store" | "user";
748748

749+
export interface ServerForPet<Req = unknown, Res = unknown> {
750+
updatePet?: (args: UpdatePetArgs<Req, Res>) => UpdatePetResult;
751+
addPet?: (args: AddPetArgs<Req, Res>) => AddPetResult;
752+
findPetsByStatus?: (args: FindPetsByStatusArgs<Req, Res>) => FindPetsByStatusResult;
753+
findPetsByTags?: (args: FindPetsByTagsArgs<Req, Res>) => FindPetsByTagsResult;
754+
getPetById?: (args: GetPetByIdArgs<Req, Res>) => GetPetByIdResult;
755+
updatePetWithForm?: (args: UpdatePetWithFormArgs<Req, Res>) => UpdatePetWithFormResult;
756+
deletePet?: (args: DeletePetArgs<Req, Res>) => DeletePetResult;
757+
uploadFile?: (args: UploadFileArgs<Req, Res>) => UploadFileResult;
758+
}
759+
760+
export interface ServerForStore<Req = unknown, Res = unknown> {
761+
getInventory?: (args: GetInventoryArgs<Req, Res>) => GetInventoryResult;
762+
placeOrder?: (args: PlaceOrderArgs<Req, Res>) => PlaceOrderResult;
763+
getOrderById?: (args: GetOrderByIdArgs<Req, Res>) => GetOrderByIdResult;
764+
deleteOrder?: (args: DeleteOrderArgs<Req, Res>) => DeleteOrderResult;
765+
}
766+
767+
export interface ServerForUser<Req = unknown, Res = unknown> {
768+
createUser?: (args: CreateUserArgs<Req, Res>) => CreateUserResult;
769+
createUsersWithListInput?: (args: CreateUsersWithListInputArgs<Req, Res>) => CreateUsersWithListInputResult;
770+
loginUser?: (args: LoginUserArgs<Req, Res>) => LoginUserResult;
771+
logoutUser?: (args: LogoutUserArgs<Req, Res>) => LogoutUserResult;
772+
getUserByName?: (args: GetUserByNameArgs<Req, Res>) => GetUserByNameResult;
773+
updateUser?: (args: UpdateUserArgs<Req, Res>) => UpdateUserResult;
774+
deleteUser?: (args: DeleteUserArgs<Req, Res>) => DeleteUserResult;
775+
}
776+
777+
export function registerRouteHandlersByTag<Req, Res>(tag: "pet", server: Partial<ServerForPet<Req, Res>>): Route[];
778+
export function registerRouteHandlersByTag<Req, Res>(tag: "store", server: Partial<ServerForStore<Req, Res>>): Route[];
779+
export function registerRouteHandlersByTag<Req, Res>(tag: "user", server: Partial<ServerForUser<Req, Res>>): Route[];
749780
export function registerRouteHandlersByTag<Req, Res>(tag: Tag, server: Partial<Server<Req, Res>>): Route[] {
750781
const routes: Route[] = [];
751782

packages/openapi-typescript-server/bin/cli/index.cjs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,29 @@ function generate(spec, types, outpath, version) {
515515
});
516516
}
517517
});
518-
sourceFile.addFunction({
518+
Object.entries(tagToOperations).forEach(([tag, operations]) => {
519+
const interfaceName = tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`;
520+
const tagInterface = sourceFile.addInterface({
521+
name: interfaceName,
522+
isExported: true,
523+
typeParameters: [
524+
{ name: "Req", default: "unknown" },
525+
{ name: "Res", default: "unknown" }
526+
]
527+
});
528+
operations.forEach((operationId) => {
529+
const op = operationsById[operationId];
530+
if (op) {
531+
tagInterface.addProperty({
532+
name: operationId,
533+
type: `(args: ${op.args}<Req, Res>) => ${op.result}`,
534+
hasQuestionToken: true
535+
// Make it optional since it's a partial implementation
536+
});
537+
}
538+
});
539+
});
540+
const registerByTagFunc = sourceFile.addFunction({
519541
name: "registerRouteHandlersByTag",
520542
isExported: true,
521543
parameters: [
@@ -554,6 +576,18 @@ function generate(spec, types, outpath, version) {
554576
writer.writeLine("return routes;");
555577
}
556578
});
579+
Object.entries(tagToOperations).forEach(([tag, _operations]) => {
580+
const tagValue = tag === "null" ? "null" : `"${tag}"`;
581+
const interfaceName = tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`;
582+
registerByTagFunc.addOverload({
583+
parameters: [
584+
{ name: "tag", type: tagValue },
585+
{ name: "server", type: `Partial<${interfaceName}<Req, Res>>` }
586+
],
587+
typeParameters: [{ name: "Req" }, { name: "Res" }],
588+
returnType: "Route[]"
589+
});
590+
});
557591
sourceFile.insertText(
558592
0,
559593
`/**

packages/openapi-typescript-server/src/cli/generate.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,34 @@ export default function generate(
275275
}
276276
});
277277

278-
// Generate registerRouteHandlersByTag function
279-
sourceFile.addFunction({
278+
// Generate a ServerForTag interface for each tag
279+
Object.entries(tagToOperations).forEach(([tag, operations]) => {
280+
const interfaceName =
281+
tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`;
282+
const tagInterface = sourceFile.addInterface({
283+
name: interfaceName,
284+
isExported: true,
285+
typeParameters: [
286+
{ name: "Req", default: "unknown" },
287+
{ name: "Res", default: "unknown" },
288+
],
289+
});
290+
291+
// Add only operations associated with this tag
292+
operations.forEach((operationId) => {
293+
const op = operationsById[operationId];
294+
if (op) {
295+
tagInterface.addProperty({
296+
name: operationId,
297+
type: `(args: ${op.args}<Req, Res>) => ${op.result}`,
298+
hasQuestionToken: true, // Make it optional since it's a partial implementation
299+
});
300+
}
301+
});
302+
});
303+
304+
// Generate registerRouteHandlersByTag function with overloads
305+
const registerByTagFunc = sourceFile.addFunction({
280306
name: "registerRouteHandlersByTag",
281307
isExported: true,
282308
parameters: [
@@ -323,6 +349,22 @@ export default function generate(
323349
},
324350
});
325351

352+
// Add overload signatures for each tag
353+
Object.entries(tagToOperations).forEach(([tag, _operations]) => {
354+
const tagValue = tag === "null" ? "null" : `"${tag}"`;
355+
const interfaceName =
356+
tag === "null" ? "ServerForUntagged" : `ServerFor${capitalize(tag)}`;
357+
358+
registerByTagFunc.addOverload({
359+
parameters: [
360+
{ name: "tag", type: tagValue },
361+
{ name: "server", type: `Partial<${interfaceName}<Req, Res>>` },
362+
],
363+
typeParameters: [{ name: "Req" }, { name: "Res" }],
364+
returnType: "Route[]",
365+
});
366+
});
367+
326368
sourceFile.insertText(
327369
0,
328370
`/**

0 commit comments

Comments
 (0)