356ee7644df9244d296d9342c4462d2729e225dc — Martin Angers 4 months ago 2274849 master
more refactoring
5 files changed, 67 insertions(+), 42 deletions(-)

M lib/funweb.js
M lib/logger.js
M lib/mwadapt.js
A lib/params.js
A lib/route.js
M lib/funweb.js => lib/funweb.js +40 -28
@@ 7,21 7,23 @@ const pug = require('pug')
  const logger = require('./logger')
  const adapt = require('./mwadapt')
+ const { prefixRouter } = require('./route')
+ 
+ // funweb: functional http server.
+ // Canonical middleware function signature:
+ // (next, ctx) => ...
+ // where next takes ctx, and ctx has at least two properties,
+ // req and res, corresponding to the stdlib request and response
+ // objects in an HTTP server.
  
  // need some automatic generation of forms/html from code (can be fully controlled with templates or jsx, or auto-generated using a distinct library)
- // TODO: make the canonical middleware signature (next, req, res) so it can be curried more easily? Or even (next, [req, res])?
  
  // status and body helper
  const handler = (status, body) => R.pipe(
-   R.last,
+   R.prop('res'),
    R.invoker(1, 'writeHead')(status),
    R.invoker(1, 'end')(body)
  )
- const notFound = handler(404, 'not found')
- 
- // static file serving
- const serve = serveStatic(path.join('testdata', 'pub'))
- const serveOrNotFound = (req, res) => serve(req, res, R.thunkify(notFound)(req, res))
  
  // template serving
  const tpldir = path.join('testdata', 'tpl')


@@ 30,8 32,7 @@ { basedir: tpldir }
  )
  const helloParams = R.pipe(
-   R.head,
-   R.prop('url'),
+   R.path(['req', 'url']),
    R.split('/'),
    R.filter(R.pipe(R.isEmpty, R.not)),
    R.nth(1),


@@ 40,29 41,40 @@ )
  const helloBody = R.pipe(helloParams, pugHello)
  
+ // static file serving
+ const pubdir = path.join('testdata', 'pub')
+ const serve = adapt(serveStatic(pubdir))
+ const notFound = handler(404, 'not found')
+ 
  // map routes to handlers
- const route = (url) => R.pipe(R.head, R.prop('url'), R.startsWith(url))
- const routes = R.cond([
-   [route('/hello'), ([req, res]) => handler(200, helloBody(req))(req, res)],
-   [route('/nothello'), handler(200, 'NOT HELLO!')],
-   [R.T, serveOrNotFound]
- ])
+ const routes = prefixRouter({
+   '/hello': (ctx) => handler(200, helloBody(ctx))(ctx),
+   '/nothello': handler(200, 'NOT HELLO!'),
+   '/throw': (ctx) => { throw new Error('delibearte throw') },
+   '': serve(notFound)
+ })
  
- /*
  // middleware
  const compress = adapt(compression())
- console.log(compress.length)
- // TODO: configure CORS properly for the application's needs
  const corsFn = adapt(cors())
- const app = R.pipe(R.pair, corsFn(compress(logger(routes))))
- */
+ const recover = (err, ctx) => {
+   console.trace(`error=${err}`)
+   ctx.res.writeHead(500).end()
+ }
  
- // start server
- const logAddress = R.pipe(
-   R.invoker(0, 'address'),
-   R.props(['address', 'port']),
-   R.join(':'),
-   (addr) => console.log(`listening on ${addr}...`)
+ const middleware = logger(
+   R.tryCatch(
+     corsFn(compress(routes)),
+     recover
+   )
  )
- const server = http.createServer(R.pipe(R.pair, R.curry(logger)(routes)))
- server.listen(3000, '127.0.0.1', () => logAddress(server))
+ 
+ // start server
+ const app = R.pipe(R.pair, R.zipObj(['req', 'res']), middleware)
+ const server = http.createServer(app)
+ 
+ const logAddress = (server) => {
+   const addr = server.address()
+   console.log(`listening on ${addr.address}:${addr.port}...`)
+ }
+ server.listen(9000, '127.0.0.1', () => logAddress(server))

M lib/logger.js => lib/logger.js +4 -4
@@ 1,12 1,12 @@ const R = require('ramda')
  
- function logger (next, [req, res]) {
+ function logger (next, ctx) {
    const start = Date.now()
-   const val = next([req, res])
+   const val = next(ctx)
    const end = Date.now()
    // TODO: should take a Writable stream as argument
-   console.log(`url=${req.url} status=${res.statusCode} duration=${end - start}ms`)
+   console.log(`url=${ctx.req.url} status=${ctx.res.statusCode} duration=${end - start}ms`)
    return val
  }
  
- module.exports = logger
+ module.exports = R.curry(logger)

M lib/mwadapt.js => lib/mwadapt.js +4 -10
@@ 1,14 1,8 @@ const R = require('ramda')
  
- function middleware (fn, next, [req, res]) {
-   return fn(req, res, R.thunkify(next)(req, res))
- }
- 
  // takes a fn that accepts the usual connect middleware arguments
  // (req, res, next), where next is nullary, and converts it to the
- // (next, [req, res]) signature, as a curried function.
- function adapt (fn) {
-   return R.curry(middleware)(fn)
- }
- 
- module.exports = adapt
+ // (next, ctx) signature, as a curried function.
+ module.exports = (fn) => R.curry(
+   (next, ctx) => fn(ctx.req, ctx.res, R.thunkify(next)(ctx))
+ )

A lib/params.js => lib/params.js +1 -0
@@ 0,0 1,1 @@
+ const R = require('ramda')

A lib/route.js => lib/route.js +18 -0
@@ 0,0 1,18 @@
+ const R = require('ramda')
+ 
+ // prefixRouter takes obj (Object) where keys correspond to
+ // a path that must match literally as a prefix of the requested
+ // url, and an associated handler function as value, which must
+ // be a unary function expecting a ctx.
+ // TODO: order of keys not guaranteed, but important :/
+ function prefixRouter (obj) {
+   const route = (url) => R.pipe(R.path(['req', 'url']), R.startsWith(url))
+   const toCond = (key) => [ route(key), obj[key] ]
+   return R.cond(R.map(toCond, R.keys(obj)))
+ }
+ 
+ // TODO: regex/express style router
+ 
+ module.exports = {
+   prefixRouter
+ }