Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## [0.14.0] - 2025-02-04

## Changed

- multiple paths can now be registered on the same HTTP verb, and if the paths
are exactly the same, or if the "static" and the "parameter" portions of the
paths somehow overlap, then the handler is invoked **once for each registered
path**; for example if we register `@Get("/foo/:bar")` and `@Get("/foo/bar")`
(in that order) on the same handler function, then for every request to
`/foo/bar`, that handler is invoked **twice**, the first time having no
"param" at all, and the 2nd time having the param `bar` with the value
`"bar"`. This behavior follows TC39 decorator specs where all decorators to a
function are applied "from inside out" aka: the last declared decorator gets
applied first, and so on

## Added

- `ctx.state._oakRoutingCtrl_regPath` is available as a pointer to the
registered path that matches the URL request currently being handled; this is
helpful in rare situations where multiple overlapping paths are registered on
the same handler function, causing it to be invoked multiple times, and so we
may benefit from a mechanism to control when to write to the response body (as
this operation can only be done once)

## [0.13.0] - 2025-02-01

### Added
Expand Down
9 changes: 5 additions & 4 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dklab/oak-routing-ctrl",
"version": "0.13.0",
"version": "0.14.0",
"exports": {
".": "./mod.ts",
"./mod": "./mod.ts"
Expand All @@ -15,8 +15,8 @@
},
"tasks": {
"pretty": "deno lint --ignore=docs && deno check . && deno fmt",
"test": "deno test -RE",
"check-doc": "deno check --doc .",
"test": "deno test -RE -I=jspm.dev,jsr.io,deno.land -N=0.0.0.0,127.0.0.1",
"check-doc": "deno check -I=jspm.dev,jsr.io,deno.land --doc .",
"doc": "deno doc --html mod.ts"
},
"imports": {
Expand All @@ -26,7 +26,8 @@
"@std/io": "jsr:@std/io@^0.225.2",
"@std/path": "jsr:@std/path@^1.0.8",
"@std/testing": "jsr:@std/testing@^1.0.9",
"zod": "npm:zod@^3.24.1"
"zod": "npm:zod@^3.24.1",
"superoak": "https://deno.land/x/[email protected]/mod.ts"
},
"fmt": {
"useTabs": false,
Expand Down
96 changes: 96 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ export {
z,
type zInfer,
} from "./src/utils/schema_utils.ts";

export type {
/**
* re-exporting from oak for convenient uses
* @ignore
*/
Context,
} from "@oak/oak";
18 changes: 14 additions & 4 deletions src/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,29 @@ type ClassDecorator = (
*/
export const Controller =
(pathPrefix: string = ""): ClassDecorator => (target, context): void => {
const ctrlClassName = target.name;
debug(
`invoking ControllerDecorator for ${target.name} -`,
`invoking ControllerDecorator for ${ctrlClassName} -`,
"runtime provides context:",
context,
);
const fnNames: string[] = Object.getOwnPropertyNames(target.prototype);
for (const fnName of fnNames) {
const pair = store.get(fnName);
if (!pair) continue;
pair.forEach((path, verb, p) => {
const patchedPair = new Map();
pair.forEach((verb, path) => {
const fullPath = join(pathPrefix, path);
p.set(verb, fullPath);
patchOasPath(fnName, verb, fullPath);
patchedPair.set(fullPath, verb);
debug(
`[${ctrlClassName}] @Controller: patched [${verb}] ${path} to ${fullPath}`,
);
// @TODO consider throwing if we discover 2 (or more) Controllers
// sharing the exact same set of path, fnName, and method
patchOasPath(ctrlClassName, fnName, verb, fullPath);
});
store.delete(fnName);
const fqFnName = `${ctrlClassName}.${fnName}`;
store.set(fqFnName, patchedPair);
}
};
2 changes: 1 addition & 1 deletion src/Delete_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Delete decorator", () => {
assertSpyCall(Delete, 0, { args: ["/bar"] });
assertInstanceOf(Delete.calls[0].returned, Function);
assertSpyCalls(Delete, 1);
assertEquals(store.get("doSomething")?.get("delete"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "delete");
});
2 changes: 1 addition & 1 deletion src/Get_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Get decorator", () => {
assertSpyCall(Get, 0, { args: ["/bar"] });
assertInstanceOf(Get.calls[0].returned, Function);
assertSpyCalls(Get, 1);
assertEquals(store.get("doSomething")?.get("get"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "get");
});
2 changes: 1 addition & 1 deletion src/Head_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Head decorator", () => {
assertSpyCall(Head, 0, { args: ["/bar"] });
assertInstanceOf(Head.calls[0].returned, Function);
assertSpyCalls(Head, 1);
assertEquals(store.get("doSomething")?.get("head"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "head");
});
2 changes: 1 addition & 1 deletion src/Options_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Options decorator", () => {
assertSpyCall(Options, 0, { args: ["/bar"] });
assertInstanceOf(Options.calls[0].returned, Function);
assertSpyCalls(Options, 1);
assertEquals(store.get("doSomething")?.get("options"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "options");
});
2 changes: 1 addition & 1 deletion src/Patch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Patch decorator", () => {
assertSpyCall(Patch, 0, { args: ["/bar"] });
assertInstanceOf(Patch.calls[0].returned, Function);
assertSpyCalls(Patch, 1);
assertEquals(store.get("doSomething")?.get("patch"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "patch");
});
2 changes: 1 addition & 1 deletion src/Post_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Deno.test("@Post decorator", () => {
assertSpyCall(Post, 0, { args: ["/bar"] });
assertInstanceOf(Post.calls[0].returned, Function);
assertSpyCalls(Post, 1);
assertEquals(store.get("doSomething")?.get("post"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "post");
});
2 changes: 1 addition & 1 deletion src/Put_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Deno.test("@Put decorator", () => {
assertSpyCall(Put, 0, { args: ["/bar"] });
assertInstanceOf(Put.calls[0].returned, Function);
assertSpyCalls(Put, 1);
assertEquals(store.get("doSomething")?.get("put"), "/bar");
assertEquals(store.get("doSomething")?.get("/bar"), "put");

// assertSpyCalls(spyLoggerDebug, 1);
// assertSpyCalls(spyStoreRegister, 1);
Expand Down
11 changes: 8 additions & 3 deletions src/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type SupportedVerb =
| "head"
| "options";

export const store: Map<string, Map<SupportedVerb, string>> = new Map();
export const store: Map<string, Map<string, SupportedVerb>> = new Map();

/**
* internal library helper method, used to keep track of the declared
Expand All @@ -21,8 +21,13 @@ export const register = (
const normalizedVerb: SupportedVerb = verb.toLowerCase() as SupportedVerb;
const existingPair = store.get(fnName);
if (existingPair) {
existingPair.set(normalizedVerb, path);
// @NOTE that we intentionally allow multiple paths registered on the
// same verb e.g.
// - @Get('/foo') AND @Get('/foo/bar')
// - @Get('/foo') AND @Get('/bar')
// - @Get('/foo/:bar') AND @Get('/foo/bar')
existingPair.set(path, normalizedVerb);
} else {
store.set(fnName, new Map([[normalizedVerb, path]]));
store.set(fnName, new Map([[path, normalizedVerb]]));
}
};
10 changes: 5 additions & 5 deletions src/Store_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ Deno.test("Store", () => {
assertSpyCall(spyStoreGet, 3, { args: ["handlerFnName"] });
assertSpyCall(spyStoreGet, 4, { args: ["handlerFnName"] });
const finalMap = new Map([
["get", "/foo"],
["post", "/bar"],
["put", "/baz"],
["delete", "/maz"],
["patch", "/laz"],
["/foo", "get"],
["/bar", "post"],
["/baz", "put"],
["/maz", "delete"],
["/laz", "patch"],
]);
assertEquals(store.get("handlerFnName"), finalMap);

Expand Down
Loading