First commit

Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
2025-05-07 10:53:56 +02:00
commit 1d2e89feca
20 changed files with 919 additions and 0 deletions

121
src/HttpKernel.ts Normal file
View File

@@ -0,0 +1,121 @@
import {
IContext,
IHandler,
IHttpKernel,
IInternalRoute,
IMiddleware,
IRouteBuilder,
IRouteBuilderFactory,
IRouteDefinition,
ResponseDecorator,
} from './Interfaces/mod.ts';
import { RouteBuilder } from './RouteBuilder.ts';
/**
* The central HTTP kernel responsible for managing route definitions,
* executing middleware chains, and dispatching HTTP requests to their handlers.
*
* This class supports a fluent API for route registration and allows the injection
* of custom response decorators and route builder factories for maximum flexibility and testability.
*/
export class HttpKernel implements IHttpKernel {
/**
* The list of internally registered routes, each with method, matcher, middleware, and handler.
*/
private routes: IInternalRoute[] = [];
/**
* Creates a new instance of the `HttpKernel`.
*
* @param decorateResponse - An optional response decorator function that is applied to all responses
* after the middleware/handler pipeline. Defaults to identity (no modification).
* @param routeBuilderFactory - Optional factory for creating route builders. Defaults to using `RouteBuilder`.
*/
public constructor(
private readonly decorateResponse: ResponseDecorator = (res) => res,
private readonly routeBuilderFactory: IRouteBuilderFactory =
RouteBuilder,
) {}
/**
* @inheritdoc
*/
public route(definition: IRouteDefinition): IRouteBuilder {
return new this.routeBuilderFactory(
this.registerRoute.bind(this),
definition,
);
}
/**
* @inheritdoc
*/
public handle = async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const method = request.method.toUpperCase();
for (const route of this.routes) {
if (route.method !== method) continue;
const match = route.matcher(url, request);
if (match) {
const ctx: IContext = {
req: request,
params: match.params,
state: {},
};
return await this.executePipeline(
ctx,
route.middlewares,
route.handler,
);
}
}
return new Response('Not Found', { status: 404 });
};
/**
* Registers a finalized route by pushing it into the internal route list.
*
* This method is typically called by the route builder after `.handle()` is invoked.
*
* @param route - The fully constructed route including matcher, middlewares, and handler.
*/
private registerRoute(route: IInternalRoute): void {
this.routes.push(route);
}
/**
* Executes the middleware pipeline and final handler for a given request context.
*
* This function recursively invokes middleware in the order they were registered,
* ending with the route's final handler. If a middleware returns a response directly
* without calling `next()`, the pipeline is short-circuited.
*
* The final response is passed through the `decorateResponse` function before being returned.
*
* @param ctx - The request context containing the request, parameters, and shared state.
* @param middleware - The ordered list of middleware to apply before the handler.
* @param handler - The final request handler to invoke at the end of the pipeline.
* @returns The final HTTP response after middleware and decoration.
*/
private async executePipeline(
ctx: IContext,
middleware: IMiddleware[],
handler: IHandler,
): Promise<Response> {
let i = -1;
const dispatch = async (index: number): Promise<Response> => {
if (index <= i) throw new Error('next() called multiple times');
i = index;
const fn: IMiddleware | IHandler = index < middleware.length
? middleware[index]
: handler;
if (!fn) return new Response('Internal error', { status: 500 });
return index < middleware.length
? await fn(ctx, () => dispatch(index + 1))
: await (fn as IHandler)(ctx);
};
return this.decorateResponse(await dispatch(0));
}
}

View File

