Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active May 17, 2020 21:21
Show Gist options
  • Select an option

  • Save baetheus/0258d34412909ac79e26e4da6125fbe5 to your computer and use it in GitHub Desktop.

Select an option

Save baetheus/0258d34412909ac79e26e4da6125fbe5 to your computer and use it in GitHub Desktop.
Deno Oak adapter for hyper-ts
export * from "https://cdn.pika.dev/fp-ts@^2.6.1";
export * from "https://cdn.pika.dev/hyper-ts@^0.5.2";
import { either as E, taskEither as TE, pipeable } from "../deps/fp_ts.ts";
import { RouterContext, Request, RouterMiddleware } from "../deps/oak.ts";
import {
Status,
CookieOptions,
Connection,
HeadersOpen,
ResponseEnded,
Middleware,
execMiddleware,
StatusOpen,
} from "../deps/hyper_ts.ts";
const { pipe } = pipeable;
export type LinkedList<A> =
| { type: "Nil"; length: number }
| { type: "Cons"; head: A; tail: LinkedList<A>; length: number };
export const nil: LinkedList<never> = { type: "Nil", length: 0 };
export function cons<A>(head: A, tail: LinkedList<A>): LinkedList<A> {
return {
type: "Cons",
head,
tail,
length: tail.length + 1,
};
}
export function toArray<A>(list: LinkedList<A>): Array<A> {
const len = list.length;
const r: Array<A> = new Array(len);
let l: LinkedList<A> = list;
let i = 1;
while (l.type !== "Nil") {
r[len - i] = l.head;
i++;
l = l.tail;
}
return r;
}
export type Action =
| { type: "setBody"; body: unknown }
| { type: "endResponse" }
| { type: "setStatus"; status: Status }
| { type: "setHeader"; name: string; value: string }
| { type: "clearCookie"; name: string; options: CookieOptions }
| { type: "setCookie"; name: string; value: string; options: CookieOptions };
const endResponse: Action = { type: "endResponse" };
const parseQueryParams = (fragment: string): Record<string, string> => {
const split = fragment.split("?");
if (split[1] === undefined) {
return {};
}
return split[1].split("&").reduce((qs, q) => {
const [k, v] = q.split("=");
return { ...qs, [decodeURIComponent(k)]: decodeURIComponent(v || "") };
}, {});
};
export class OakConnection<S> implements Connection<S> {
readonly _S!: S;
constructor(
readonly ctx: RouterContext,
readonly actions: LinkedList<Action> = nil,
readonly ended: boolean = false
) {}
chain<T>(action: Action, ended: boolean = false): OakConnection<T> {
return new OakConnection<T>(this.ctx, cons(action, this.actions), ended);
}
getRequest(): Request {
return this.ctx.request;
}
getBody(): unknown {
return this.ctx.response.body;
}
getHeader(name: string): unknown {
return this.ctx.request.headers.get(name);
}
getParams(): unknown {
return this.ctx.params;
}
getQuery(): unknown {
return this.ctx.request.url.searchParams;
}
getOriginalUrl(): string {
return this.ctx.request.url.toString();
}
getMethod(): string {
return this.ctx.request.method;
}
setCookie(
name: string,
value: string,
options: CookieOptions
): OakConnection<HeadersOpen> {
return this.chain({ type: "setCookie", name, value, options });
}
clearCookie(
name: string,
options: CookieOptions
): OakConnection<HeadersOpen> {
return this.chain({ type: "clearCookie", name, options });
}
setHeader(name: string, value: string): OakConnection<HeadersOpen> {
return this.chain({ type: "setHeader", name, value });
}
setStatus(status: Status): OakConnection<HeadersOpen> {
return this.chain({ type: "setStatus", status });
}
setBody(body: unknown): OakConnection<ResponseEnded> {
return this.chain({ type: "setBody", body }, true);
}
endResponse(): OakConnection<ResponseEnded> {
return this.chain(endResponse, true);
}
}
function run(ctx: RouterContext, action: Action): RouterContext {
switch (action.type) {
case "clearCookie":
ctx.cookies.delete(action.name);
return ctx;
case "endResponse":
return ctx;
case "setBody":
ctx.response.body = action.body;
return ctx;
case "setCookie":
ctx.cookies.set(action.name, action.value, action.options);
return ctx;
case "setHeader":
ctx.response.headers.set(action.name, action.value);
return ctx;
case "setStatus":
ctx.response.status = action.status;
return ctx;
}
}
function exec<I, O, E>(
middleware: Middleware<I, O, E, void>,
ctx: RouterContext,
next: () => Promise<void>
): Promise<void> {
return execMiddleware(middleware, new OakConnection<I>(ctx))().then((e) =>
pipe(
e,
E.fold(next, async (c) => {
const { actions: list, ctx, ended } = c as OakConnection<O>;
const len = list.length;
const actions = toArray(list);
for (let i = 0; i < len; i++) {
run(ctx, actions[i]);
}
if (!ended) {
next();
}
})
)
);
}
export function toRequestHandler<I, O, E>(
middleware: Middleware<I, O, E, void>
): RouterMiddleware {
return (ctx, next) => exec(middleware, ctx, next);
}
/**
* @since 0.5.0
*/
export function fromRequestHandler<I = StatusOpen, E = never, A = never>(
requestHandler: RouterMiddleware,
f: (ctx: RouterContext) => A
): Middleware<I, I, E, A> {
return (c) =>
TE.rightTask(
() =>
new Promise((resolve) => {
const { ctx } = c as OakConnection<I>;
requestHandler(ctx, async () => resolve([f(ctx), c]));
})
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment