Skip to content

Commit aabd25d

Browse files
committed
Controller name is now path of route identifier
1 parent 67cdfc6 commit aabd25d

File tree

5 files changed

+141
-22
lines changed

5 files changed

+141
-22
lines changed

src/Controller.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ type ClassDecorator = (
2626
*/
2727
export const Controller =
2828
(pathPrefix: string = ""): ClassDecorator => (target, context): void => {
29+
const ctrlClassName = target.name;
2930
debug(
30-
`invoking ControllerDecorator for ${target.name} -`,
31+
`invoking ControllerDecorator for ${ctrlClassName} -`,
3132
"runtime provides context:",
3233
context,
3334
);
@@ -39,8 +40,15 @@ export const Controller =
3940
pair.forEach((verb, path) => {
4041
const fullPath = join(pathPrefix, path);
4142
patchedPair.set(fullPath, verb);
42-
patchOasPath(fnName, verb, fullPath);
43+
debug(
44+
`[${ctrlClassName}] @Controller: patched [${verb}] ${path} to ${fullPath}`,
45+
);
46+
// @TODO consider throwing if we discover 2 (or more) Controllers
47+
// sharing the exact same set of path, fnName, and method
48+
patchOasPath(ctrlClassName, fnName, verb, fullPath);
4349
});
44-
store.set(fnName, patchedPair);
50+
store.delete(fnName);
51+
const fqFnName = `${ctrlClassName}.${fnName}`;
52+
store.set(fqFnName, patchedPair);
4553
}
4654
};

src/oasStore.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ type TheRouteConfig = RouteConfig & {
77
tags?: string[];
88
};
99

10-
// fnName|method|path => OasRouteConfig
10+
const TMP_CTRL_NAME = "FILLED_LATER";
11+
12+
// ctrlName|fnName|method|path => OasRouteConfig
1113
export const oasStore: Map<string, TheRouteConfig> = new Map();
1214

1315
const getRouteId = (
16+
ctrlName: string,
1417
fnName: string,
1518
method: SupportedVerb,
1619
path: string,
17-
) => `${fnName}|${method}|${path}`;
20+
) => `${ctrlName}|${fnName}|${method}|${path}`;
1821

1922
/**
2023
* input: `/some/:foo/and/:bar`
@@ -43,7 +46,7 @@ export const updateOas = (
4346
// because we don't want "documentation without consent"
4447
if (!specs) return;
4548

46-
const oasRouteIdentifier = getRouteId(fnName, method, path);
49+
const oasRouteIdentifier = getRouteId(TMP_CTRL_NAME, fnName, method, path);
4750

4851
const oasPath = getOasCompatPath(path);
4952

@@ -70,30 +73,43 @@ export const updateOas = (
7073
tags: specs?.tags,
7174
};
7275

73-
debug(`OpenApiSpec: recording for [${method}] ${path}`);
76+
debug(`[${TMP_CTRL_NAME}] OpenApiSpec: recording for [${method}] ${path}`);
7477

7578
oasStore.set(oasRouteIdentifier, updated);
7679
};
7780

81+
/**
82+
* patch the Open API Spec config, designed to be invoked in the context of the `@Controller` decorator
83+
* @param ctrlName the name of the class being decorated with `@Controller`
84+
* @param fnName the name of the function being decorated with e.g. `@Get`, `@Post`, and so on
85+
*/
7886
export const patchOasPath = (
87+
ctrlName: string,
7988
fnName: string,
8089
method: SupportedVerb,
8190
path: string,
8291
) => {
83-
oasStore.forEach((storedSpecs, routeId) => {
84-
const [storedFnName, storedMethod, storedPath] = routeId.split("|");
92+
for (const [routeId, storedSpecs] of oasStore) {
93+
const [storedCtrlName, storedFnName, storedMethod, storedPath] = routeId
94+
.split("|");
95+
8596
if (
97+
storedCtrlName === TMP_CTRL_NAME &&
8698
fnName === storedFnName &&
8799
method === storedMethod &&
88-
path.length > storedPath.length &&
100+
path.length >= storedPath.length &&
89101
path.endsWith(storedPath)
90102
) {
91-
debug(`OpenApiSpec: patching ${storedSpecs.path} to ${path}`);
92103
storedSpecs.path = getOasCompatPath(path);
93-
// @TODO consider throwing if we discover 2 (or more) Controllers
94-
// sharing the exact same set of path, fnName, and method
104+
const newRouteId = getRouteId(ctrlName, fnName, method, path);
105+
oasStore.delete(routeId);
106+
oasStore.set(newRouteId, storedSpecs);
107+
debug(
108+
`[${ctrlName}] OpenApiSpec: patched [${method}] ${storedPath} to ${path}`,
109+
);
110+
break;
95111
}
96-
});
112+
}
97113
};
98114

99115
export const _internal = {

src/oasStore_test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Deno.test("no-op", () => {
1212
});
1313

1414
Deno.test("store entry creation & update", () => {
15+
const ctrlName = "TestCtrl";
1516
const fnName = "doSomething";
1617
const method = "post";
1718
const path = "/hello/:name";
@@ -40,16 +41,18 @@ Deno.test("store entry creation & update", () => {
4041
},
4142
});
4243

43-
const record = oasStore.get(getRouteId(fnName, method, path));
44+
const record = oasStore.get(getRouteId("FILLED_LATER", fnName, method, path));
4445
assertEquals(record?.method, method);
4546
assertEquals(record?.path, getOasCompatPath(path));
4647
assertInstanceOf(
4748
record?.request?.body?.content?.["application/json"]?.schema,
4849
ZodObject,
4950
);
5051

51-
patchOasPath(fnName, method, patchedPath);
52-
const patchedRecord = oasStore.get(getRouteId(fnName, method, path));
52+
patchOasPath(ctrlName, fnName, method, patchedPath);
53+
const patchedRecord = oasStore.get(
54+
getRouteId(ctrlName, fnName, method, patchedPath),
55+
);
5356
assertEquals(patchedRecord?.method, method);
5457
assertEquals(patchedRecord?.path, getOasCompatPath(patchedPath));
5558
assertInstanceOf(

src/useOakServer.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const useOakServer = (
1919
const ctrlProps: string[] = Object.getOwnPropertyNames(Ctrl.prototype);
2020
for (const propName of ctrlProps) {
2121
if (propName === "constructor") continue;
22-
const pair = store.get(propName);
22+
const fqFnName = `${Ctrl.name}.${propName}`;
23+
const pair = store.get(fqFnName);
2324
if (!pair) continue;
2425
for (const [path, verb] of pair) {
2526
oakRouter[verb](
@@ -30,7 +31,9 @@ export const useOakServer = (
3031
// to the currently registered path every time the handler is
3132
// invoked per match
3233
ctx.state._oakRoutingCtrl_regPath = path;
33-
debug(`handling literally-registered path ${path}`);
34+
debug(
35+
`handling literally-registered path ${path} with ${fqFnName}`,
36+
);
3437

3538
const handler = Object.getOwnPropertyDescriptor(
3639
Ctrl.prototype,
@@ -52,7 +55,7 @@ export const useOakServer = (
5255
await next();
5356
},
5457
);
55-
debug(`mapping route [${verb}] ${path} -> ${propName}`);
58+
debug(`mapping route [${verb}] ${path} -> ${fqFnName}`);
5659
}
5760
}
5861
}

src/useOakServer_multipaths_test.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Controller,
1313
ControllerMethodArgs,
1414
Get,
15+
Patch,
1516
Post,
1617
Put,
1718
useOakServer,
@@ -23,11 +24,12 @@ const spyTemplate = {
2324
handler1(..._: unknown[]) {},
2425
handler2(..._: unknown[]) {},
2526
handler3(..._: unknown[]) {},
27+
handler4(..._: unknown[]) {},
2628
catchAll(..._: unknown[]) {},
2729
};
2830

2931
@Controller("/test")
30-
class TestController {
32+
class TestController1 {
3133
@Get("/handler1/:bar")
3234
@Get("/handler1/bar")
3335
@Post("/handler1/par")
@@ -70,10 +72,41 @@ class TestController {
7072
spyTemplate.handler3(method, reqPath, regPath, { ...param });
7173
return { method, reqPath, regPath, param, body };
7274
}
75+
76+
@Patch("/handler4")
77+
@ControllerMethodArgs("body", "param")
78+
handler4(
79+
body: Record<string, unknown>,
80+
param: Record<string, unknown>,
81+
ctx: Context,
82+
) {
83+
const method = ctx.request.method;
84+
const reqPath = ctx.request.url.pathname;
85+
const regPath = ctx.state._oakRoutingCtrl_regPath;
86+
spyTemplate.handler4(method, reqPath, regPath, { ...param });
87+
return { method, reqPath, regPath, param, body };
88+
}
89+
}
90+
91+
@Controller("/test")
92+
class TestController2 {
93+
@Patch("/handler4/:bar")
94+
@ControllerMethodArgs("body", "param")
95+
handler4(
96+
body: Record<string, unknown>,
97+
param: Record<string, unknown>,
98+
ctx: Context,
99+
) {
100+
const method = ctx.request.method;
101+
const reqPath = ctx.request.url.pathname;
102+
const regPath = ctx.state._oakRoutingCtrl_regPath;
103+
spyTemplate.handler4(method, reqPath, regPath, { ...param });
104+
return { method, reqPath, regPath, param, body };
105+
}
73106
}
74107

75108
const app = new Application();
76-
useOakServer(app, [TestController]);
109+
useOakServer(app, [TestController1, TestController2]);
77110
app.use((ctx) => {
78111
spyTemplate.catchAll(
79112
"catch-all middleware invoked for",
@@ -282,3 +315,59 @@ Deno.test("Similar paths that do not actually overlap - path 2", async () => {
282315
body: { charlie: true },
283316
});
284317
});
318+
319+
Deno.test("[PATCH] /test/handler4", async () => {
320+
const handler4Spy = spy(spyTemplate, "handler4");
321+
const catchAllSpy = spy(spyTemplate, "catchAll");
322+
323+
const req = await superoak(app);
324+
const res = await req.patch("/test/handler4");
325+
326+
assertSpyCalls(handler4Spy, 1);
327+
assertSpyCallArgs(handler4Spy, 0, [
328+
"PATCH",
329+
"/test/handler4",
330+
"/test/handler4",
331+
{},
332+
]);
333+
assertSpyCalls(catchAllSpy, 1);
334+
335+
handler4Spy.restore();
336+
catchAllSpy.restore();
337+
338+
assertEquals(res.body, {
339+
method: "PATCH",
340+
reqPath: "/test/handler4",
341+
regPath: "/test/handler4",
342+
param: {},
343+
body: {},
344+
});
345+
});
346+
347+
Deno.test("[PATCH] /test/handler4/:bar", async () => {
348+
const handler4Spy = spy(spyTemplate, "handler4");
349+
const catchAllSpy = spy(spyTemplate, "catchAll");
350+
351+
const req = await superoak(app);
352+
const res = await req.patch("/test/handler4/bob").send({ bob: true });
353+
354+
assertSpyCalls(handler4Spy, 1);
355+
assertSpyCallArgs(handler4Spy, 0, [
356+
"PATCH",
357+
"/test/handler4/bob",
358+
"/test/handler4/:bar",
359+
{ bar: "bob" },
360+
]);
361+
assertSpyCalls(catchAllSpy, 1);
362+
363+
handler4Spy.restore();
364+
catchAllSpy.restore();
365+
366+
assertEquals(res.body, {
367+
method: "PATCH",
368+
reqPath: "/test/handler4/bob",
369+
regPath: "/test/handler4/:bar",
370+
param: { bar: "bob" },
371+
body: { bob: true },
372+
});
373+
});

0 commit comments

Comments
 (0)