@@ -0,0 +1,31 @@
/**
* Represents the per-request context passed through the middleware pipeline and to the final handler.
*
* This context object encapsulates the original HTTP request,
* the path parameters extracted from the matched route,
* and a mutable state object for sharing information across middlewares and handlers.
*/
export interface IContext {
/**
* The original HTTP request object as received by Deno.
* Contains all standard fields like headers, method, body, etc.
*/
req: Request;
/**
* Route parameters parsed from the URL path, based on route definitions
* that include dynamic segments (e.g., `/users/:id` → `{ id: "123" }`).
*
* These parameters are considered read-only and are set by the router.
*/
params: Record<string, string>;
/**
* A shared, mutable object used to pass arbitrary data between middlewares and handlers.
*
* Use this field to attach validated user info, auth state, logging context, etc.
*
* Each key should be well-named to avoid collisions across layers.
*/
state: Record<string, unknown>;
}

View File

@@ -0,0 +1,16 @@
import { IContext } from './IContext.ts';
/**
* Represents a final request handler responsible for generating a response.
*
* The handler is the last step in the middleware pipeline and must return
* a valid HTTP `Response`. It has access to all data injected into the
* request context, including path parameters and any state added by middleware.
*/
export interface IHandler {
/**
* @param ctx - The complete request context, including parameters and middleware state.
* @returns A promise resolving to an HTTP `Response`.
*/
(ctx: IContext): Promise<Response>;
}

View File

@@ -0,0 +1,33 @@
import { IRouteBuilder } from './IRouteBuilder.ts';
import { IRouteDefinition } from './IRouteDefinition.ts';
/**
* Defines the core interface for the HTTP kernel, responsible for route registration,
* middleware orchestration, and request dispatching.
*/
export interface IHttpKernel {
/**
* Registers a new route with a static path pattern or a dynamic matcher.
*
* This method accepts both conventional route definitions (with path templates)
* and advanced matcher-based routes for flexible URL structures.
*
* Returns a route builder that allows chaining middleware and assigning a handler.
*
* @param definition - A static or dynamic route definition, including the HTTP method
* and either a path pattern or custom matcher function.
* @returns A builder interface to attach middleware and define the handler.
*/
route(definition: IRouteDefinition): IRouteBuilder;
/**
* Handles an incoming HTTP request by matching it against registered routes,
* executing any associated middleware in order, and invoking the final route handler.
*
* This method serves as the main entry point to integrate with `Deno.serve`.
*
* @param request - The incoming HTTP request object.
* @returns A promise resolving to the final HTTP response.
*/
handle(request: Request): Promise<Response>;
}

View File

@@ -0,0 +1,42 @@
import { IHandler } from './IHandler.ts';
import { IMiddleware } from './IMiddleware.ts';
/**
* Represents an internally registered route within the HttpKernel.
*
* Contains all data required to match an incoming request and dispatch it
* through the associated middleware chain and final handler.
*/
export interface IInternalRoute {
/**
* The HTTP method (e.g. 'GET', 'POST') that this route responds to.
* The method should always be in uppercase.
*/
method: string;
/**
* A matcher function used to determine whether this route matches a given request.
*
* If the matcher returns `null`, the route does not apply to the request.
* If it returns a params object, the route is considered matched and the extracted
* parameters are passed into the request context.
*
* @param url - The parsed URL object from the incoming request.
* @param req - The original Request object.
* @returns An object with extracted path parameters, or `null` if not matched.
*/
matcher: (
url: URL,
req: Request,
) => null | { params: Record<string, string> };
/**
* An ordered list of middleware functions to be executed before the handler.
*/
middlewares: IMiddleware[];
/**
* The final handler that generates the HTTP response after all middleware has run.
*/
handler: IHandler;
}

View File

@@ -0,0 +1,20 @@
import { IContext } from './IContext.ts';
/**
* Represents a middleware function in the HTTP request pipeline.
*
* Middleware can perform tasks such as logging, authentication, validation,
* or response transformation. It receives the current request context and
* a `next()` function to delegate control to the next middleware or final handler.
*
* To stop the request pipeline, a middleware can return a `Response` directly
* without calling `next()`.
*/
export interface IMiddleware {
/**
* @param ctx - The request context, containing the request, path parameters, and shared state.
* @param next - A function that continues the middleware pipeline. Returns the final `Response`.
* @returns A promise resolving to an HTTP `Response`.
*/
(ctx: IContext, next: () => Promise<Response>): Promise<Response>;
}

