refactor(core): enhance HttpKernel pipeline and matcher system with full context and error handling
BREAKING CHANGE: `parseQuery` utility removed; `IRouteMatcher` now includes query parsing; `RouteBuilder.middleware` and `handle` are now strictly typed per builder instance. - Add `isHandler` and `isMiddleware` runtime type guards for validation in `HttpKernel`. - Introduce `createEmptyContext` for constructing default context objects. - Support custom HTTP error handlers (`404`, `500`) via `IHttpKernelConfig.httpErrorHandlers`. - Default error handlers return meaningful HTTP status text (e.g., "Not Found"). - Replace legacy `parseQuery` logic with integrated query extraction via `createRouteMatcher`. - Strongly type `RouteBuilder.middleware()` and `.handle()` methods without generic overrides. - Simplify `HttpKernel.handle()` and `executePipeline()` through precise control flow and validation. - Remove deprecated `registerRoute.ts` and `HttpKernelConfig.ts` in favor of colocated type exports. - Add tests for integrated query parsing in `createRouteMatcher`. - Improve error handling tests: middleware/handler validation, double `next()` call, thrown exceptions. - Replace `assertRejects` with plain response code checks (via updated error handling). - Removed `parseQuery.ts` and all related tests — query parsing is now built into route matching. - `IRouteMatcher` signature changed to return `{ params, query }` instead of only `params`. - `HttpKernelConfig` now uses `DeepPartial` and includes `httpErrorHandlers`. - `RouteBuilder`'s generics are simplified for better DX and improved type safety. This refactor improves clarity, test coverage, and runtime safety of the request lifecycle while reducing boilerplate and eliminating duplicated query handling logic. Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
@@ -1,61 +1,52 @@
|
||||
import { IRouteDefinition, IRouteMatcher } from '../Interfaces/mod.ts';
|
||||
// createRouteMatcher.ts
|
||||
|
||||
import {
|
||||
IRouteDefinition,
|
||||
IRouteMatch,
|
||||
IRouteMatcher,
|
||||
isDynamicRouteDefinition,
|
||||
} from '../Interfaces/mod.ts';
|
||||
import { Params, Query } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* Creates a matcher function from a given route definition.
|
||||
* Transforms a route definition into a matcher using Deno's URLPattern API.
|
||||
*
|
||||
* This utility supports both static path-based route definitions (e.g. `/users/:id`)
|
||||
* and custom matcher functions for dynamic routing scenarios.
|
||||
*
|
||||
* ### Static Path Example
|
||||
* For a definition like:
|
||||
* ```ts
|
||||
* { method: "GET", path: "/users/:id" }
|
||||
* ```
|
||||
* the returned matcher function will:
|
||||
* - match requests to `/users/123`
|
||||
* - extract `{ id: "123" }` as `params`
|
||||
*
|
||||
* ### Dynamic Matcher Example
|
||||
* If the `IRouteDefinition` includes a `matcher` function, it will be used as-is.
|
||||
*
|
||||
* @param def - The route definition to convert into a matcher function.
|
||||
* Can be static (`path`) or dynamic (`matcher`).
|
||||
*
|
||||
* @returns A matcher function that receives a `URL` and `Request` and returns:
|
||||
* - `{ params: Record<string, string> }` if the route matches
|
||||
* - `null` if the route does not match the request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const matcher = createRouteMatcher({ method: "GET", path: "/repo/:owner/:name" });
|
||||
* const result = matcher(new URL("http://localhost/repo/foo/bar"), req);
|
||||
* // result: { params: { owner: "foo", name: "bar" } }
|
||||
* ```
|
||||
* @param def - Static path pattern or custom matcher.
|
||||
* @returns IRouteMatcher that returns `{ params, query }` or `null`.
|
||||
*/
|
||||
export function createRouteMatcher(
|
||||
def: IRouteDefinition,
|
||||
): IRouteMatcher {
|
||||
if ('matcher' in def) {
|
||||
// 1. Allow users to provide their own matcher
|
||||
if (isDynamicRouteDefinition(def)) {
|
||||
return def.matcher;
|
||||
} else {
|
||||
const pattern = def.path;
|
||||
const keys: string[] = [];
|
||||
const regex = new RegExp(
|
||||
'^' +
|
||||
pattern.replace(/:[^\/]+/g, (m) => {
|
||||
keys.push(m.substring(1));
|
||||
return '([^/]+)';
|
||||
}) +
|
||||
'$',
|
||||
);
|
||||
return (url: URL) => {
|
||||
const match = url.pathname.match(regex);
|
||||
if (!match) return null;
|
||||
const params: Record<string, string> = {};
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
params[keys[i]] = decodeURIComponent(match[i + 1]);
|
||||
}
|
||||
return { params };
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Build URLPattern; supports :id, *wildcards, regex groups, etc.
|
||||
const pattern = new URLPattern({ pathname: def.path });
|
||||
|
||||
// 3. The actual matcher closure
|
||||
return (url: URL): IRouteMatch | null => {
|
||||
const result = pattern.exec(url);
|
||||
|
||||
// 3a. Path did not match
|
||||
if (!result) return null;
|
||||
|
||||
// 3b. Extract route params
|
||||
const params: Params = {};
|
||||
for (const [key, value] of Object.entries(result.pathname.groups)) {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
// 3c. Extract query parameters – keep duplicates as arrays
|
||||
const query: Query = {};
|
||||
for (const key of url.searchParams.keys()) {
|
||||
const values = url.searchParams.getAll(key); // → string[]
|
||||
query[key] = values.length === 1
|
||||
? values[0] // single → "foo"
|
||||
: values; // multi → ["foo","bar"]
|
||||
}
|
||||
|
||||
return { params, query };
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user