Skip to content

Commit d3383c2

Browse files
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>
1 parent 17d0bb9 commit d3383c2

File tree

7 files changed

+154
-78
lines changed

7 files changed

+154
-78
lines changed

examples/tags/api.ts

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type * as ServerTypes from "./gen/server.ts";
2-
import * as server from "./gen/server.ts";
32
import type { Request, Response } from "express";
43
import { promises as fs } from "fs";
54
import { join } from "path";
@@ -8,8 +7,24 @@ import { fileURLToPath } from "url";
87
const __filename = fileURLToPath(import.meta.url);
98
const __dirname = join(__filename, "..");
109

11-
const API: ServerTypes.Server<Request, Response> = {
12-
getPetById: async ({ parameters, req }): ServerTypes.GetPetByIdResult => {
10+
// Service implementation for "pets" tag
11+
export const petsService: ServerTypes.ServerForPets<Request, Response> = {
12+
listPets: async (): ServerTypes.ListPetsResult => {
13+
return {
14+
content: {
15+
200: {
16+
"application/json": {
17+
pets: [
18+
{ id: 1, name: "dog" },
19+
{ id: 2, name: "cat" },
20+
],
21+
},
22+
},
23+
},
24+
};
25+
},
26+
27+
getPetById: async ({ parameters }): ServerTypes.GetPetByIdResult => {
1328
if (parameters.path.petId === 42) {
1429
return {
1530
content: {
@@ -56,45 +71,10 @@ const API: ServerTypes.Server<Request, Response> = {
5671
},
5772
};
5873
},
74+
};
5975

60-
mixedContentTypes: async ({
61-
parameters,
62-
requestBody,
63-
contentType,
64-
}): ServerTypes.MixedContentTypesResult => {
65-
const { petId } = parameters.path;
66-
let status: "available" | "pending" | "sold" | undefined;
67-
68-
// Since each content type has different structures,
69-
// use the request content type and requestBody discriminator to narrow the type in each case.
70-
71-
if (
72-
contentType === "application/json" &&
73-
requestBody.mediaType === "application/json"
74-
) {
75-
status = requestBody.content.jsonstatus;
76-
}
77-
78-
if (
79-
contentType == "application/xml" &&
80-
requestBody.mediaType === "application/xml"
81-
) {
82-
status = requestBody.content.xmlstatus;
83-
}
84-
85-
return {
86-
content: {
87-
200: {
88-
"application/json": {
89-
pet: { id: petId, name: "dog", status },
90-
},
91-
},
92-
},
93-
};
94-
},
95-
96-
listPets: server.listPetsUnimplemented,
97-
76+
// Service implementation for "media" tag
77+
export const mediaService: ServerTypes.ServerForMedia<Request, Response> = {
9878
getPetImage: async (): ServerTypes.GetPetImageResult => {
9979
const image = await fs.readFile(join(__dirname, `./cat.jpeg`), {
10080
encoding: "base64",
@@ -121,4 +101,42 @@ const API: ServerTypes.Server<Request, Response> = {
121101
},
122102
};
123103

124-
export default API;
104+
// Service implementation for untagged operations
105+
export const untaggedService: ServerTypes.ServerForUntagged<Request, Response> =
106+
{
107+
mixedContentTypes: async ({
108+
parameters,
109+
requestBody,
110+
contentType,
111+
}): ServerTypes.MixedContentTypesResult => {
112+
const { petId } = parameters.path;
113+
let status: "available" | "pending" | "sold" | undefined;
114+
115+
// Since each content type has different structures,
116+
// use the request content type and requestBody discriminator to narrow the type in each case.
117+
118+
if (
119+
contentType === "application/json" &&
120+
requestBody.mediaType === "application/json"
121+
) {
122+
status = requestBody.content.jsonstatus;
123+
}
124+
125+
if (
126+
contentType == "application/xml" &&
127+
requestBody.mediaType === "application/xml"
128+
) {
129+
status = requestBody.content.xmlstatus;
130+
}
131+
132+
return {
133+
content: {
134+
200: {
135+
"application/json": {
136+
pet: { id: petId, name: "dog", status },
137+
},
138+
},
139+
},
140+
};
141+
},
142+
};

examples/tags/app.test.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -129,38 +129,43 @@ describe("mixed content types with different structures", async () => {
129129
});
130130

131131
describe("listPets", async () => {
132-
it("propagates unimplemented error", async () => {
132+
it("returns 200", async () => {
133133
const response = await request(app)
134134
.get("/api/v3/pets")
135135
.set("Accept", "application/json");
136136

137-
assert.equal(response.status, 501);
138-
assert.equal(response.body.message, "Not Implemented");
137+
assert.equal(response.status, 200);
138+
assert.deepEqual(response.body, {
139+
pets: [
140+
{ id: 1, name: "dog" },
141+
{ id: 2, name: "cat" },
142+
],
143+
});
139144
});
145+
});
140146

141-
describe("getPetImage", async () => {
142-
it("returns 200", async () => {
143-
const response = await request(app)
144-
.get("/api/v3/pet/123/image")
145-
.set("Accept", "image/jpeg");
147+
describe("getPetImage", async () => {
148+
it("returns 200", async () => {
149+
const response = await request(app)
150+
.get("/api/v3/pet/123/image")
151+
.set("Accept", "image/jpeg");
146152

147-
assert.equal(response.status, 200);
148-
assert(response.body);
149-
assert.equal(response.headers["content-type"], "image/jpeg");
150-
});
153+
assert.equal(response.status, 200);
154+
assert(response.body);
155+
assert.equal(response.headers["content-type"], "image/jpeg");
151156
});
157+
});
152158

153-
describe("getPetWebpage", async () => {
154-
it("returns 200", async () => {
155-
const response = await request(app)
156-
.get("/api/v3/pet/123/webpage")
157-
.set("Accept", "text/html");
158-
159-
assert.equal(response.status, 200);
160-
assert.equal(
161-
response.text,
162-
"<html><body><h1>Hello, pet 123!</h1></body></html>",
163-
);
164-
});
159+
describe("getPetWebpage", async () => {
160+
it("returns 200", async () => {
161+
const response = await request(app)
162+
.get("/api/v3/pet/123/webpage")
163+
.set("Accept", "text/html");
164+
165+
assert.equal(response.status, 200);
166+
assert.equal(
167+
response.text,
168+
"<html><body><h1>Hello, pet 123!</h1></body></html>",
169+
);
165170
});
166171
});

examples/tags/app.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import express from "express";
22
import type { Request, Response, NextFunction } from "express";
3-
import { registerRouteHandlers } from "./gen/server.ts";
3+
import { registerRouteHandlersByTag } from "./gen/server.ts";
44
import registerRoutes from "openapi-typescript-server-express";
5-
import API from "./api.ts";
5+
import { petsService, mediaService, untaggedService } from "./api.ts";
66
import OpenApiValidator from "express-openapi-validator";
77
import { NotImplementedError } from "openapi-typescript-server-runtime";
88
import xmlparser from "express-xml-bodyparser";
@@ -26,13 +26,23 @@ export default function makeApp() {
2626
validateResponses: false,
2727
}),
2828
);
29-
registerRoutes(registerRouteHandlers(API), apiRouter, {
30-
serializers: {
31-
"image/jpeg": (content) => {
32-
return Buffer.from(content, "base64");
29+
30+
// Register routes by tag using registerRouteHandlersByTag
31+
const petsRoutes = registerRouteHandlersByTag("pets", petsService);
32+
const mediaRoutes = registerRouteHandlersByTag("media", mediaService);
33+
const untaggedRoutes = registerRouteHandlersByTag(null, untaggedService);
34+
35+
registerRoutes(
36+
[...petsRoutes, ...mediaRoutes, ...untaggedRoutes],
37+
apiRouter,
38+
{
39+
serializers: {
40+
"image/jpeg": (content) => {
41+
return Buffer.from(content, "base64");
42+
},
3343
},
3444
},
35-
});
45+
);
3646

3747
app.use("/api/v3", apiRouter);
3848

examples/tags/gen/server.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Do not make direct changes to the file.
44
*/
55

6-
import type { paths } from "./schema";
6+
import type { paths } from "./schema.d.ts";
77
import type { Route } from "openapi-typescript-server-runtime";
88
import { NotImplementedError } from "openapi-typescript-server-runtime";
99

@@ -218,23 +218,31 @@ export function registerRouteHandlers<Req, Res>(server: Server<Req, Res>): Route
218218
]
219219
}
220220

221-
export type Tag = null;
221+
export type Tag = "pets" | "media" | null;
222222

223-
export interface ServerForUntagged<Req = unknown, Res = unknown> {
223+
export interface ServerForPets<Req = unknown, Res = unknown> {
224224
listPets: (args: ListPetsArgs<Req, Res>) => ListPetsResult;
225225
getPetById: (args: GetPetByIdArgs<Req, Res>) => GetPetByIdResult;
226226
updatePetWithForm: (args: UpdatePetWithFormArgs<Req, Res>) => UpdatePetWithFormResult;
227+
}
228+
229+
export interface ServerForUntagged<Req = unknown, Res = unknown> {
227230
mixedContentTypes: (args: MixedContentTypesArgs<Req, Res>) => MixedContentTypesResult;
231+
}
232+
233+
export interface ServerForMedia<Req = unknown, Res = unknown> {
228234
getPetImage: (args: GetPetImageArgs<Req, Res>) => GetPetImageResult;
229235
getPetWebpage: (args: GetPetWebpageArgs<Req, Res>) => GetPetWebpageResult;
230236
}
231237

238+
export function registerRouteHandlersByTag<Req, Res>(tag: "pets", server: ServerForPets<Req, Res>): Route[];
232239
export function registerRouteHandlersByTag<Req, Res>(tag: null, server: ServerForUntagged<Req, Res>): Route[];
240+
export function registerRouteHandlersByTag<Req, Res>(tag: "media", server: ServerForMedia<Req, Res>): Route[];
233241
export function registerRouteHandlersByTag<Req, Res>(tag: Tag, server: Partial<Server<Req, Res>>): Route[] {
234242
const routes: Route[] = [];
235243

236244
switch (tag) {
237-
case null:
245+
case "pets":
238246
routes.push({
239247
method: "get",
240248
path: "/pets",
@@ -250,11 +258,15 @@ export function registerRouteHandlersByTag<Req, Res>(tag: Tag, server: Partial<S
250258
path: "/pet/{petId}",
251259
handler: server.updatePetWithForm as Route["handler"],
252260
});
261+
break;
262+
case null:
253263
routes.push({
254264
method: "post",
255265
path: "/pet/{petId}/mixed-content-types",
256266
handler: server.mixedContentTypes as Route["handler"],
257267
});
268+
break;
269+
case "media":
258270
routes.push({
259271
method: "get",
260272
path: "/pet/{petId}/image",

examples/tags/openapi.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ servers:
77
paths:
88
/pets:
99
get:
10+
tags:
11+
- pets
1012
operationId: listPets
1113
summary: Returns all pets from the system that the user has access to
1214
responses:
@@ -29,6 +31,8 @@ paths:
2931
$ref: "#/components/schemas/ErrorResponse"
3032
/pet/{petId}:
3133
get:
34+
tags:
35+
- pets
3236
operationId: getPetById
3337
parameters:
3438
- name: petId
@@ -55,6 +59,8 @@ paths:
5559
schema:
5660
$ref: "#/components/schemas/ErrorResponse"
5761
post:
62+
tags:
63+
- pets
5864
operationId: updatePetWithForm
5965
parameters:
6066
- name: petId
@@ -151,6 +157,8 @@ paths:
151157
$ref: "#/components/schemas/ErrorResponse"
152158
/pet/{petId}/image:
153159
get:
160+
tags:
161+
- media
154162
operationId: getPetImage
155163
parameters:
156164
- name: petId
@@ -170,6 +178,8 @@ paths:
170178
format: binary
171179
/pet/{petId}/webpage:
172180
get:
181+
tags:
182+
- media
173183
operationId: getPetWebpage
174184
parameters:
175185
- name: petId

examples/tags/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
"supertest": "^7.1.4",
2222
"xml-js": "^1.6.11"
2323
}
24-
}
24+
}

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)