View File

@@ -0,0 +1,35 @@
import { IHandler } from './IHandler.ts';
import { IInternalRoute } from './IInternalRoute.ts';
import { IMiddleware } from './IMiddleware.ts';
import { IRouteDefinition } from './IRouteDefinition.ts';
export interface IRouteBuilderFactory {
new (
registerRoute: (route: IInternalRoute) => void,
def: IRouteDefinition,
mws?: IMiddleware[],
): IRouteBuilder;
}
/**
* Provides a fluent API to build a single route configuration by chaining
* middleware and setting the final request handler.
*/
export interface IRouteBuilder {
/**
* Adds a middleware to the current route.
* Middleware will be executed in the order of registration.
*
* @param mw - A middleware function.
* @returns The route builder for further chaining.
*/
middleware(mw: IMiddleware): IRouteBuilder;
/**
* Sets the final request handler for the route.
* Calling this finalizes the route and registers it in the kernel.
*
* @param handler - The function to execute when this route is matched.
*/
handle(handler: IHandler): void;
}

View File

@@ -0,0 +1,46 @@
import { IRouteMatcher } from './IRouteMatcher.ts';
/**
* Defines a static route using a path pattern with optional parameters.
*
* Suitable for conventional routes like "/users/:id", which can be parsed
* into named parameters using a path-matching library.
*/
export interface IStaticRouteDefinition {
/**
* The HTTP method this route should match (e.g. "GET", "POST").
*/
method: string;
/**
* A static path pattern for the route, which may include named parameters
* (e.g. "/caches/:id"). Internally, this can be converted to a regex matcher.
*/
path: string;
}
/**
* Defines a dynamic route using a custom matcher function instead of a static path.
*
* Useful for complex URL structures that cannot easily be expressed using a static pattern,
* such as routes with variable prefixes or conditional segment logic.
*/
export interface IDynamicRouteDefinition {
/**
* The HTTP method this route should match (e.g. "GET", "POST").
*/
method: string;
/**
* A custom matcher function that receives the parsed URL and raw request.
* If the function returns `null`, the route does not match.
* If the function returns a params object, the route is considered matched.
*/
matcher: IRouteMatcher;
}
/**
* A route definition can either be a conventional static route with a path pattern,
* or a dynamic route with a custom matcher function for advanced matching logic.
*/
export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition;

View File

@@ -0,0 +1,34 @@
import { IRouteDefinition } from './IRouteDefinition.ts';
/**
* Defines a route matcher function that evaluates whether a route applies to a given request.
*
* If the route matches, the matcher returns an object containing extracted route parameters.
* Otherwise, it returns `null`.
*/
export interface IRouteMatcher {
/**
* Evaluates whether the given URL and request match a defined route.
*
* @param url - The full URL of the incoming request.
* @param req - The raw Request object (may be used for context or headers).
* @returns An object containing path parameters if matched, or `null` if not matched.
*/
(url: URL, req: Request): null | { params: Record<string, string> };
}
/**
* Represents a factory for creating route matcher functions from route definitions.
*
* This allows the matcher logic to be injected or replaced (e.g. for testing,
* pattern libraries, or advanced routing scenarios).
*/
export interface IRouteMatcherFactory {
/**
* Creates a matcher function based on a given route definition.
*
* @param def - The route definition (static or dynamic).
* @returns A matcher function that checks if a request matches and extracts parameters.
*/
(def: IRouteDefinition): IRouteMatcher;
}

View File

@@ -0,0 +1,25 @@
/**
* A function that modifies or enriches an outgoing HTTP response before it is returned to the client.
*
* This decorator can be used to inject headers (e.g., CORS, security), apply global transformations,
* or wrap responses for logging, analytics, or debugging purposes.
*
* It is called exactly once at the end of the middleware/handler pipeline,
* allowing central response customization without interfering with business logic.
*
* @param res - The original `Response` object produced by the route handler or middleware chain.
* @returns A modified or wrapped `Response` object to be sent back to the client.
*
* @example
* ```ts
* const addCors: ResponseDecorator = (res) => {
* const headers = new Headers(res.headers);
* headers.set("Access-Control-Allow-Origin", "*");
* return new Response(res.body, {
* status: res.status,
* headers,
* });
* };
* ```
*/
export type ResponseDecorator = (res: Response) => Response;

13
src/Interfaces/mod.ts Normal file
View File

@@ -0,0 +1,13 @@
export type { IContext } from './IContext.ts';
export type { IMiddleware } from './IMiddleware.ts';
export type { IHandler } from './IHandler.ts';
export type { IHttpKernel } from './IHttpKernel.ts';
export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
export type {
IDynamicRouteDefinition,
IRouteDefinition,
IStaticRouteDefinition,
} from './IRouteDefinition.ts';
export type { IInternalRoute } from './IInternalRoute.ts';
export type { IRouteMatcher } from './IRouteMatcher.ts';
export type { ResponseDecorator } from './ResponseDecorator.ts';

66
src/RouteBuilder.ts Normal file
View File

@@ -0,0 +1,66 @@
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import {
IHandler,
IInternalRoute,
IMiddleware,
IRouteBuilder,
IRouteDefinition,
} from './Interfaces/mod.ts';
import { createRouteMatcher } from './Utils.ts';
/**
* Provides a fluent builder interface for defining a single route,
* including HTTP method, path or matcher, middleware chain and final handler.
*
* This builder is stateless and immutable; each chained call returns a new instance.
*/
export class RouteBuilder implements IRouteBuilder {
/**
* Constructs a new instance of the route builder.
*
* @param registerRoute - A delegate used to register the finalized route definition.
* @param def - The route definition (static path or dynamic matcher).
* @param mws - The list of middleware functions collected so far (default: empty).
*/
constructor(
private readonly registerRoute: (route: IInternalRoute) => void,
private readonly def: IRouteDefinition,
private readonly mws: IMiddleware[] = [],
private readonly matcherFactory: IRouteMatcherFactory =
createRouteMatcher,
) {}
/**
* Adds a middleware function to the current route definition.
*
* Middleware is executed in the order it is added.
* Returns a new builder instance with the additional middleware appended.
*
* @param mw - A middleware function to be executed before the handler.
* @returns A new `RouteBuilder` instance for continued chaining.
*/
middleware(mw: IMiddleware): IRouteBuilder {
return new RouteBuilder(this.registerRoute, this.def, [
...this.mws,
mw,
]);
}
/**
* Finalizes the route by assigning the handler and registering the route.
*
* Internally constructs a matcher function from the route definition
* and passes all route data to the registration delegate.
*
* @param handler - The final request handler for this route.
*/
handle(handler: IHandler): void {
const matcher = this.matcherFactory(this.def);
this.registerRoute({
method: this.def.method.toUpperCase(),
matcher,
middlewares: this.mws,
handler,
});
}
}

61
src/Utils.ts Normal file
View File

@@ -0,0 +1,61 @@
import { IRouteDefinition, IRouteMatcher } from './Interfaces/mod.ts';
/**
* Creates a matcher function from a given route definition.
*
* 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" } }
* ```
*/
export function createRouteMatcher(
def: IRouteDefinition,
): IRouteMatcher {
if ('matcher' in 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 };
};
}
}

View File

