~mna/funweb

356ee7644df9244d296d9342c4462d2729e225dc — Martin Angers 1 year, 3 days 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 cors = require('cors')
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 @@ const pugHello = pug.compileFile(
  { 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 helloParams = R.pipe(
)
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
}