summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2024-02-23 14:40:24 -0300
committerEuAndreh <eu@euandre.org>2024-02-23 14:40:24 -0300
commit21be0bfc48d73beb9860a47d28b5b16b9c2a29b8 (patch)
tree17eccfccb7c5d54d65f6c133cd3585f983175c95
parentImplement accretion.runMigrations() and wrappings of node-sqlite3 (diff)
downloadpapod-21be0bfc48d73beb9860a47d28b5b16b9c2a29b8.tar.gz
papod-21be0bfc48d73beb9860a47d28b5b16b9c2a29b8.tar.xz
src/hero.mjs: Add buildServer()
-rw-r--r--src/db.mjs5
-rw-r--r--src/hero.mjs43
-rw-r--r--src/utils.mjs4
-rwxr-xr-xtests/cli-opts.sh2
-rw-r--r--tests/js/db.mjs33
-rw-r--r--tests/js/hero.mjs346
-rw-r--r--tests/js/utils.mjs33
7 files changed, 322 insertions, 144 deletions
diff --git a/src/db.mjs b/src/db.mjs
index fdfd899..d32df54 100644
--- a/src/db.mjs
+++ b/src/db.mjs
@@ -5,13 +5,10 @@ import url from "node:url";
import sqlite from "./sqlite.cjs";
+import { promisify } from "./utils.mjs";
import { runMigrations } from "./accretion.mjs";
-export const promisify = nativeFn => (...args) =>
- new Promise((resolve, reject) =>
- nativeFn(...args, (err, data) => err ? reject(err) : resolve(data)));
-
export const promisifyDb = dbHandle => ({
ref: dbHandle,
all: promisify((...args) => dbHandle.all(...args)),
diff --git a/src/hero.mjs b/src/hero.mjs
index a8532a8..38fdde4 100644
--- a/src/hero.mjs
+++ b/src/hero.mjs
@@ -1,8 +1,8 @@
-import assert from "node:assert";
+import assert from "node:assert/strict";
import crypto from "node:crypto";
import http from "node:http";
-import { assocIn, getIn, first, log } from "./utils.mjs";
+import { assocIn, getIn, first, log, promisify } from "./utils.mjs";
export const normalizeSegments = segments =>
segments.length === 1 && segments[0] === "" ?
@@ -50,13 +50,6 @@ export const addRoute = (table, methods, path, handlerFn) => {
);
};
-export const buildRoutes = routes =>
- routes.reduce(
- (acc, [methods, path, handlerFn]) =>
- addRoute(acc, methods, path, handlerFn),
- {}
- );
-
export const findStaticHandler = (table, method, segments) => {
const handlerFn = getIn(table, ["static", method].concat(segments));
return !handlerFn ? null : { handlerFn, params: {} };
@@ -217,4 +210,34 @@ export const chainInterceptors = arr =>
arr[0](req, chainInterceptors(arr.slice(1)));
export const wrapHandler = (fn, arr) =>
- chainInterceptors(arr.concat([ (req, _next) => fn(req) ]));
+ arr.length === 0 ?
+ fn :
+ chainInterceptors(arr.concat([ (req, _next) => fn(req) ]));
+
+export const buildRoutes = (routes, globalInterceptors = []) =>
+ routes.reduce(
+ (acc, [methods, path, handlerFn, interceptors = []]) =>
+ addRoute(
+ acc,
+ methods,
+ path,
+ wrapHandler(
+ handlerFn,
+ globalInterceptors.concat(interceptors),
+ ),
+ ),
+ {}
+ );
+
+export const promisifyServer = serverHandle => ({
+ ref: serverHandle,
+ listen: promisify((...args) => serverHandle.listen(...args)),
+ close: promisify((...args) => serverHandle.close(...args)),
+});
+
+export const buildServer = (routes, globalInterceptors = []) => {
+ const table = buildRoutes(routes, globalInterceptors);
+ const requestListener = makeRequestListener(table);
+ const server = http.createServer(requestListener);
+ return promisifyServer(server);
+};
diff --git a/src/utils.mjs b/src/utils.mjs
index e0e20a7..8ce9076 100644
--- a/src/utils.mjs
+++ b/src/utils.mjs
@@ -74,3 +74,7 @@ export const first = (arr, fn) => {
};
export const log = o => console.error(JSON.stringify(o));
+
+export const promisify = fn => (...args) =>
+ new Promise((resolve, reject) =>
+ fn(...args, (err, data) => err ? reject(err) : resolve(data)));
diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh
index a25b66c..b67ab11 100755
--- a/tests/cli-opts.sh
+++ b/tests/cli-opts.sh
@@ -1,4 +1,4 @@
#!/bin/sh
set -eu
-"$@" -V
+"$@" -V >&2
diff --git a/tests/js/db.mjs b/tests/js/db.mjs
index 372f64a..fcea535 100644
--- a/tests/js/db.mjs
+++ b/tests/js/db.mjs
@@ -3,39 +3,9 @@ import assert from "node:assert/strict";
import sqlite from "../../src/sqlite.cjs";
import { runTests } from "../runner.mjs";
-import { promisify, promisifyDb, open, db, init } from "../../src/db.mjs";
+import { promisifyDb, open, db, init } from "../../src/db.mjs";
-const test_promisify = t => {
- t.start("promisify()");
-
- t.test("we wrap the callbacky function", async () => {
- const okFn1 = (a, b, cb) =>
- setTimeout(() => cb(null, { a, b, ok: true }));
- const errFn1 = (a, b, c, cb) =>
- setTimeout(() => cb({ err: true, a }, "ignored"));
-
- const okFn2 = promisify(okFn1);
- const errFn2 = promisify(errFn1);
-
- assert.deepEqual(
- await okFn2("a-value", "b-value"),
- {
- a: "a-value",
- b: "b-value",
- ok: true,
- },
- );
-
- assert.rejects(
- async () => await errFn2("aa", "bb", "cc"),
- {
- err: true,
- a: "aa",
- },
- );
- });
-};
const test_promisifyDb = t => {
t.start("promisifyDb()");
@@ -128,7 +98,6 @@ const test_init = t => {
await runTests([
- test_promisify,
test_promisifyDb,
test_open,
test_init,
diff --git a/tests/js/hero.mjs b/tests/js/hero.mjs
index 09e6e54..7b8a5cc 100644
--- a/tests/js/hero.mjs
+++ b/tests/js/hero.mjs
@@ -1,4 +1,6 @@
import assert from "node:assert/strict";
+import http from "node:http";
+
import { runTests } from "../runner.mjs";
import { assocIn } from "../../src/utils.mjs";
import {
@@ -6,7 +8,6 @@ import {
pathToSegments,
hasPathParams,
addRoute,
- buildRoutes,
findStaticHandler,
firstParamMatch,
findDynamicHandler,
@@ -15,8 +16,12 @@ import {
handleRequest,
makeRequestListener,
interceptorsFn,
+ interceptors,
chainInterceptors,
wrapHandler,
+ buildRoutes,
+ promisifyServer,
+ buildServer,
} from "../../src/hero.mjs";
@@ -167,101 +172,6 @@ const test_addRoute = t => {
});
};
-const test_buildRoutes = t => {
- t.start("buildRoutes()");
-
- t.test("empty values", () => {
- assert.deepEqual(buildRoutes([]), {});
- });
-
- t.test("overwrites", () => {
- const fn1 = () => {};
- const fn2 = () => {};
-
- const r1 = [ [ "GET", "/", fn1 ] ];
- const r2 = [ [ "GET", "/", fn2 ] ];
-
- assert.deepEqual(
- buildRoutes(r1),
- {
- static: {
- GET: {
- "": fn1,
- },
- },
- },
- );
-
- assert.deepEqual(
- buildRoutes(r2),
- {
- static: {
- GET: {
- "": fn2,
- },
- },
- },
- );
-
- assert.deepEqual(
- buildRoutes(r1.concat(r2)),
- buildRoutes(r2),
- );
-
- assert.deepEqual(
- buildRoutes(r2.concat(r1)),
- buildRoutes(r1),
- );
- });
-
- t.test("multiple routes built", () => {
- const fn1 = () => {};
- const fn2 = () => {};
- const fn3 = () => {};
- const fn4 = () => {};
- const fn5 = () => {};
-
- const routes = [
- [ "GET", "/", fn1 ],
- [ "GET", "/home", fn2 ],
- [ "GET", "/user", fn3 ],
- [ "POST", "/user", fn4 ],
- [ "GET", "/user/:id", fn5 ],
- ];
-
- assert.deepEqual(
- buildRoutes(routes),
- {
- static: {
- GET: {
- "": fn1,
- home: {
- "": fn2,
- },
- user: {
- "": fn3,
- },
- },
- POST: {
- user: {
- "": fn4,
- },
- },
- },
- dynamic: {
- GET: {
- user: {
- ":id": {
- "": fn5,
- }
- }
- }
- },
- },
- );
- });
-};
-
const test_findStaticHandler = t => {
t.start("findStaticHandler()");
@@ -400,6 +310,22 @@ const test_firstParamMatch = t => {
{ handlerFn: fn1, params: { aaa: "someId" }},
);
});
+
+ t.test(`we deal with segments that start with ":"`, () => {
+ const segments = [ "path", ":param", "" ];
+ const tree = {
+ path: {
+ ":arg": {
+ "": fn1,
+ },
+ },
+ };
+
+ assert.deepEqual(
+ firstParamMatch(tree, segments, params),
+ { handlerFn: fn1, params: { arg: ":param" }},
+ );
+ });
};
const test_findDynamicHandler = t => {
@@ -973,6 +899,15 @@ const test_chainInterceptors = t => {
const test_wrapHandler = t => {
t.start("wrapHandler()");
+ t.test("noop when arr is empty", () => {
+ const fn = () => {};
+ const wrappedFn1 = wrapHandler(fn, []);
+ assert.deepEqual(fn, wrappedFn1);
+
+ const wrappedFn2 = wrapHandler(fn, [ interceptors.requestId ]);
+ assert.notDeepEqual(fn, wrappedFn2);
+ });
+
t.test("a handler with chained interceptors change its behaviour", async () => {
let i = 0;
const uuidFn = () => `${i++}`;
@@ -1030,13 +965,227 @@ const test_wrapHandler = t => {
});
};
+const test_buildRoutes = t => {
+ t.start("buildRoutes()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(buildRoutes([]), {});
+ });
+
+ t.test("overwrites", () => {
+ const fn1 = () => {};
+ const fn2 = () => {};
+
+ const r1 = [ [ "GET", "/", fn1 ] ];
+ const r2 = [ [ "GET", "/", fn2 ] ];
+
+ assert.deepEqual(
+ buildRoutes(r1),
+ {
+ static: {
+ GET: {
+ "": fn1,
+ },
+ },
+ },
+ );
+
+ assert.deepEqual(
+ buildRoutes(r2),
+ {
+ static: {
+ GET: {
+ "": fn2,
+ },
+ },
+ },
+ );
+
+ assert.deepEqual(
+ buildRoutes(r1.concat(r2)),
+ buildRoutes(r2),
+ );
+
+ assert.deepEqual(
+ buildRoutes(r2.concat(r1)),
+ buildRoutes(r1),
+ );
+ });
+
+ t.test("wrapped handler functions", async () => {
+ const handler = req => ({ ...req, handled: true });
+ const interceptor = (req, next) => next({ ...req, intercepted: true });
+
+ const routes = [
+ [ "GET", "/without", handler ],
+ [ "GET", "/with", handler, [ interceptor ] ],
+ ];
+
+ const table = buildRoutes(routes);
+
+ {
+ const { handled, intercepted } =
+ await handleRequest(table, "GET", "/without");
+ assert.deepEqual(
+ { handled, intercepted },
+ { handled: true, intercepted: undefined },
+ );
+ }
+ {
+ const { handled, intercepted } =
+ await handleRequest(table, "GET", "/with");
+ assert.deepEqual(
+ { handled, intercepted },
+ { handled: true, intercepted: true },
+ );
+ }
+ });
+
+ t.test("interceptors are combined", async () => {
+ const handler = req => ({ ...req, handled: true });
+ const interceptor1 = (req, next) => next({ ...req, interceptor1: true });
+ const interceptor2 = (req, next) => next({ ...req, interceptor2: true });
+
+ const routes = [
+ [ "GET", "/global-only", handler ],
+ [ "GET", "/global-and-local", handler, [ interceptor2 ] ],
+ ];
+
+ const table = buildRoutes(routes, [ interceptor1 ]);
+
+ {
+ const { handled, interceptor1, interceptor2 } =
+ await handleRequest(table, "GET", "/global-only");
+ assert.deepEqual(
+ { handled, interceptor1, interceptor2 },
+ { handled: true, interceptor1: true, interceptor2: undefined },
+ );
+ }
+ {
+ const { handled, interceptor1, interceptor2 } =
+ await handleRequest(table, "GET", "/global-and-local");
+ assert.deepEqual(
+ { handled, interceptor1, interceptor2 },
+ { handled: true, interceptor1: true, interceptor2: true },
+ );
+ }
+ });
+
+ t.test("multiple routes built", () => {
+ const fn1 = () => {};
+ const fn2 = () => {};
+ const fn3 = () => {};
+ const fn4 = () => {};
+ const fn5 = () => {};
+
+ const routes = [
+ [ "GET", "/", fn1 ],
+ [ "GET", "/home", fn2 ],
+ [ "GET", "/user", fn3 ],
+ [ "POST", "/user", fn4 ],
+ [ "GET", "/user/:id", fn5 ],
+ ];
+
+ assert.deepEqual(
+ buildRoutes(routes),
+ {
+ static: {
+ GET: {
+ "": fn1,
+ home: {
+ "": fn2,
+ },
+ user: {
+ "": fn3,
+ },
+ },
+ POST: {
+ user: {
+ "": fn4,
+ },
+ },
+ },
+ dynamic: {
+ GET: {
+ user: {
+ ":id": {
+ "": fn5,
+ }
+ }
+ }
+ },
+ },
+ );
+ });
+};
+
+const test_promisifyServer = t => {
+ t.start("promisifyServer()");
+
+ t.test("we can access the underlying server ref", () => {
+ const server = promisifyServer(http.createServer(() => {}));
+ assert.ok(server.ref instanceof http.Server);
+ });
+};
+
+const test_buildServer = t => {
+ t.start("buildServer()");
+
+ const socketRequest = (socketPath, path) =>
+ new Promise((resolve, reject) => {
+ const callback = res => {
+ let body = "";
+ res.on("data", chunk => body += chunk);
+ res.on("error", reject);
+ res.on("end", () => resolve({
+ body,
+ status: res.statusCode,
+ }));
+ };
+ const request = http.request({
+ socketPath,
+ path,
+ }, callback);
+ request.end();
+ });
+
+ t.test("empty values", async () => {
+ const socketPath = "./tests/hero-0.sock";
+ const server = buildServer([]);
+
+ await server.listen(socketPath);
+ const response = await socketRequest(socketPath, "/anything");
+ await server.close();
+
+ assert.deepEqual(response, { status: 404, body: "Not Found" });
+ });
+
+ t.test("integrated application server", async () => {
+ const socketPath = "./tests/hero-1.sock";
+ const pathHandler = req => ({ status: 200, body: "OK" });
+ const globalInterceptors = [
+ interceptors.serverError,
+ interceptors.contentType,
+ interceptors.requestId,
+ interceptors.logged,
+ ];
+ const routes = [ [ "GET", "/path", pathHandler ] ];
+ const server = buildServer(routes, globalInterceptors);
+
+ await server.listen(socketPath);
+ const response = await socketRequest(socketPath, "/path");
+ await server.close();
+
+ assert.deepEqual(response, { status: 200, body: "OK" });
+ });
+};
+
await runTests([
test_normalizeSegments,
test_pathToSegments,
test_hasPathParams,
test_addRoute,
- test_buildRoutes,
test_findStaticHandler,
test_firstParamMatch,
test_findDynamicHandler,
@@ -1047,4 +1196,7 @@ await runTests([
test_interceptorsFn,
test_chainInterceptors,
test_wrapHandler,
+ test_buildRoutes,
+ test_promisifyServer,
+ test_buildServer,
]);
diff --git a/tests/js/utils.mjs b/tests/js/utils.mjs
index 94e8eae..e4f21d0 100644
--- a/tests/js/utils.mjs
+++ b/tests/js/utils.mjs
@@ -9,6 +9,7 @@ import {
getIn,
first,
log,
+ promisify,
} from "../../src/utils.mjs";
const test_eq = t => {
@@ -258,6 +259,37 @@ const test_log = t => {
});
};
+const test_promisify = t => {
+ t.start("promisify()");
+
+ t.test("we wrap the callbacky function", async () => {
+ const okFn1 = (a, b, cb) =>
+ setTimeout(() => cb(null, { a, b, ok: true }));
+ const errFn1 = (a, b, c, cb) =>
+ setTimeout(() => cb({ err: true, a }, "ignored"));
+
+ const okFn2 = promisify(okFn1);
+ const errFn2 = promisify(errFn1);
+
+ assert.deepEqual(
+ await okFn2("a-value", "b-value"),
+ {
+ a: "a-value",
+ b: "b-value",
+ ok: true,
+ },
+ );
+
+ assert.rejects(
+ async () => await errFn2("aa", "bb", "cc"),
+ {
+ err: true,
+ a: "aa",
+ },
+ );
+ });
+};
+
await runTests([
test_eq,
@@ -267,4 +299,5 @@ await runTests([
test_getIn,
test_first,
test_log,
+ test_promisify,
]);