diff options
author | EuAndreh <eu@euandre.org> | 2024-02-23 14:40:24 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-02-23 14:40:24 -0300 |
commit | 21be0bfc48d73beb9860a47d28b5b16b9c2a29b8 (patch) | |
tree | 17eccfccb7c5d54d65f6c133cd3585f983175c95 | |
parent | Implement accretion.runMigrations() and wrappings of node-sqlite3 (diff) | |
download | papod-21be0bfc48d73beb9860a47d28b5b16b9c2a29b8.tar.gz papod-21be0bfc48d73beb9860a47d28b5b16b9c2a29b8.tar.xz |
src/hero.mjs: Add buildServer()
-rw-r--r-- | src/db.mjs | 5 | ||||
-rw-r--r-- | src/hero.mjs | 43 | ||||
-rw-r--r-- | src/utils.mjs | 4 | ||||
-rwxr-xr-x | tests/cli-opts.sh | 2 | ||||
-rw-r--r-- | tests/js/db.mjs | 33 | ||||
-rw-r--r-- | tests/js/hero.mjs | 346 | ||||
-rw-r--r-- | tests/js/utils.mjs | 33 |
7 files changed, 322 insertions, 144 deletions
@@ -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, ]); |