@@ -0,0 +1,161 @@
import {
assertEquals,
assertRejects,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
Deno.test('HttpKernel: matches static route and executes handler', async () => {
const kernel = new HttpKernel();
const def: IRouteDefinition = { method: 'GET', path: '/hello' };
let called = false;
kernel.route(def).handle(() => {
called = true;
return Promise.resolve(new Response('OK', { status: 200 }));
});
const res = await kernel.handle(
new Request('http://localhost/hello', { method: 'GET' }),
);
assertEquals(res.status, 200);
assertEquals(await res.text(), 'OK');
assertEquals(called, true);
});
Deno.test('HttpKernel: supports dynamic matcher', async () => {
const kernel = new HttpKernel();
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null,
};
kernel.route(def).handle(() =>
Promise.resolve(new Response('Dyn', { status: 200 }))
);
const res = await kernel.handle(new Request('http://localhost/dyn'));
assertEquals(res.status, 200);
assertEquals(await res.text(), 'Dyn');
});
Deno.test('HttpKernel: calls middleware in order and passes to handler', async () => {
const kernel = new HttpKernel();
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/test' })
.middleware(async (ctx, next) => {
calls.push('mw1');
return await next();
})
.middleware(async (ctx, next) => {
calls.push('mw2');
return await next();
})
.handle(() => {
calls.push('handler');
return Promise.resolve(new Response('done'));
});
const res = await kernel.handle(
new Request('http://localhost/test', { method: 'GET' }),
);
assertEquals(await res.text(), 'done');
assertEquals(calls, ['mw1', 'mw2', 'handler']);
});
Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
const kernel = new HttpKernel();
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/stop' })
.middleware(() => {
calls.push('mw1');
return Promise.resolve(new Response('blocked', { status: 403 }));
})
.middleware(() => {
calls.push('mw2');
return Promise.resolve(new Response('should-not-call'));
})
.handle(() => {
calls.push('handler');
return Promise.resolve(new Response('ok'));
});
const res = await kernel.handle(
new Request('http://localhost/stop', { method: 'GET' }),
);
assertEquals(res.status, 403);
assertEquals(await res.text(), 'blocked');
assertEquals(calls, ['mw1']);
});
Deno.test('HttpKernel: 404 for unmatched route', async () => {
const kernel = new HttpKernel();
const res = await kernel.handle(new Request('http://localhost/nothing'));
assertEquals(res.status, 404);
});
Deno.test('HttpKernel: skips route with wrong method', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'POST', path: '/only-post' })
.handle(() => Promise.resolve(new Response('nope')));
const res = await kernel.handle(
new Request('http://localhost/only-post', { method: 'GET' }),
);
assertEquals(res.status, 404);
});
Deno.test('HttpKernel: throws on next() called twice', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/bad' })
.middleware(async (ctx, next) => {
await next();
await next(); // ❌
return new Response('should never reach');
})
.handle(() => Promise.resolve(new Response('OK')));
await assertRejects(
() => kernel.handle(new Request('http://localhost/bad')),
Error,
'next() called multiple times',
);
});
Deno.test('HttpKernel: handler throws → error propagates', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/throw' })
.handle(() => {
throw new Error('fail!');
});
await assertRejects(
() => kernel.handle(new Request('http://localhost/throw')),
Error,
'fail!',
);
});
Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => {
const kernel = new HttpKernel();
// Force-manual Registrierung mit `handler: undefined`
// Umgehen des Builders zur Simulation dieses Edge-Cases
kernel['routes'].push({
method: 'GET',
matcher: (url) => url.pathname === '/fail' ? { params: {} } : null,
middlewares: [],
// @ts-expect-error absichtlich ungültiger Handler
handler: undefined,
});
const res = await kernel.handle(new Request('http://localhost/fail'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal error');
});

View File

@@ -0,0 +1,111 @@
import {
assert,
assertEquals,
assertNotEquals,
assertThrows,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import {
IHandler,
IInternalRoute,
IMiddleware,
IRouteDefinition,
} from '../Interfaces/mod.ts';
import { RouteBuilder } from '../mod.ts';
// Dummy objects
const dummyHandler: IHandler = async () => new Response('ok');
const dummyMiddleware: IMiddleware = async (_, next) => await next();
const dummyDef: IRouteDefinition = { method: 'get', path: '/hello' };
const dummyMatcher = () => ({ params: {} });
Deno.test('middleware: single middleware is registered correctly', () => {
let registered: IInternalRoute | null = null as IInternalRoute | null;
const builder = new RouteBuilder((r) => registered = r, dummyDef)
.middleware(dummyMiddleware);
builder.handle(dummyHandler);
assert(registered);
assertEquals(registered?.middlewares.length, 1);
assertEquals(registered?.middlewares[0], dummyMiddleware);
});
Deno.test('middleware: middleware is chained immutably', () => {
const builder1 = new RouteBuilder(() => {}, dummyDef);
const builder2 = builder1.middleware(dummyMiddleware);
assertNotEquals(builder1, builder2);
});
Deno.test('middleware: preserves order of middleware', () => {
const mw1: IMiddleware = async (_, next) => await next();
const mw2: IMiddleware = async (_, next) => await next();
let result: IInternalRoute | null = null as IInternalRoute | null;
const builder = new RouteBuilder((r) => result = r, dummyDef)
.middleware(mw1)
.middleware(mw2);
builder.handle(dummyHandler);
assert(result);
assertEquals(result!.middlewares, [mw1, mw2]);
});
Deno.test('handle: uppercases method', () => {
let result: IInternalRoute | null = null as IInternalRoute | null;
new RouteBuilder((r) => result = r, { method: 'post', path: '/x' })
.handle(dummyHandler);
assertEquals(result?.method, 'POST');
});
Deno.test('handle: works with no middleware', async () => {
let route: IInternalRoute | null = null as IInternalRoute | null;
const builder = new RouteBuilder((r) => route = r, dummyDef);
builder.handle(dummyHandler);
assert(route);
assertEquals(route?.middlewares.length, 0);
const request = new Request('http://localhost');
const res1 = await route?.handler({ req: request, params: {}, state: {} });
const res2 = await dummyHandler({ req: request, params: {}, state: {} });
assertEquals(res1?.status, res2?.status);
assertEquals(await res1?.text(), await res2?.text());
});
Deno.test('handle: uses custom matcher factory', () => {
let called = false;
const factory = (def: IRouteDefinition) => {
called = true;
return dummyMatcher;
};
let route: IInternalRoute | null = null as IInternalRoute | null;
new RouteBuilder((r) => route = r, dummyDef, [], factory).handle(
dummyHandler,
);
assert(called);
assert(route);
assertEquals(route!.matcher, dummyMatcher);
});
Deno.test('handle: throws if matcher factory throws', () => {
const faultyFactory = () => {
throw new Error('matcher fail');
};
const builder = new RouteBuilder(() => {}, dummyDef, [], faultyFactory);
assertThrows(() => builder.handle(dummyHandler), Error, 'matcher fail');
});

View File

@@ -0,0 +1,54 @@
import {
assert,
assertEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
import { createRouteMatcher } from '../mod.ts';
// Dummy request
const dummyRequest = new Request('http://localhost');
Deno.test('createRouteMatcher: static route matches and extracts params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/users/42'), dummyRequest);
assert(result);
assertEquals(result.params, { id: '42' });
});
Deno.test('createRouteMatcher: static route with multiple params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' };
const matcher = createRouteMatcher(def);
const result = matcher(
new URL('http://localhost/repo/max/wiki'),
dummyRequest,
);
assert(result);
assertEquals(result.params, { owner: 'max', name: 'wiki' });
});
Deno.test('createRouteMatcher: static route does not match wrong path', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/posts/42'), dummyRequest);
assertStrictEquals(result, null);
});
Deno.test('createRouteMatcher: uses custom matcher if provided', () => {
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/ping' ? { params: {} } : null,
};
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/ping'), dummyRequest);
assert(result);
assertEquals(result.params, {});
});

4
src/mod.ts Normal file
View File

@@ -0,0 +1,4 @@
// deno-coverage-ignore-file
export { HttpKernel } from './HttpKernel.ts';
export { RouteBuilder } from './RouteBuilder.ts';
export { createRouteMatcher } from './Utils.ts';