@bronti/wooter 0.0.0-stage → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOC.md +289 -0
- package/LICENSE +7 -0
- package/README.md +23 -2
- package/package.json +13 -4
- package/src/Graph.ts +231 -0
- package/src/MiddlewareContext.ts +216 -0
- package/src/RouteContext.ts +312 -0
- package/src/Wooter.ts +318 -0
- package/src/WooterError.ts +26 -0
- package/src/export/error.ts +17 -0
- package/src/export/mod.ts +10 -0
- package/src/export/option.ts +8 -0
- package/src/export/path.ts +13 -0
- package/src/export/response.ts +39 -0
- package/src/export/result.ts +14 -0
- package/src/export/types.ts +1 -0
- package/src/export/use.ts +68 -0
- package/src/path/index.ts +84 -0
- package/src/path/match.ts +82 -0
- package/src/path/matcher.ts +29 -0
- package/src/path/types.ts +39 -0
- package/src/types/internal.ts +41 -0
- package/src/types/types.ts +39 -0
package/src/Wooter.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type { EmptyObject } from "./types/types.ts";
|
|
2
|
+
import type { MethodDefinitionInput, MethodDefinitions } from "./Graph.ts";
|
|
3
|
+
import type {
|
|
4
|
+
MiddlewareHandler,
|
|
5
|
+
OptionalMerge,
|
|
6
|
+
Params,
|
|
7
|
+
RouteHandler,
|
|
8
|
+
State,
|
|
9
|
+
} from "./types/types.ts";
|
|
10
|
+
import type { Merge } from "./types/internal.ts";
|
|
11
|
+
import RouteContext, {
|
|
12
|
+
RouteContext__execution,
|
|
13
|
+
RouteContext__respond,
|
|
14
|
+
} from "./RouteContext.ts";
|
|
15
|
+
import { Graph } from "./Graph.ts";
|
|
16
|
+
import type { ParamsOfPath, Path } from "./path/types.ts";
|
|
17
|
+
|
|
18
|
+
type KeysSubset<U, T> =
|
|
19
|
+
Exclude<keyof U, keyof T> extends never ? unknown : never;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Typed HTTP router with composable middleware and nested route namespaces.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam TState Middleware-provided state available on every handler context.
|
|
25
|
+
* @typeParam TParentParams Params inherited from parent routers.
|
|
26
|
+
*/
|
|
27
|
+
export default class Wooter<
|
|
28
|
+
BaseState extends State | undefined = undefined,
|
|
29
|
+
TState extends State | undefined = undefined,
|
|
30
|
+
BasePath extends Path = Path,
|
|
31
|
+
TParentParams extends Params | undefined = ParamsOfPath<BasePath>,
|
|
32
|
+
> {
|
|
33
|
+
private graph: Graph;
|
|
34
|
+
#notFoundHandler?: RouteHandler<EmptyObject>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the registered 404 handler, or a default fallback when none is set.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
private get notFoundHandler(): RouteHandler<EmptyObject> {
|
|
42
|
+
return (
|
|
43
|
+
this.#notFoundHandler ??
|
|
44
|
+
(({ resp, url, request }) =>
|
|
45
|
+
resp(
|
|
46
|
+
new Response(`Not found ${request.method} ${url.pathname}`, {
|
|
47
|
+
status: 404,
|
|
48
|
+
}),
|
|
49
|
+
))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new router instance.
|
|
55
|
+
*
|
|
56
|
+
* @param basePath Optional base path prepended to all routes registered on this instance.
|
|
57
|
+
* @param catchStrayErrors Error sink used for asynchronous errors that occur after a response was already sent.
|
|
58
|
+
*/
|
|
59
|
+
constructor(
|
|
60
|
+
basePath?: BasePath,
|
|
61
|
+
protected catchStrayErrors: (e: unknown) => void = (e) => {
|
|
62
|
+
throw e;
|
|
63
|
+
},
|
|
64
|
+
) {
|
|
65
|
+
this.graph = new Graph(basePath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registers a handler for one or more HTTP methods on a path.
|
|
70
|
+
*
|
|
71
|
+
* Routes are matched in registration order. Use this overload for single methods,
|
|
72
|
+
* arrays of methods, or the wildcard `"*"`.
|
|
73
|
+
*
|
|
74
|
+
* @example Register a GET route
|
|
75
|
+
* ```ts
|
|
76
|
+
* router.route(p.path("users"), "GET", ({ resp }) => {
|
|
77
|
+
* resp(Response.json([]))
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Register multiple methods
|
|
82
|
+
* ```ts
|
|
83
|
+
* router.route(p.path("users"), ["GET", "POST"], ({ request, resp }) => {
|
|
84
|
+
* if (request.method === "GET") resp(Response.json([]))
|
|
85
|
+
* else resp(Response.json({}, { status: 201 }))
|
|
86
|
+
* })
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @typeParam TParams Parameter type inferred from the path.
|
|
90
|
+
* @param path Typed route path built with the path API.
|
|
91
|
+
* @param method HTTP method: string, array of methods, or `"*"` for all.
|
|
92
|
+
* @param handler Route handler receiving the route context.
|
|
93
|
+
* @returns The current router for method chaining.
|
|
94
|
+
* @throws TypeError if handler is not provided with string/array method.
|
|
95
|
+
*/
|
|
96
|
+
route<
|
|
97
|
+
TPath extends Path,
|
|
98
|
+
TParams extends Record<string, unknown> = ParamsOfPath<TPath>,
|
|
99
|
+
>(
|
|
100
|
+
path: TPath,
|
|
101
|
+
method: MethodDefinitionInput,
|
|
102
|
+
handler: RouteHandler<OptionalMerge<TParams, TParentParams>, TState>,
|
|
103
|
+
): this;
|
|
104
|
+
/**
|
|
105
|
+
* Registers different handlers for different HTTP methods on the same path.
|
|
106
|
+
*
|
|
107
|
+
* This overload accepts a method-to-handler map. Omitted methods are not handled.
|
|
108
|
+
*
|
|
109
|
+
* @example Register per-method handlers
|
|
110
|
+
* ```ts
|
|
111
|
+
* router.route(p.path("users"), {
|
|
112
|
+
* GET: ({ resp }) => resp(Response.json([])),
|
|
113
|
+
* POST: ({ request, resp }) => {
|
|
114
|
+
* const body = await request.json()
|
|
115
|
+
* resp(Response.json(body, { status: 201 }))
|
|
116
|
+
* },
|
|
117
|
+
* })
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* @typeParam TParams Parameter type inferred from the path.
|
|
121
|
+
* @param path Typed route path built with the path API.
|
|
122
|
+
* @param handlers Map of HTTP methods to their handler functions.
|
|
123
|
+
* @returns The current router for method chaining.
|
|
124
|
+
*/
|
|
125
|
+
route<
|
|
126
|
+
TPath extends Path,
|
|
127
|
+
TParams extends Record<string, unknown> = ParamsOfPath<TPath>,
|
|
128
|
+
>(
|
|
129
|
+
path: TPath,
|
|
130
|
+
handlers: MethodDefinitions<Merge<TParams, TParentParams>, TState>,
|
|
131
|
+
): this;
|
|
132
|
+
/**
|
|
133
|
+
* Registers a route definition on this router.
|
|
134
|
+
*
|
|
135
|
+
* @param path Typed route path.
|
|
136
|
+
* @param methodOrHandlers Method selector(s) or method-to-handler map.
|
|
137
|
+
* @param handler Handler used when `methodOrHandlers` is method-based.
|
|
138
|
+
* @returns The current router instance for chaining.
|
|
139
|
+
*/
|
|
140
|
+
route<
|
|
141
|
+
TPath extends Path,
|
|
142
|
+
TParams extends Record<string, unknown> = ParamsOfPath<TPath>,
|
|
143
|
+
>(
|
|
144
|
+
path: TPath,
|
|
145
|
+
methodOrHandlers:
|
|
146
|
+
| MethodDefinitionInput
|
|
147
|
+
| MethodDefinitions<Merge<TParams, TParentParams>, TState>,
|
|
148
|
+
handler?: RouteHandler<OptionalMerge<TParams, TParentParams>, TState>,
|
|
149
|
+
): this {
|
|
150
|
+
if (
|
|
151
|
+
typeof methodOrHandlers == "string" ||
|
|
152
|
+
Array.isArray(methodOrHandlers)
|
|
153
|
+
) {
|
|
154
|
+
if (!handler) throw new TypeError();
|
|
155
|
+
if (methodOrHandlers === "*") {
|
|
156
|
+
this.graph.addRoute_wildcardMethod(path, handler);
|
|
157
|
+
} else {
|
|
158
|
+
const methods = new Set(
|
|
159
|
+
[methodOrHandlers].flat().map((x) => x.toUpperCase()),
|
|
160
|
+
);
|
|
161
|
+
this.graph.addRoute_withMethodSet(path, handler, methods);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
this.graph.addRoute_withMethodMap(
|
|
165
|
+
path,
|
|
166
|
+
methodOrHandlers as MethodDefinitions<
|
|
167
|
+
Merge<TParams, TParentParams>,
|
|
168
|
+
TState
|
|
169
|
+
>,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Adds middleware that can enrich context state for downstream handlers.
|
|
177
|
+
*
|
|
178
|
+
* @param handler Middleware to run before route handlers.
|
|
179
|
+
* @returns A typed router view whose `state` reflects middleware output.
|
|
180
|
+
*
|
|
181
|
+
* @ignore
|
|
182
|
+
*/
|
|
183
|
+
use<TNextState extends State | undefined = undefined>(
|
|
184
|
+
handler: MiddlewareHandler<Params, TState, TNextState>,
|
|
185
|
+
): Wooter<
|
|
186
|
+
BaseState,
|
|
187
|
+
OptionalMerge<TState, TNextState>,
|
|
188
|
+
BasePath,
|
|
189
|
+
TParentParams
|
|
190
|
+
>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Adds middleware authored against a narrower input state shape.
|
|
194
|
+
*
|
|
195
|
+
* @param handler Middleware to run before route handlers.
|
|
196
|
+
* @returns A typed router view whose `state` reflects middleware output.
|
|
197
|
+
*
|
|
198
|
+
* @ignore
|
|
199
|
+
*/
|
|
200
|
+
use<
|
|
201
|
+
TNextState extends State | undefined = undefined,
|
|
202
|
+
THandlerInputState extends
|
|
203
|
+
(State & KeysSubset<THandlerInputState, TState>) | undefined = undefined,
|
|
204
|
+
>(
|
|
205
|
+
handler: MiddlewareHandler<
|
|
206
|
+
Record<string, unknown>,
|
|
207
|
+
THandlerInputState,
|
|
208
|
+
TNextState
|
|
209
|
+
>,
|
|
210
|
+
): Wooter<
|
|
211
|
+
BaseState,
|
|
212
|
+
OptionalMerge<TState, TNextState>,
|
|
213
|
+
BasePath,
|
|
214
|
+
TParentParams
|
|
215
|
+
>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Adds middleware to this router.
|
|
219
|
+
*
|
|
220
|
+
* @param handler Middleware to run before matching route handlers.
|
|
221
|
+
* @returns A typed router view whose `state` includes middleware output.
|
|
222
|
+
*/
|
|
223
|
+
use<TNextState extends State | undefined = undefined>(
|
|
224
|
+
handler: MiddlewareHandler<Record<string, unknown>, TState, TNextState>,
|
|
225
|
+
): Wooter<
|
|
226
|
+
BaseState,
|
|
227
|
+
OptionalMerge<TState, TNextState>,
|
|
228
|
+
BasePath,
|
|
229
|
+
TParentParams
|
|
230
|
+
> {
|
|
231
|
+
this.graph.addMiddleware(handler);
|
|
232
|
+
return this as unknown as Wooter<
|
|
233
|
+
BaseState,
|
|
234
|
+
OptionalMerge<TState, TNextState>,
|
|
235
|
+
BasePath,
|
|
236
|
+
TParentParams
|
|
237
|
+
>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Creates a child router mounted under the current router.
|
|
242
|
+
*
|
|
243
|
+
* Routes registered on the returned router are reachable through this router.
|
|
244
|
+
*
|
|
245
|
+
* @param basePath Path prefix for the child router.
|
|
246
|
+
* @returns A new router instance scoped to `basePath`.
|
|
247
|
+
*/
|
|
248
|
+
branch<
|
|
249
|
+
TPath extends Path,
|
|
250
|
+
TParams extends Record<string, unknown> = ParamsOfPath<TPath>,
|
|
251
|
+
>(
|
|
252
|
+
basePath: TPath,
|
|
253
|
+
): Wooter<BaseState, TState, BasePath, Merge<TParams, TParentParams>> {
|
|
254
|
+
const router = new Wooter<
|
|
255
|
+
BaseState,
|
|
256
|
+
TState,
|
|
257
|
+
BasePath,
|
|
258
|
+
Merge<TParams, TParentParams>
|
|
259
|
+
>();
|
|
260
|
+
this.graph.addNamespace(basePath, router.graph);
|
|
261
|
+
return router;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Registers a fallback handler used when no route matches.
|
|
266
|
+
*
|
|
267
|
+
* @param handler Route handler for unmatched requests.
|
|
268
|
+
* @returns The current router instance for chaining.
|
|
269
|
+
*/
|
|
270
|
+
notFound(handler: RouteHandler<EmptyObject>): this {
|
|
271
|
+
this.#notFoundHandler = handler;
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handles a request through middleware and route matching.
|
|
277
|
+
*
|
|
278
|
+
* This is the entry point to the router. Export it directly to an HTTP server.
|
|
279
|
+
*
|
|
280
|
+
* @example Use with Deno
|
|
281
|
+
* ```ts
|
|
282
|
+
* export default app
|
|
283
|
+
* // Or serve directly: Deno.serve(app.fetch)
|
|
284
|
+
* ```
|
|
285
|
+
*
|
|
286
|
+
* @param request The incoming HTTP request.
|
|
287
|
+
* @returns A promise resolving to the HTTP response.
|
|
288
|
+
* @throws On framework errors (handler didn't respond, responded twice, etc.).
|
|
289
|
+
*/
|
|
290
|
+
readonly fetch = (
|
|
291
|
+
request: Request,
|
|
292
|
+
base_state?: BaseState,
|
|
293
|
+
): Promise<Response> => {
|
|
294
|
+
const url = new URL(request.url);
|
|
295
|
+
let handler = this.graph.getHandler(url.pathname, request.method);
|
|
296
|
+
if (!handler) {
|
|
297
|
+
handler = RouteContext.useRouteHandler(
|
|
298
|
+
this.notFoundHandler,
|
|
299
|
+
{},
|
|
300
|
+
this.catchStrayErrors,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { promise, resolve, reject } = Promise.withResolvers<Response>();
|
|
305
|
+
const ctx = handler(base_state ?? {}, request, this.catchStrayErrors);
|
|
306
|
+
const execution = ctx[RouteContext__execution];
|
|
307
|
+
const respond = ctx[RouteContext__respond];
|
|
308
|
+
|
|
309
|
+
respond.then(resolve);
|
|
310
|
+
execution.then((result) => {
|
|
311
|
+
result.inspect((err) => {
|
|
312
|
+
reject(err);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return promise;
|
|
317
|
+
};
|
|
318
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel error used to abort handler execution without surfacing as a failure.
|
|
3
|
+
*
|
|
4
|
+
* Throw this after sending a response to stop further lifecycle work.
|
|
5
|
+
*/
|
|
6
|
+
export const ControlFlowBreak = Symbol("ControlFlowBreak");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type of {@link ControlFlowBreak}.
|
|
10
|
+
*/
|
|
11
|
+
export type ControlFlowBreak = typeof ControlFlowBreak;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base error class for framework-level errors.
|
|
15
|
+
*/
|
|
16
|
+
export default class WooterError extends Error {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks whether a value is a known internal framework error.
|
|
20
|
+
*
|
|
21
|
+
* @param v Value to test.
|
|
22
|
+
* @returns `true` when `v` is a `WooterError` or `ControlFlowBreak`.
|
|
23
|
+
*/
|
|
24
|
+
export function isWooterError(v: unknown): v is WooterError {
|
|
25
|
+
return v instanceof WooterError || v == ControlFlowBreak;
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public error exports.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
ControlFlowBreak,
|
|
9
|
+
default as WooterError,
|
|
10
|
+
isWooterError,
|
|
11
|
+
} from "../WooterError.ts";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
HandlerDidntRespondError,
|
|
15
|
+
HandlerRespondedTwiceError,
|
|
16
|
+
} from "../RouteContext.ts";
|
|
17
|
+
export { MiddlewareHandlerDidntCallUpError } from "../MiddlewareContext.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./error.ts";
|
|
2
|
+
export * from "./response.ts";
|
|
3
|
+
export { default as use, middleware } from "./use.ts";
|
|
4
|
+
/** Typed path builders and matchers. */
|
|
5
|
+
export * as p from "./path.ts";
|
|
6
|
+
export { Option, UnwrapNoneError } from "./option.ts";
|
|
7
|
+
export { ExpectErrError, ExpectOkError, Result } from "./result.ts";
|
|
8
|
+
export { default as Wooter } from "../Wooter.ts";
|
|
9
|
+
|
|
10
|
+
export * from "./types.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Typed path utility exports.
|
|
2
|
+
*
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as _p from "../path/index.ts";
|
|
7
|
+
|
|
8
|
+
/** Namespace-like default export containing all path utilities. */
|
|
9
|
+
const p: typeof import("../path/index.ts") = _p;
|
|
10
|
+
|
|
11
|
+
export default p;
|
|
12
|
+
export * from "../path/index.ts";
|
|
13
|
+
export * from "../path/types.ts";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a redirect response.
|
|
3
|
+
*
|
|
4
|
+
* @param location Redirect destination.
|
|
5
|
+
* @param init Optional response init. `status` defaults to `307`.
|
|
6
|
+
* @returns Redirect response with `Location` header set.
|
|
7
|
+
*/
|
|
8
|
+
export function makeRedirect(
|
|
9
|
+
location: string | URL,
|
|
10
|
+
init?: ResponseInit,
|
|
11
|
+
): Response {
|
|
12
|
+
const headers = new Headers(init?.headers);
|
|
13
|
+
headers.set("Location", location.toString());
|
|
14
|
+
return new Response(null, {
|
|
15
|
+
...init,
|
|
16
|
+
status: init?.status ?? 307,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates an error response with an optional body message.
|
|
23
|
+
*
|
|
24
|
+
* @param status HTTP status code.
|
|
25
|
+
* @param message Optional error message for body and status text.
|
|
26
|
+
* @param headers Optional response headers.
|
|
27
|
+
* @returns Error response.
|
|
28
|
+
*/
|
|
29
|
+
export function makeError(
|
|
30
|
+
status: number,
|
|
31
|
+
message?: string,
|
|
32
|
+
headers?: HeadersInit,
|
|
33
|
+
): Response {
|
|
34
|
+
return new Response(message, {
|
|
35
|
+
status,
|
|
36
|
+
statusText: message,
|
|
37
|
+
headers,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../types/types.ts";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MiddlewareHandler,
|
|
3
|
+
OptionalMerge,
|
|
4
|
+
Params,
|
|
5
|
+
RouteHandler,
|
|
6
|
+
State,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
import {
|
|
9
|
+
RouteContext__execution,
|
|
10
|
+
RouteContext__reporter,
|
|
11
|
+
RouteContext__respond,
|
|
12
|
+
} from "../RouteContext.ts";
|
|
13
|
+
import { Graph } from "../Graph.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Applies middleware directly to a route handler and returns a wrapped handler.
|
|
17
|
+
*
|
|
18
|
+
* @param middlewareHandler Middleware to apply before the handler.
|
|
19
|
+
* @param handler Route handler to wrap.
|
|
20
|
+
* @returns Route handler that executes middleware and then delegates to `handler`.
|
|
21
|
+
*/
|
|
22
|
+
export default function use<
|
|
23
|
+
NextState extends State | undefined = undefined,
|
|
24
|
+
BaseState extends State | undefined = undefined,
|
|
25
|
+
// deno-lint-ignore ban-types
|
|
26
|
+
TParams extends Params = {},
|
|
27
|
+
>(
|
|
28
|
+
middlewareHandler: MiddlewareHandler<TParams, BaseState, NextState>,
|
|
29
|
+
handler: RouteHandler<TParams, OptionalMerge<BaseState, NextState>>,
|
|
30
|
+
): RouteHandler<TParams, BaseState> {
|
|
31
|
+
return async (ctx) => {
|
|
32
|
+
const { params, request, state, resp } = ctx;
|
|
33
|
+
const reporter = ctx[RouteContext__reporter];
|
|
34
|
+
// @ts-expect-error: Generics defaults get annoying here
|
|
35
|
+
const internalHandler = Graph.compose(handler, params, [middlewareHandler]);
|
|
36
|
+
const new_ctx = internalHandler(state, request, reporter);
|
|
37
|
+
|
|
38
|
+
new_ctx[RouteContext__respond].promise.then((r) => resp(r));
|
|
39
|
+
return (await new_ctx[RouteContext__execution].promise).inspect((e) => {
|
|
40
|
+
throw e;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Identity helper used to type middleware declarations.
|
|
47
|
+
*
|
|
48
|
+
* @param handler Middleware handler.
|
|
49
|
+
* @returns The same middleware handler with preserved generic inference.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const userAgent = middleware<{ userAgent: string | null }>(async ({ request, next }) => {
|
|
54
|
+
* await next({ userAgent: request.headers.get("User-Agent") })
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* router.use(userAgent)
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function middleware<
|
|
61
|
+
TNextState extends State | undefined = undefined,
|
|
62
|
+
TState extends State | undefined = undefined, // deno-lint-ignore ban-types
|
|
63
|
+
TParams extends Params = {},
|
|
64
|
+
>(
|
|
65
|
+
handler: MiddlewareHandler<TParams, TState, TNextState>,
|
|
66
|
+
): MiddlewareHandler<TParams, TState, TNextState> {
|
|
67
|
+
return handler;
|
|
68
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { single, wrap } from "./matcher.ts";
|
|
2
|
+
import type { Matcher, Param, Path } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export * from "./matcher.ts";
|
|
5
|
+
export * from "./match.ts";
|
|
6
|
+
|
|
7
|
+
/** Builds a typed route path from literal segments and parameters. */
|
|
8
|
+
export const path = <const TPath extends Path>(...path: TPath): TPath => path;
|
|
9
|
+
|
|
10
|
+
/** Creates a named route parameter, optionally parsed by a matcher. */
|
|
11
|
+
export function param<Name extends string>(name: Name): Param<Name, string>;
|
|
12
|
+
/** Creates a named route parameter parsed by a matcher. */
|
|
13
|
+
export function param<Name extends string, T>(
|
|
14
|
+
name: Name,
|
|
15
|
+
matcher: Matcher<T, undefined>,
|
|
16
|
+
): Param<Name, T, undefined>;
|
|
17
|
+
/** Creates a named route parameter parsed by a matcher with options. */
|
|
18
|
+
export function param<Name extends string, T, Options>(
|
|
19
|
+
name: Name,
|
|
20
|
+
matcher: Matcher<T, Options>,
|
|
21
|
+
options: Options,
|
|
22
|
+
): Param<Name, T, Options>;
|
|
23
|
+
export function param(
|
|
24
|
+
name: string,
|
|
25
|
+
matcher?: Matcher<any, any>,
|
|
26
|
+
options?: any,
|
|
27
|
+
): Param<any, any, any> {
|
|
28
|
+
return matcher ? { name, matcher, options } : { name };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Matches a segment and converts it to a number. */
|
|
32
|
+
export const num: Matcher<number> = single<number>(function num(seg) {
|
|
33
|
+
const value = Number(seg);
|
|
34
|
+
if (isNaN(value)) return { ok: false };
|
|
35
|
+
return { ok: true, value };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Matches and returns one non-empty path segment. */
|
|
39
|
+
export const passthrough: Matcher<string> = single<string>((value) =>
|
|
40
|
+
value ? { ok: true, value } : { ok: false },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** Repeats a matcher zero or more times, or one or more times when requested. */
|
|
44
|
+
export function multiple<T, Opt>(
|
|
45
|
+
matcher: Matcher<T, Opt>,
|
|
46
|
+
at_least_one = false,
|
|
47
|
+
): Matcher<T[], Opt> {
|
|
48
|
+
return wrap<T[], Opt>(
|
|
49
|
+
(path, i, opt) => {
|
|
50
|
+
let consume = 0;
|
|
51
|
+
const value = [];
|
|
52
|
+
|
|
53
|
+
while (true) {
|
|
54
|
+
const result = matcher(path, i, opt);
|
|
55
|
+
if (!result.ok || !result.consume || result.consume <= 0) break;
|
|
56
|
+
value.push(result.value);
|
|
57
|
+
i += result.consume;
|
|
58
|
+
consume += result.consume;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (consume == 0 && at_least_one) return { ok: false };
|
|
62
|
+
return { ok: true, consume, value };
|
|
63
|
+
},
|
|
64
|
+
matcher.name,
|
|
65
|
+
(name) => name + (at_least_one ? "+" : "*"),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Matches the remaining path segments. */
|
|
70
|
+
export const rest: Matcher<string[]> = multiple(passthrough);
|
|
71
|
+
|
|
72
|
+
/** Makes a matcher optional. */
|
|
73
|
+
export function optional<T, Opt>(
|
|
74
|
+
matcher: Matcher<T, Opt>,
|
|
75
|
+
): Matcher<T | undefined, Opt> {
|
|
76
|
+
return wrap<T | undefined, Opt>(
|
|
77
|
+
(path, i, opt) => {
|
|
78
|
+
const result = matcher(path, i, opt);
|
|
79
|
+
return result.ok ? result : { ok: true, value: undefined, consume: 0 };
|
|
80
|
+
},
|
|
81
|
+
matcher.name,
|
|
82
|
+
(name) => `${name}?`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ParamsOfPath, Path } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Matches path segments against a typed pattern and extracts params.
|
|
5
|
+
*
|
|
6
|
+
* When `exact` is `true`, all remaining segments must be consumed.
|
|
7
|
+
*/
|
|
8
|
+
export function match<const TPath extends Path>(
|
|
9
|
+
segments: string[],
|
|
10
|
+
i: number,
|
|
11
|
+
pattern: TPath,
|
|
12
|
+
exact: true,
|
|
13
|
+
): { match: false } | { match: true; params: ParamsOfPath<TPath> };
|
|
14
|
+
/**
|
|
15
|
+
* Matches path segments against a typed pattern and extracts params.
|
|
16
|
+
*
|
|
17
|
+
* When `exact` is `true`, all remaining segments must be consumed.
|
|
18
|
+
*/
|
|
19
|
+
export function match<const TPath extends Path>(
|
|
20
|
+
segments: string[],
|
|
21
|
+
i: number,
|
|
22
|
+
pattern: TPath,
|
|
23
|
+
exact: false,
|
|
24
|
+
):
|
|
25
|
+
| { match: false }
|
|
26
|
+
| { match: true; params: ParamsOfPath<TPath>; stopped_at: number };
|
|
27
|
+
export function match(
|
|
28
|
+
segments: string[],
|
|
29
|
+
i: number,
|
|
30
|
+
pattern: Path,
|
|
31
|
+
exact: boolean,
|
|
32
|
+
):
|
|
33
|
+
| { match: false }
|
|
34
|
+
| { match: true; params: Record<string, unknown>; stopped_at?: number } {
|
|
35
|
+
const params: Record<string, unknown> = {};
|
|
36
|
+
|
|
37
|
+
for (const patternElement of pattern) {
|
|
38
|
+
if (typeof patternElement == "string") {
|
|
39
|
+
if (i >= segments.length || segments[i] != patternElement)
|
|
40
|
+
return { match: false };
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!("matcher" in patternElement)) {
|
|
45
|
+
if (i >= segments.length) return { match: false };
|
|
46
|
+
params[patternElement.name] = segments[i];
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = patternElement.matcher(segments, i, patternElement.options);
|
|
52
|
+
if (result.ok) {
|
|
53
|
+
params[patternElement.name] = result.value;
|
|
54
|
+
i += result.consume;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { match: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (i < segments.length && exact) return { match: false };
|
|
62
|
+
|
|
63
|
+
return { match: true, params, stopped_at: i };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Converts a typed path pattern into a readable route string. */
|
|
67
|
+
export function stringify(path: Path): string {
|
|
68
|
+
const strs = [];
|
|
69
|
+
for (const patternElement of path) {
|
|
70
|
+
if (typeof patternElement == "string") {
|
|
71
|
+
strs.push(patternElement);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if ("matcher" in patternElement && patternElement.matcher.name) {
|
|
75
|
+
strs.push(`:${patternElement.name}(${patternElement.matcher.name})`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
strs.push(`:${patternElement.name}`);
|
|
79
|
+
}
|
|
80
|
+
strs.unshift("");
|
|
81
|
+
return strs.join("/");
|
|
82
|
+
}
|