Skip to content

Commit

Permalink
progress on fart server
Browse files Browse the repository at this point in the history
  • Loading branch information
EthanThatOneKid committed Dec 18, 2021
1 parent 3f20d43 commit 1b85eec
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 52 deletions.
9 changes: 9 additions & 0 deletions fart_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Fart Server

## Usage

### Spin up local Fart server

```bash
deno run --allow-env --allow-net fart_server/serve.ts
```
6 changes: 3 additions & 3 deletions fart_server/bonus_features/shortlinks/shortlinks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"design": "https://docs.google.com/document/d/1pGNLsDr-WysIIqB4nc1pTCL8FMmPxkJMNoGsRMkA0TY/edit",
"github": "https://github.com/EthanThatOneKid/fart/",
"author": "https://etok.codes/"
"/design": "https://docs.google.com/document/d/1pGNLsDr-WysIIqB4nc1pTCL8FMmPxkJMNoGsRMkA0TY/edit",
"/github": "https://github.com/EthanThatOneKid/fart/",
"/author": "https://etok.codes/"
}
20 changes: 19 additions & 1 deletion fart_server/bonus_features/shortlinks/shortlinks.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
// TODO(@ethanthatonekid): write implementation for shortlinks.test.ts
// NOTE: import data from shortlinks.json
import shortlinks from "./shortlinks.json" assert { type: "json" };

const map = Object.entries(shortlinks)
.reduce((result, [key, value]) => {
result.set(key, value);
return result;
}, new Map<string, string>());

export const redirectIfShortlink = (request: Request): Response | null => {
const { pathname } = new URL(request.url);
if (pathname.includes("?")) {
const query = pathname.slice(pathname.indexOf("?"));
const shortlink = map.get(pathname.slice(0, pathname.indexOf("?")));
if (shortlink !== undefined) return Response.redirect(shortlink + query);
}
const shortlink = map.get(pathname.replace(/([^:]\/)\/+/g, "/"));
if (shortlink !== undefined) return Response.redirect(shortlink);
return null;
};
17 changes: 0 additions & 17 deletions fart_server/handle_request.ts

This file was deleted.

46 changes: 46 additions & 0 deletions fart_server/serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { redirectToDenoDeployPreviewUrl } from "./bonus_features/versions/mod.ts";
import { getSize, inject, register } from "./utils.ts";

export const handleRequest = async (event: Deno.RequestEvent) => {
const { request, respondWith } = event;
respondWith(await inject(request));
};

// redirect to another server running a different version of the Fart library
register(redirectToDenoDeployPreviewUrl);

// redirect to an external URL
register(redirectIfShortlink);

// show how many handlers are registered
register((request) => {
if (new URL(request.url).pathname === "/debug/size") {
return new Response(String(getSize()));
}
return null;
});

// show deployment ID if running on Deno Deploy
register((request) => {
if (new URL(request.url).pathname === "/debug/deployment") {
return new Response(String(Deno.env.get("DENO_DEPLOYMENT_ID")));
}
return null;
});

if (Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined) {
// add the fetch listener if running on Deno Deploy
addEventListener(
"fetch",
handleRequest as unknown as EventListenerOrEventListenerObject,
);
} else if (import.meta.main) {
// serve the HTTP server if running locally
const port = parseInt(Deno.env.get("PORT") || "8080");
console.log(`Access HTTP webserver at: http://localhost:${port}/`);
for await (const connection of Deno.listen({ port })) {
for await (const event of Deno.serveHttp(connection)) {
await handleRequest(event);
}
}
}
49 changes: 37 additions & 12 deletions fart_server/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import { assertEquals } from "../deps/std/testing.ts";
import type { MatchingFunction } from "./utils.ts";
import { matchHome, matchSubroute, register, route } from "./utils.ts";
import { clear, getSize, inject, register } from "./utils.ts";

const assertMatches = async (matchingFn: MatchingFunction, url: string) => {
const expectation = new Response("Hello, world!");
register(matchingFn, () => expectation);
const reality = await route(new Request(url));
assertEquals(reality, expectation);
};
// Note: Make sure each test clears the handlers if changes were made.

Deno.test("matches a simple home route", async () => {
await assertMatches(matchHome, "https://example.com/");
Deno.test("returns 404 without registering a handler", async () => {
const { status } = await inject(new Request("https://example.com/"));
assertEquals(status, 404);
});

Deno.test("matches a custom route", async () => {
await assertMatches(matchSubroute("/abc"), "http://example.com/abc");
Deno.test("size of handlers is 0 without registering a handler", () => {
assertEquals(getSize(), 0);
});

Deno.test("size is reduced to 0 when clear is called", () => {
register(() => null);
assertEquals(getSize(), 1);
register(() => null, () => null);
assertEquals(getSize(), 3);
clear();
assertEquals(getSize(), 0);
});

Deno.test("returns 404 when all handlers return null", async () => {
register(() => null);
const { status } = await inject(new Request("https://example.com/"));
assertEquals(status, 404);
clear();
});

Deno.test("returns data when a handler returns a response", async () => {
register(() => new Response("abc"));
const response = await inject(new Request("https://example.com/"));
assertEquals(await response.text(), "abc");
clear();
});

Deno.test("returns data when a handler returns a response and cascades on null", async () => {
register(() => null, () => null, () => null, () => new Response("abc"));
const response = await inject(new Request("https://example.com/"));
assertEquals(await response.text(), "abc");
clear();
});
36 changes: 17 additions & 19 deletions fart_server/utils.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
export type MatchingFunction = (r: Request) => boolean;
export type RequestHandler = (r: Request) => Response | Promise<Response>;
export type Result = null | Response | Promise<null | Response>;

export type Route = [MatchingFunction, RequestHandler];
export type RequestHandler = (r: Request) => Result;

/**
* In-memory storage of the Fart Server's configuration.
*/
const routes: Route[] = [];
const handlers: RequestHandler[] = [];

/**
* Routes a given HTTP request to the intended `bonus_features` and
* sets the appropriate content type header.
* @param request incoming http request
* @returns routed Fart server response
*/
export const route = async (request: Request): Promise<Response> => {
for (const [match, handler] of routes) {
if (match(request)) {
return await handler(request);
export const inject = async (request: Request): Promise<Response> => {
for (const handler of handlers) {
const result = await handler(request);
if (result !== null) {
return result;
}
}
return new Response("404", { status: 404 });
};

export const register = (
matchRoute: MatchingFunction,
handler: RequestHandler,
) => {
routes.push([matchRoute, handler]);
export const register = (...gimmeHandlers: RequestHandler[]) => {
handlers.push(...gimmeHandlers);
};

export const matchHome = (r: Request): boolean => {
return new URL(r.url).pathname === "/";
export const clear = () => {
handlers.length = 0;
};

export const matchSubroute = (subroute: string) => {
return (r: Request): boolean => {
return new URL(r.url).pathname === subroute;
};
export const getSize = () => {
return handlers.length;
};

// TODO(@ethanthatonekid): Write new functions to access the Fart Server's
// configuration.

0 comments on commit 1b85eec

Please sign in to comment.