hotcoffee
Version:
Brew you some hot micro servers
268 lines (231 loc) • 7.48 kB
text/coffeescript
http = require 'http'
URL = require 'url'
qs = require 'querystring'
path = require 'path'
bunyan = require 'bunyan'
EventEmitter = require('events').EventEmitter
class Hotcoffee extends EventEmitter
constructor: (config)->
=
'get': .bind @
'post': .bind @
'patch': .bind @
'put': .bind @
'delete': .bind @
# default hooks
= []
# default output formats
default_output = (res)->
result = res.result.map (item)->
item.href = [res.endpoint, item.type, 'id', item.props.id].join '/' unless item.href?
return item
output = {
success: true
items: result
href: res.endpoint + res.req.url
}
output.success = false if String(res.statusCode).match /^5|^4/
res.setHeader 'Content-Type', 'application/json'
str = JSON.stringify(output, null, 2) + '\n'
res.end str
=
json: default_output
'application/json': default_output
config
init: (={}, done)->
= .log || bunyan.createLogger name: 'hotcoffee'
= .process or process
.setMaxListeners 0
.port = .env.PORT or ?.port or 1337
.host = ?.host or 'localhost'
.endpoint = .env.ENDPOINT or ?.endpoint or "http://#{@config.host}:#{@config.port}"
= {} # in-memory db
= http.createServer .bind @
= {} # list of plugins
.once 'exit', .bind @
.once 'SIGINT', .bind @
'error', (err)=>
.error err.message
'init',
return done(null) if done?
# plugins
use: (fn, opts)=>
plugin = fn @, opts
[plugin.name] = plugin
plugin
'use', fn, opts
return @
# content negotiation
accept: (formats)=>
# merge with default outputs
[key] = value for key, value of formats
return @
isRoot: (url)-> url == '/'
onExit: -> 'exit'
onSIGINT: ->
.exit(0)
merge: (dest, source)->
for key, value of dest.props
dest.props[key] = source[key] if source[key]?
for key, value of source
dest.props[key] = source[key]
writeHead: (res)->
res.setHeader 'Access-Control-Allow-Origin', '*'
register: (resource)->
[resource] = []
parseURL: (url)->
x = URL.parse(url).pathname.split('/')
x.shift() # remove first empty string element
ext = url
if ext?
[rest..., last] = x
x[x.length-1] = last.split('.')[0]
return x
filterResult: (result, filterBy)->
{resource, key, value} = filterBy
result = result.filter((x) -> x.props[key]?) if key? and key.length > 0
result = result.filter((x) -> String(x.props[key]) == decodeURIComponent(String(value))) if value?
return result
parseBody: (req, res, next)->
contentType = req.headers['content-type']
body = ''
req.on 'data', (data)->
body += data
req.on 'end', ->
if contentType == 'application/json'
body = JSON.parse body
if contentType == 'application/x-www-form-urlencoded'
body = qs.parse body
for k, v of body
try
body[k] = JSON.parse v
catch error
req.body = body
next null, body
onGET: (req, res)->
'GET', req, res
[ resource, key, value ] = req.url
result = []
if req.url
result = ({ type:'resource', href:[res.endpoint, name].join('/'), props: { name: name } } for name of )
else
if [resource]?
result = [resource], { resource: resource, key: key, value: value}
res.result = result
res
onPOST: (req, res)->
[ resource, key, value ] = req.url
[resource] ?= []
req.links ?= []
if resource != ""
item = { type: resource, props: req.body, links: req.links }
[resource].push item
res.result = [item]
res
'POST', req, res
else
res
onPATCH: (req, res)->
[ resource, key, value ] = req.url
[resource] ?= []
result = [resource], { resource: resource, key: key, value: value }
k, req.body for k in result
res.result = result
res
'PATCH', req, res
onPUT: (req, res)->
[ resource, key, value ] = req.url
[resource] ?= []
result = [resource], { resource: resource, key: key, value: value }
for item in result
item.links.push link for link in req.body.links if req.body.links?
res.result = result
res
'PUT', req, res
onDELETE: (req, res)->
[ resource, key, value ] = req.url
[resource] ?= []
result = [] # deleted items
if resource? and not key?
# delete collection
result = [resource].slice(0) # clone array
[resource] = []
else
# delete items
[resource] = [resource].filter((x) -> (String(x.props[key]) != decodeURIComponent(String(value))) or (!result.push x))
res.result = result
res
'DELETE', req, res
extendResponse: (req, res)->
res.req = req
res.endpoint = .endpoint
getExtension: (url)->
x = path.extname(URL.parse(url).pathname).split('.')
return x[1] if x.length > 1
return null
getResource: (url)->
[resource] = url
return resource
mapResult: (res)->
if ?
extension = res.req.extension
accept = res.req.headers['accept']
format = 'json'
if [extension]
format = extension
else if [accept]
format = accept
[format] res
render: (res)->
res
'render', res
hook: (fn)->
.push fn
runHooks: (req, res, arr, done)->
return done() if arr.length is 0
arr.shift() req, res, (err)=>
return done err if err
req, res, arr, done
onRequest: (req, res)->
res
req, res
req.resource = req.url
req.extension = req.url
res.result = []
method = req.method.toLowerCase()
.info req.method, req.url
req, res, [].concat(...), (err)=>
if err
res.statusCode = if err.statusCode? then err.statusCode else 500
res.result = [ { type: 'error', props: { message: err.message } } ]
res
'error', err
else
if [method]?
[method] req, res
else
err = new Error('Method not supported.')
res.end err.message
'error', err
'request', req, res
start: (done)->
.listen .port, =>
.info {port: .port}, "server started"
'start'
done() if done?
return @
stop: ->
.close()
'stop'
return @
logPluginEvents: (plugin)->
unless plugin.on?
.warn {plugin: plugin.name}, "not an event emitter"
return
plugin.on 'info', (args...) =>
.info {plugin: plugin.name}, args...
plugin.on 'error', (args...)=>
.error {plugin: plugin.name}, args...
module.exports = (config)->
return new Hotcoffee config