Last active
October 23, 2023 03:58
-
-
Save baetheus/a4ff018238614e88bf83a73f0e649228 to your computer and use it in GitHub Desktop.
Basic Router
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
/** @jsx h */ | |
import { pipe } from "fun/fn.ts"; | |
import { h } from "https://esm.sh/[email protected]"; | |
import * as R from "../router.ts"; | |
import { jsx } from "../jsx.ts"; | |
type State = { count: number }; | |
function Count({ count }: State) { | |
return <h1>Count is {count}</h1>; | |
} | |
const handler = pipe( | |
R.router<State>(), | |
R.handle( | |
"GET /count", | |
({ state }) => { | |
state.count++; | |
return jsx(<Count {...state} />); | |
}, | |
), | |
R.handle("POST /proxy", async ({ request }) => { | |
const body = await request.text().catch(() => "https://bee.ignoble.dev"); | |
return fetch(body); | |
}), | |
R.use({ count: 0 }), | |
); | |
Deno.serve(handler); |
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
import type { VNode } from "https://esm.sh/[email protected]"; | |
import { render } from "https://esm.sh/[email protected]"; | |
import { html } from "./response.ts"; | |
export function jsx(vnode: VNode): Response { | |
return html(render(vnode)); | |
} |
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
/** | |
* Responses | |
*/ | |
export function html(html: string): Response { | |
return new Response(html, { | |
headers: { "content-type": "text/html; charset=utf-8" }, | |
}); | |
} |
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
import type { Option } from "fun/option.ts"; | |
import * as A from "fun/array.ts"; | |
import * as O from "fun/option.ts"; | |
/** | |
* Accepted HTTP Verbs in Route String | |
*/ | |
type HttpVerbs = | |
| "GET" | |
| "HEAD" | |
| "POST" | |
| "PUT" | |
| "DELETE" | |
| "CONNECT" | |
| "OPTIONS" | |
| "TRACE" | |
| "PATCH"; | |
/** | |
* Given a const string return a single key object with a string value at that | |
* key. | |
*/ | |
type Rec<Key extends string = string> = { readonly [K in Key]: string }; | |
/** | |
* The format of a route string | |
*/ | |
type RouteString = `${HttpVerbs} /${string}`; | |
/** | |
* Parse variables from RouteString at the type level | |
*/ | |
type ParseVars< | |
P extends string, | |
// deno-lint-ignore ban-types | |
R extends Record<string, string> = {}, | |
> = P extends `${HttpVerbs} /${infer Part}` ? ParseVars<Part> | |
: P extends `:${infer Key}/${infer Part}` ? ParseVars<Part, R & Rec<Key>> | |
: P extends `${infer _}/${infer Part}` ? ParseVars<Part, R> | |
: P extends `:${infer Key}` ? ParseVars<"", R & Rec<Key>> | |
: { readonly [K in keyof R]: string }; | |
/** | |
* A route parser is a function that takes a verb and a split path from a | |
* request and returns an Option containing possible path variables. None | |
* implies that the path does not match and some implies that it does. | |
*/ | |
type RouteParser<V> = ( | |
verb: HttpVerbs, | |
split: readonly string[], | |
) => Option<V>; | |
/** | |
* Parse a route into (HttpVerb, path[]) => Option<Variables> | |
*/ | |
export function routeParser<In extends RouteString>( | |
route: In, | |
): RouteParser<ParseVars<In>> { | |
const [verb, rest] = route.split(" "); | |
const words = rest.split("/"); | |
return (v, s) => { | |
if (verb !== v.toUpperCase()) { | |
return O.none; | |
} else if (words.length !== s.length) { | |
return O.none; | |
} else { | |
const vars: Record<string, string> = {}; | |
for (let i = 0; i < words.length; i++) { | |
const left = words[i]; | |
const right = s[i]; | |
// Set variable in vars record | |
if (left.startsWith(":")) { | |
vars[left.slice(1)] = right; | |
continue; | |
} | |
// Bail early if not a variable and route doesn't match | |
if (left.toUpperCase() !== right.toUpperCase()) { | |
return O.none; | |
} | |
} | |
return O.some(vars as ParseVars<In>); | |
} | |
}; | |
} | |
/** | |
* Context for a request. | |
*/ | |
export type Context<V, S> = { | |
readonly request: Request; | |
readonly variables: V; | |
readonly state: S; | |
}; | |
export function context<V, S>( | |
request: Request, | |
variables: V, | |
state: S, | |
): Context<V, S> { | |
return { request, variables, state }; | |
} | |
/** | |
* Handle a request with Context as input. | |
*/ | |
export type Handler<Vars, S = unknown> = ( | |
ctx: Context<Vars, S>, | |
) => Response | Promise<Response>; | |
/** | |
* A Route is a combination of a RouteString, a RouteParser, and a Handler. | |
*/ | |
export type Route<V, S> = { | |
readonly route: RouteString; | |
readonly parser: RouteParser<V>; | |
readonly handler: Handler<V, S>; | |
}; | |
export function route<V, S>( | |
route: RouteString, | |
parser: RouteParser<V>, | |
handler: Handler<V, S>, | |
): Route<V, S> { | |
return { route, parser, handler }; | |
} | |
/** | |
* A Router is an Array of Routes that all use the same state. | |
*/ | |
// deno-lint-ignore no-explicit-any | |
export type Router<S = unknown> = readonly Route<any, S>[]; | |
export function router<S>(): Router<S> { | |
return []; | |
} | |
/** | |
* The handle function parses variables out of a RouteString and uses them to | |
* construct the types for a handler function that is also passed in. | |
*/ | |
export function handle<R extends RouteString, S>( | |
routeString: R, | |
handler: Handler<ParseVars<R>, S>, | |
): (router: Router<S>) => Router<S> { | |
const parser = routeParser(routeString); | |
return A.append(route(routeString, parser, handler)); | |
} | |
const NotFound = new Response("Not Found", { status: 404 }); | |
/** | |
* The use function takes an initial state, then a router, and uses it to | |
* construct a Deno.ServeHandler for use with Deno.serve. | |
*/ | |
export function use<S>( | |
state: S, | |
): (router: Router<S>) => Deno.ServeHandler { | |
return (router) => (request) => { | |
const verb = request.method.toUpperCase() as HttpVerbs; | |
const url = new URL(request.url); | |
const path = url.pathname.split("/"); | |
for (const route of router) { | |
const variables = route.parser(verb, path); | |
if (O.isNone(variables)) { | |
continue; | |
} | |
return route.handler(context(request, variables.value, state)); | |
} | |
return NotFound; | |
}; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment