-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7ce9e2c
commit a51e17a
Showing
8 changed files
with
331 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,13 +6,10 @@ | |
"./src/parser.test.ts": "./src/parser.test.ts", | ||
"./src/parser.ts": "./src/parser.ts", | ||
"./src/types.ts": "./src/types.ts", | ||
"./src/deps.ts": "./src/deps.ts", | ||
"./jsr.json": "./jsr.json", | ||
"./.vscode/settings.json": "./.vscode/settings.json" | ||
}, | ||
"imports": { | ||
"@std/assert": "jsr:@std/[email protected]", | ||
"mock_fetch/": "https://deno.land/x/[email protected]/", | ||
"zod/": "https://deno.land/x/[email protected]/" | ||
"@std/assert": "jsr:@std/[email protected]" | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { z } from "./deps.ts"; | ||
import { z } from "npm:zod"; | ||
|
||
export interface PlaylistHeader { | ||
attrs: { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// Copyright 2021 denosaurs. All rights reserved. MIT license. | ||
|
||
/** | ||
* A deno deploy compatible request handler which can be either sync or async | ||
* and gets passed the `Request`, it then eventually returns a `Response` | ||
*/ | ||
export type RequestHandler = (req: Request) => Response | Promise<Response>; | ||
|
||
/** | ||
* A handler type for anytime the `MatchHandler` or `other` parameter handler | ||
* fails | ||
*/ | ||
export type ErrorHandler = ( | ||
req: Request, | ||
err: unknown, | ||
) => Response | Promise<Response>; | ||
|
||
/** | ||
* A handler type for anytime a method is received that is not defined | ||
*/ | ||
export type UnknownMethodHandler = ( | ||
req: Request, | ||
knownMethods: string[], | ||
) => Response | Promise<Response>; | ||
|
||
/** | ||
* A handler type for a router path match which gets passed the matched values | ||
*/ | ||
export type MatchHandler = (req: Request, match: Record<string, string>) => Response | Promise<Response>; | ||
|
||
/** | ||
* A record of route paths and `MatchHandler`s which are called when a match is | ||
* found along with it's values. | ||
* | ||
* The route paths follow the path-to-regexp format with the addition of being able | ||
* to prefix a route with a method name and the `@` sign. For example a route only | ||
* accepting `GET` requests would look like: `GET@/`. | ||
*/ | ||
export type Routes = Record<string, MatchHandler>; | ||
|
||
/** | ||
* The default other handler for the router | ||
*/ | ||
export function defaultOtherHandler(_req: Request): Response { | ||
return new Response(null, { | ||
status: 404, | ||
}); | ||
} | ||
|
||
/** | ||
* The default error handler for the router | ||
*/ | ||
export function defaultErrorHandler(_req: Request, err: unknown): Response { | ||
console.error(err); | ||
|
||
return new Response(null, { | ||
status: 500, | ||
}); | ||
} | ||
|
||
/** | ||
* The default unknown method handler for the router | ||
*/ | ||
export function defaultUnknownMethodHandler( | ||
_req: Request, | ||
knownMethods: string[], | ||
): Response { | ||
return new Response(null, { | ||
status: 405, | ||
headers: { | ||
Accept: knownMethods.join(", "), | ||
}, | ||
}); | ||
} | ||
|
||
export const METHODS = [ | ||
"GET", | ||
"HEAD", | ||
"POST", | ||
"PUT", | ||
"DELETE", | ||
"OPTIONS", | ||
"PATCH", | ||
] as const; | ||
|
||
const methodRegex = new RegExp(`(?<=^(?:${METHODS.join("|")}))@`); | ||
|
||
/** | ||
* A simple and tiny router for deno deploy | ||
* | ||
* ``` | ||
* import { listenAndServe } from "https://deno.land/std/http/server.ts"; | ||
* import { router } from "https://crux.land/[email protected]"; | ||
* | ||
* await listenAndServe( | ||
* ":8080", | ||
* router({ | ||
* "/": (_req) => new Response("Hello world!", { status: 200 }), | ||
* }), | ||
* ); | ||
* ``` | ||
* | ||
* @param routes A record of all routes and their corresponding handler functions | ||
* @param other An optional parameter which contains a handler for anything that | ||
* doesn't match the `routes` parameter | ||
* @param error An optional parameter which contains a handler for any time it | ||
* fails to run the default request handling code | ||
* @param unknownMethod An optional parameter which contains a handler for any time a method | ||
* that is not defined is used | ||
* @returns A deno deploy compatible request handler | ||
*/ | ||
export function router( | ||
routes: Routes, | ||
other: RequestHandler = defaultOtherHandler, | ||
error: ErrorHandler = defaultErrorHandler, | ||
unknownMethod: UnknownMethodHandler = defaultUnknownMethodHandler, | ||
): RequestHandler { | ||
return async (req) => { | ||
try { | ||
// route > method > handler | ||
const internalRoutes: Record<string, Record<string, MatchHandler>> = {}; | ||
|
||
for (const [route, handler] of Object.entries(routes)) { | ||
const [methodOrPath, path] = route.split(methodRegex); | ||
|
||
if (path) { | ||
internalRoutes[path] ??= {}; | ||
internalRoutes[path][methodOrPath] = handler; | ||
} else { | ||
internalRoutes[methodOrPath] ??= {}; | ||
internalRoutes[methodOrPath]["any"] = handler; | ||
} | ||
} | ||
|
||
for (const [path, methods] of Object.entries(internalRoutes)) { | ||
const pattern = new URLPattern({ | ||
pathname: path, | ||
}); | ||
const res = pattern.exec(req.url); | ||
|
||
if (res !== null) { | ||
for (const [method, handler] of Object.entries(methods)) { | ||
if (req.method === method) { | ||
const groups: Record<string, string> = {}; | ||
for (const [key, value] of Object.entries(res.pathname.groups)) { | ||
if (value !== undefined) { | ||
groups[key] = value; | ||
} | ||
} | ||
return await handler(req, groups); | ||
} | ||
} | ||
if (methods["any"]) { | ||
const groups: Record<string, string> = {}; | ||
for (const [key, value] of Object.entries(res.pathname.groups)) { | ||
if (value !== undefined) { | ||
groups[key] = value; | ||
} | ||
} | ||
return await methods["any"](req, groups); | ||
} else { | ||
return await unknownMethod(req, Object.keys(methods)); | ||
} | ||
} | ||
} | ||
|
||
return await other(req); | ||
} catch (err) { | ||
return error(req, err); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { MatchHandler, router, Routes } from "../../../crux.land/api/get/2KNRVU.ts"; | ||
|
||
export type { MatchHandler }; | ||
|
||
class UnhandledRouteError extends Error { | ||
routes: Routes; | ||
request: Request; | ||
constructor(init: { request: Request; routes: Routes }) { | ||
const { request, routes } = init; | ||
|
||
const method = request.method; | ||
const reqPath = new URL(request.url).pathname; | ||
const routesNumber = Object.entries(routes).length; | ||
const routePlural = routesNumber === 1 | ||
? "route has a handler" | ||
: "routes have handlers"; | ||
|
||
// deno-fmt-ignore | ||
super(`${method} ${reqPath} (${routesNumber} ${routePlural})`); | ||
|
||
this.name = this.constructor.name; | ||
if (Error.captureStackTrace) { | ||
Error.captureStackTrace(this, this.constructor); | ||
} | ||
|
||
this.routes = routes; | ||
this.request = request; | ||
} | ||
} | ||
|
||
export interface MockFetch { | ||
fetch: typeof globalThis.fetch; | ||
mock: (route: string, handler: MatchHandler) => void; | ||
remove: (route: string) => void; | ||
reset: () => void; | ||
} | ||
|
||
/** | ||
* Get a set of functions that do not share any state with the globals. | ||
* | ||
* The returned object can be destructured. | ||
* | ||
* ``` | ||
* const { fetch, mock, remove, reset } = sandbox() | ||
* ``` | ||
*/ | ||
export function sandbox(): MockFetch { | ||
const routeStore = new Map<string, MatchHandler>(); | ||
|
||
async function fetch( | ||
input: string | Request | URL, | ||
init?: RequestInit, | ||
): Promise<Response> { | ||
// Request constructor won't take a URL, so we need to normalize it first. | ||
if (input instanceof URL) input = input.toString(); | ||
const req = new Request(input, init); | ||
|
||
const routes = Object.fromEntries(routeStore.entries()); | ||
|
||
// The router needs to be constructed every time because the routes map is | ||
// very likely to change between fetches. | ||
return await router( | ||
routes, | ||
// If an unhandled route is fetched, throw an error. | ||
(request) => { | ||
throw new UnhandledRouteError({ request, routes }); | ||
}, | ||
// Errors thrown by a handler, including the unknown route handler, will | ||
// return a 500 Internal Server Error. That's the right behaviour in most | ||
// cases, but we actually *want* that to throw. | ||
(_, error) => { | ||
throw error; | ||
}, | ||
)(req); | ||
} | ||
|
||
function mock(route: string, handler: MatchHandler) { | ||
routeStore.set(route, handler); | ||
} | ||
|
||
function remove(route: string) { | ||
routeStore.delete(route); | ||
} | ||
|
||
function reset() { | ||
routeStore.clear(); | ||
} | ||
|
||
return { | ||
reset, | ||
mock, | ||
remove, | ||
fetch, | ||
}; | ||
} | ||
|
||
const globalMockFetch = sandbox(); | ||
|
||
/** This is the function that replaces `fetch` when you call `install()`. */ | ||
export const mockedFetch = globalMockFetch.fetch; | ||
|
||
/** | ||
* Mock a new route, or override an existing handler. | ||
* | ||
* The route uses URLPattern syntax, with the additional extension of | ||
* (optional) method routing by prefixing with the method, | ||
* eg. `"POST@/user/:id"`. | ||
* | ||
* The handler function may be asynchronous. | ||
* | ||
* ``` | ||
* mock("GET@/users/:id", async (_req, params) => { | ||
* const id = parseInt(params["id"]); | ||
* const data = await magicallyGetMyUserData(id); | ||
* return new Response(JSON.stringify(data)); | ||
* }) | ||
* ``` | ||
*/ | ||
export const mock = globalMockFetch.mock; | ||
|
||
/** Remove an existing route handler. */ | ||
export const remove = globalMockFetch.remove; | ||
|
||
/** Remove all existing route handlers. */ | ||
export const reset = globalMockFetch.reset; | ||
|
||
// Store the original fetch so it can be restored later | ||
const originalFetch = globalThis.fetch; | ||
|
||
// The functions below are `const` for consistency. | ||
|
||
/** | ||
* Replace `globalThis.fetch` with `mockedFetch` (or another function that | ||
* matches the `fetch` signature) | ||
* | ||
* To restore the original `globalThis.fetch`, call `uninstall()`. | ||
*/ | ||
export const install = (replacement?: typeof fetch) => { | ||
globalThis.fetch = replacement ?? mockedFetch; | ||
}; | ||
|
||
/** | ||
* Restore `globalThis.fetch` to what it was before this library was imported. | ||
*/ | ||
export const uninstall = () => { | ||
globalThis.fetch = originalFetch; | ||
reset(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"imports": { | ||
"https://crux.land/[email protected]": "./crux.land/api/get/2KNRVU.ts", | ||
"https://crux.land/": "./crux.land/", | ||
"https://deno.land/": "./deno.land/" | ||
} | ||
} |