Skip to content

Loading…

Plugins #15

Open
wants to merge 16 commits into from

2 participants

@isaacs

This is a bunch of the Route/plugins refactoring that we were talking about. It's not ready for prime-time, but I want to check-in at this point to make sure we're not diverging.

Caveats:

  1. route.must('thing', handler) breaks the pattern of route.accepts(..., handler). It makes it look like the handler you pass to must is what gets set if you DO have the thing, when in fact, it's what gets set if you don't have it. So you'd do something like route.must('auth', 401) to say "You must auth, or else you get a 401 error".

  2. Something apparently is busted in filed when I try to use it for stuff, though all the tests pass. I'm seeing a lot of 'Not Implemented, lazy (future) dynamic read/write discovery,' errors. Also, it seems to set the 200 status code, even when a file is being used as a 404 error, which is weird and wrong.

  3. Error handling, omg, it's such a mess. There needs to be some consistent/intuitive way to say, "Yeah, I know what I told you before, but forget all that, this is an error now, deal with it", and then provide some handler to send the error contents. But, you probably want those error handlers to also do content type negotiation, so that means they really ought to be Routes rather than just handlers.

  4. routes.Route vs Route vs route.Router vs Router. This is extremely confusing and weird. I cleaned it up a bit by making the result of router.match() go into req.match rather than req.route, so req.route is always and only a reference to the tako route. But it's still really just kind of screwy still. I get why there needs to be two kinds of Route objects (since routes.Route objects are just a dumb little regexp-and-function thingie), but why is there two different kinds of Routers?

@mikeal
Owner

1) so basically, must is a bad word to use here, i'm open to other suggestions.

2) I thought i fixed that in the error handling code. might need to dig in to filed to make this work properly.

3) I take you mean mostly the global and route error handlers and not resp.error(). I get what you're saying, we already have per-route error handling that supports, strings, buffers, objects and a req/resp listener. I guess we could also support.

var err = app.route('/error').json().html()

app.route('/').error(err)

4) Internally, and externally, anything that refers to the routes package should follow a different naming convention. You're right, it's confusing, and having app.route() and req.route is enough to deal with. Also, this may get moved to director at some point.

@mikeal
Owner

how close is this to being done?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 23, 2012
  1. @isaacs

    Whitespace cleanup

    isaacs committed
  2. @isaacs
  3. @isaacs
  4. @isaacs

    s/_header/_headerSent/g

    isaacs committed
    The _header field can be set in a few ways that don't necessarily
    mean that it's been sent.  Also, for HTTP 0.9 requests, it should
    be set to an empty string, even when set (this is currently not
    handled correctly in node, but will be eventually.)
    
    Replace checks for _headerSent, which is a boolean indicating that
    the header portion of the request has actually been sent to the
    client, which is the intended semantics here.
  5. @isaacs
  6. @isaacs

    Abstract out the req/resp decoration from onRequest

    isaacs committed
    Also, mark onRequest as _private
  7. @isaacs

    Remove dead code

    isaacs committed
  8. @isaacs

    Move the route match data to req.match

    isaacs committed
    The next commit will make req.route a reference to the tako Route object
  9. @isaacs
  10. @isaacs
  11. @isaacs

    Run plugins before route handler

    isaacs committed
  12. @isaacs
  13. @isaacs
  14. @isaacs

    Cookies are delicious delicacies!

    isaacs committed
  15. @isaacs
  16. @isaacs

    Implement route.must()

    isaacs committed
Showing with 579 additions and 278 deletions.
  1. +376 −272 index.js
  2. +3 −1 package.json
  3. +3 −1 tests/test-basic.js
  4. +116 −0 tests/test-cookies.js
  5. +4 −4 tests/test-hostrouter.js
  6. +77 −0 tests/test-methods.js
View
648 index.js
@@ -16,6 +16,8 @@ var util = require('util')
, handlebars = require('./handlebars')
, rfc822 = require('./rfc822')
, io = null
+ , Cookies = require('cookies')
+ , Keygrip = require('keygrip')
;
try {
@@ -70,8 +72,8 @@ function BufferResponse (buffer, mimetype) {
this.cache = true
}
BufferResponse.prototype.request = function (req, resp) {
- if (resp._header) return // This response already started
- resp.setHeader('content-type', this.mimetype)
+ if (resp._headerSent) return // This response already started
+ if (this.mimetype) resp.setHeader('content-type', this.mimetype)
if (this.cache) {
resp.setHeader('last-modified', this.timestamp)
resp.setHeader('etag', this.etag)
@@ -80,7 +82,7 @@ BufferResponse.prototype.request = function (req, resp) {
resp.statusCode = 405
return (resp._end ? resp._end : resp.end).call(resp)
}
- if (this.cache &&
+ if (this.cache &&
req.headers['if-none-match'] === this.etag ||
req.headers['if-modified-since'] === this.timestamp
) {
@@ -128,7 +130,7 @@ function Page (templatename) {
}
})
}
-
+
if (templatename) {
self.template(templatename)
}
@@ -188,7 +190,7 @@ function Templates (app) {
util.inherits(Templates, events.EventEmitter)
Templates.prototype.get = function (name, cb) {
var self = this
-
+
if (name.indexOf(' ') !== -1 || name[0] === '<') {
process.nextTick(function () {
if (!self.tempcache[name]) {
@@ -198,7 +200,7 @@ Templates.prototype.get = function (name, cb) {
})
return
}
-
+
function finish () {
if (name in self.names) {
cb(null, self.files[self.names[name]])
@@ -227,7 +229,7 @@ Templates.prototype.directory = function (dir) {
self.loaded = true
if (self.loading === 0) self.emit('loaded')
})
-}
+}
function loadfiles (f, cb) {
var filesmap = {}
@@ -258,6 +260,40 @@ function loadfiles (f, cb) {
})
}
+// interpret cb as some sort of handler.
+//
+// route.json(function (req, res) { ... }) // you handle the json
+// route.json(404) // couldn't find your json.
+// route.json(410) // json is gone. stop asking.
+// route.json({ok: true}) // jsonify
+// route.json('{"ok":true}') // return this exact json string
+// route.json(new Buffer('{"ok":true}')) // return this exact json
+// route.json('/path/to/foo.json') // send this file
+// If none of these match, returns null, which means 'no handler'
+function makeHandler (cb) {
+ if (typeof cb === 'function' || (cb instanceof BufferResponse)) {
+ // already a valid handler
+ return cb
+ }
+
+ if (typeof cb === 'number') {
+ return function (req, res) {
+ return res.error(cb)
+ }
+ }
+ if (Buffer.isBuffer(cb)) {
+ return new BufferResponse(cb)
+ }
+ if (typeof cb === 'object') {
+ return new BufferResponse(JSON.stringify(cb), 'application/json')
+ }
+ if (typeof cb === 'string') {
+ if (cb[0] === '/') return filed(cb)
+ return new BufferResponse(cb)
+ }
+ return null
+}
+
function Application (options) {
var self = this
if (!options) options = {}
@@ -265,113 +301,26 @@ function Application (options) {
self.addHeaders = {}
if (self.options.logger) {
self.logger = self.options.logger
- }
-
- self.onRequest = function (req, resp) {
- if (self.logger.info) self.logger.info('Request', req.url, req.headers)
- // Opt out entirely if this is a socketio request
- if (self.socketio && req.url.slice(0, '/socket.io/'.length) === '/socket.io/') {
- return self._ioEmitter.emit('request', req, resp)
- }
-
- for (i in self.addHeaders) {
- resp.setHeader(i, self.addHeaders[i])
- }
-
- req.accept = function () {
- if (!req.headers.accept) return '*/*'
- var cc = null
- var pos = 99999999
- for (var i=arguments.length-1;i!==-1;i--) {
- var ipos = req.headers.accept.indexOf(arguments[i])
- if ( ipos !== -1 && ipos < pos ) cc = arguments[i]
- }
- return cc
- }
-
- resp.error = function (err) {
- if (typeof(err) === "string") err = {message: err}
- if (!err.statusCode) err.statusCode = 500
- resp.statusCode = err.statusCode || 500
- self.logger.log('error %statusCode "%message "', err)
- resp.end(err.message || err) // this should be better
- }
-
- resp.notfound = function (log) {
- if (log) self.logger.log(log)
- self.notfound(req, resp)
- }
-
- // Get all the parsed url properties on the request
- // This is the same style express uses and it's quite nice
- var parsed = url.parse(req.url)
- for (i in parsed) {
- req[i] = parsed[i]
- }
-
- if (req.query) req.qs = qs.parse(req.query)
-
- req.route = self.router.match(req.pathname)
-
- if (!req.route) return self.notfound(req, resp)
-
- req.params = req.route.params
-
- var onWrites = []
- resp._write = resp.write
- resp.write = function () {
- if (resp.statusCode === 404 && self._notfound) {
- return self._notfound.request(req, resp)
- }
- if (onWrites.length === 0) return resp._write.apply(resp, arguments)
- var args = arguments
- onWrites.forEach(function (onWrite) {
- var c = onWrite.apply(resp, args)
- if (c !== undefined) args[0] = c
- })
- return resp._write.apply(resp, args)
- }
-
- // Fix for node's premature header check in end()
- resp._end = resp.end
- resp.end = function (chunk) {
- if (resp.statusCode === 404 && self._notfound) {
- return self._notfound.request(req, resp)
- }
- if (chunk) resp.write(chunk)
- resp._end()
- self.logger.info('Response', resp.statusCode, req.url, resp._headers)
- }
-
- self.emit('request', req, resp)
-
-
- req.route.fn.call(req.route, req, resp, self.authHandler)
+ }
- if (req.listeners('body').length) {
- var buffer = ''
- req.on('data', function (chunk) {
- buffer += chunk
- })
- req.on('end', function (chunk) {
- if (chunk) buffer += chunk
- req.emit('body', buffer)
- })
- }
+ if (options.keys) {
+ self.keygrip = new Keygrip(options.keys)
}
+ self._plugins = {}
+
self.router = new routes.Router()
self.on('newroute', function (route) {
- self.router.addRoute(route.path, function (req, resp, authHandler) {
- route.handler(req, resp, authHandler)
+ self.router.addRoute(route.path, function (req, resp) {
+ req.route = route
})
})
-
+
self.templates = new Templates(self)
-
+
// Default to having json enabled
self.on('request', JSONRequestHandler)
-
+
// setup servers
self.http = options.http || {}
self.https = options.https || {}
@@ -383,39 +332,39 @@ function Application (options) {
} else if (options.socketio) {
throw new Error('socket.io is not available');
}
-
+
self.httpServer = http.createServer()
self.httpsServer = https.createServer(self.https)
-
- self.httpServer.on('request', self.onRequest)
- self.httpsServer.on('request', self.onRequest)
-
+
+ self.httpServer.on('request', self._onRequest.bind(self))
+ self.httpsServer.on('request', self._onRequest.bind(self))
+
var _listenProxied = false
var listenProxy = function () {
if (!_listenProxied && self._ioEmitter) self._ioEmitter.emit('listening')
_listenProxied = true
}
-
+
self.httpServer.on('listening', listenProxy)
self.httpsServer.on('listening', listenProxy)
-
+
if (io && self.socketio) {
// setup socket.io
self._ioEmitter = new events.EventEmitter()
-
+
self.httpServer.on('upgrade', function (request, socket, head) {
self._ioEmitter.emit('upgrade', request, socket, head)
})
self.httpsServer.on('upgrade', function (request, socket, head) {
self._ioEmitter.emit('upgrade', request, socket, head)
})
-
+
self.socketioManager = new io.Manager(self._ioEmitter, self.socketio)
self.sockets = self.socketioManager.sockets
}
-
+
if (!self.logger) {
- self.logger =
+ self.logger =
{ log: console.log
, error: console.error
, info: function () {}
@@ -424,6 +373,172 @@ function Application (options) {
}
util.inherits(Application, events.EventEmitter)
+Application.prototype._onRequest = function (req, resp) {
+ var self = this
+ if (self.logger.info) self.logger.info('Request', req.url, req.headers)
+ // Opt out entirely if this is a socketio request
+ if (self.socketio && req.url.slice(0, '/socket.io/'.length) === '/socket.io/') {
+ return self._ioEmitter.emit('request', req, resp)
+ }
+
+ self._decorate(req, resp)
+
+ if (!req.match) return self.notfound(req, resp)
+
+ // attach the route handler
+ req.match.fn(req, resp)
+
+ // like doing self.emit('request', req, resp)
+ // except that we abort if anyone takes over and starts
+ // sending the response.
+ var listeners = self.listeners('request')
+ for (var i = 0; i < listeners.length; i ++) {
+ listeners[i].call(self, req, resp)
+ if (resp._headerSent) return
+ }
+ var listeners = req.route.listeners('request')
+ for (var i = 0; i < listeners.length; i ++) {
+ listeners[i].call(req.route, req, resp)
+ if (resp._headerSent) return
+ }
+
+ // all the 'request' event handlers fired, and none
+ // of them sent a response. Apply plugins, and then
+ // let the route handle it.
+ cap(req)
+ self._plug(req, resp, function () {
+ // if any of those functions (plugins or event handlers)
+ // attached a 'body' listener, then set that up.
+ if (req.listeners('body').length) {
+ var buffer = ''
+ req.on('data', function (chunk) {
+ buffer += chunk
+ })
+ req.on('end', function (chunk) {
+ if (chunk) buffer += chunk
+ req.emit('body', buffer)
+ })
+ }
+
+ req.release()
+ // now do the handler dance
+ self._applyHandler(req, resp)
+ })
+}
+
+// apply the handler that is attached to the request.
+Application.prototype._applyHandler = function (req, resp) {
+ var h = req.handler || req.route.handler
+ if (!h) return this.notfound(req, resp)
+ if (h.request) {
+ return h.request(req, resp)
+ }
+ if (h.pipe) {
+ req.pipe(h)
+ h.pipe(resp)
+ return
+ }
+ h.call(req.route, req, resp)
+}
+
+Application.prototype.plugin = function (name, fn) {
+ if (this._plugins[name]) {
+ throw new Error('Plugin '+name+' already added')
+ }
+ this._plugins[name] = fn
+}
+
+Application.prototype._plug = function (req, resp, cb) {
+ var plugins = Object.keys(this._plugins)
+ var len = plugins.length
+ var self = this
+ if (!len) return cb()
+
+ plugins.forEach(function (p) {
+ self._plugins[p].call(self, req, resp, next(p))
+ })
+
+ function next (p) { return function (er) {
+ if (er) req.pluginErrors[p] = er
+ else req[p] = req[p] || true
+ req.emit('plugin:' + p)
+ if (-- len === 0) req.emit('pluginsDone')
+ }}
+}
+
+Application.prototype._decorate = function (req, resp) {
+ var self = this
+
+ for (i in self.addHeaders) {
+ resp.setHeader(i, self.addHeaders[i])
+ }
+
+ req.cookies = resp.cookies = new Cookies(req, resp, self.keygrip)
+
+ req.accept = function () {
+ if (!req.headers.accept) return '*/*'
+ var cc = null
+ var pos = 99999999
+ if (Array.isArray(arguments[0])) arguments = arguments[0]
+ for (var i=arguments.length-1;i!==-1;i--) {
+ var ipos = req.headers.accept.indexOf(arguments[i])
+ if ( ipos !== -1 && ipos < pos ) cc = arguments[i]
+ }
+ return cc
+ }
+
+ resp.error = function (err) {
+ if (typeof(err) === "number") {
+ err = {statusCode:err, message: http.STATUS_CODES[err]}
+ }
+ if (typeof(err) === "string") err = {message: err}
+ if (!err.statusCode) err.statusCode = 500
+ resp.statusCode = err.statusCode || 500
+ self.logger.log('error %statusCode "%message "', err)
+ resp.end(err.message || err) // this should be better
+ }
+
+ resp.notfound = function (log) {
+ if (log) self.logger.log(log)
+ self.notfound(req, resp)
+ }
+
+ // Get all the parsed url properties on the request
+ // This is the same style express uses and it's quite nice
+ var parsed = url.parse(req.url)
+ for (i in parsed) if (i !== 'auth') {
+ req[i] = parsed[i]
+ }
+
+ if (req.query) req.qs = qs.parse(req.query)
+
+ // warning: this is not a Route object!
+ // TODO: req.route should be a link to the actual Route object
+ // that the user has added musts and such to. If it doesn't match
+ // any routes, then the default handler should just attach a
+ // 'resp.error(404)' handler to it.
+ req.match = self.router.match(req.pathname)
+ req.route = req.match
+ if (!req.match) return
+
+ req.params = req.match.params
+ req.splats = req.match.splats
+ req.src = req.match.route
+
+ // Fix for node's premature header check in end()
+ resp._end = resp.end
+ resp.end = function (chunk) {
+ if (resp.statusCode === 404 && self._notfound) {
+ return self._notfound.request(req, resp)
+ }
+ if (chunk) resp.write(chunk)
+ resp._end()
+ self.logger.info('Response', resp.statusCode, req.url, resp._headerSent)
+ }
+}
+
+
+
Application.prototype.addHeader = function (name, value) {
this.addHeaders[name] = value
}
@@ -444,11 +559,12 @@ Application.prototype.listen = function (createServer, port, cb) {
port = createServer
}
self.server = createServer(function (req, resp) {
- self.onRequest(req, resp)
+ self._onRequest(req, resp)
})
self.server.listen(port, cb)
return this
}
+
Application.prototype.close = function (cb) {
var counter = 1
, self = this
@@ -474,6 +590,7 @@ Application.prototype.close = function (cb) {
end()
return self
}
+
Application.prototype.notfound = function (req, resp) {
if (!resp) {
if (typeof req === "string") {
@@ -495,13 +612,15 @@ Application.prototype.notfound = function (req, resp) {
this._notfound = req
return
}
-
- if (resp._header) return // This response already started
-
+
+ if (resp._headerSent) return // This response already started
+
if (this._notfound) return this._notfound.request(req, resp)
-
+
var cc = req.accept('text/html', 'application/json', 'text/plain', '*/*') || 'text/plain'
- if (cc === '*/*') cc = 'text/plain'
+ if (cc === '*/*') {
+ cc = 'text/plain'
+ }
resp.statusCode = 404
resp.setHeader('content-type', cc)
if (cc === 'text/html') {
@@ -513,16 +632,19 @@ Application.prototype.notfound = function (req, resp) {
}
resp.end(body)
}
+
Application.prototype.auth = function (handler) {
if (!handler) return this.authHandler
this.authHandler = handler
}
+
Application.prototype.page = function () {
var page = new Page()
, self = this
;
page.application = self
- page.template = function (name) {
+
+ page.template = function (name) {
var p = page.promise("template")
self.templates.get(name, function (e, template) {
if (e) return p(e)
@@ -531,7 +653,7 @@ Application.prototype.page = function () {
process.nextTick(function () {
var text = template.render(page.results)
page.dests.forEach(function (d) {
- if (d._header) return // Don't try to write to a response that's already finished
+ if (d._headerSent) return // Don't try to write to a response that's already finished
if (d.writeHead) {
d.statusCode = 200
d.setHeader('content-type', page.mimetype || 'text/html')
@@ -552,7 +674,7 @@ module.exports.JSONRequestHandler = JSONRequestHandler
function JSONRequestHandler (req, resp) {
var orig = resp.write
resp.write = function (chunk) {
- if (resp._header) return orig.call(this, chunk) // This response already started
+ if (resp._headerSent) return orig.call(this, chunk) // This response already started
// bail fast for chunks to limit impact on streaming
if (Buffer.isBuffer(chunk)) return orig.call(this, chunk)
// if it's an object serialize it and set proper headers
@@ -586,194 +708,173 @@ function Route (path, application) {
self.app = application
self.byContentType = {}
- var returnEarly = function (req, resp, keys, authHandler) {
- if (self._events && self._events['request']) {
- if (authHandler) {
- cap(req)
- authHandler(req, resp, function (user) {
- if (resp._header) return // This response already started
- req.user = user
- if (self._must && self._must.indexOf('auth') !== -1 && !req.user) {
- resp.statusCode = 403
- resp.setHeader('content-type', 'application/json')
- resp.end(JSON.stringify({error: 'This resource requires auth.'}))
- return
- }
- self.emit('request', req, resp)
- req.release()
- })
- } else {
- if (resp._header) return // This response already started
- if (self._must && self._must.indexOf('auth') !== -1 && !req.user) {
- resp.statusCode = 403
- resp.setHeader('content-type', 'application/json')
- resp.end(JSON.stringify({error: 'This resource requires auth.'}))
- return
- }
- self.emit('request', req, resp)
- }
- } else {
- if (resp._header) return // This response already started
- resp.statusCode = 406
- resp.setHeader('content-type', 'text/plain')
- resp.end('Request does not include a valid mime-type for this resource: '+keys.join(', '))
- }
+ self.handler = function (req, resp) {
+ // the default handler is just a 404
+ application.notfound(req, resp)
}
- self.handler = function (req, resp, authHandler) {
- if (self._methods && self._methods.indexOf(req.method) === -1) {
- resp.statusCode = 405
- resp.end('Method not Allowed.')
- return
+ application.emit('newroute', self)
+}
+
+util.inherits(Route, events.EventEmitter)
+
+Route.prototype.default = function (cb) {
+ this.handler = makeHandler(cb)
+}
+
+// r.methods('POST', 'PUT', uploadHandler).methods('GET', showIt)
+Route.prototype.methods = function () {
+ var methods = new Array(arguments.length)
+ for (var i = 0; i < methods.length; i ++) {
+ methods[i] = arguments[i]
+ }
+ if (typeof methods[methods.length-1] !== 'string') {
+ var handler = makeHandler(methods.pop())
+ }
+
+ this._methods = this._methods || []
+ this._methods = this._methods.concat(methods)
+
+ var self = this
+ return this.on('request', function (req, res) {
+ var method = req.method
+
+ // HEAD is just a bodiless GET
+ if (method === 'HEAD') method = 'GET'
+
+ if (self._methods.indexOf(req.method) === -1) {
+ res.error(405)
}
-
- self.emit('before', req, resp)
- if (self.authHandler) {
- authHandler = self.authHandler
+ if (handler && !req.handler && methods.indexOf(req.method) !== -1) {
+ // first hit! assign the desired handler.
+ // because of the !req.handler check, only the first one
+ // will take control.
+ req.handler = handler
}
+ })
+}
- var keys = Object.keys(self.byContentType).concat(['*/*'])
- if (keys.length) {
- if (req.method !== 'PUT' && req.method !== 'POST') {
- var cc = req.accept.apply(req, keys)
- } else {
- var cc = req.headers['content-type']
- }
+Route.prototype.del = function (cb) {
+ return this.methods('DELETE', makeHandler(cb))
+}
- if (!cc) return returnEarly(req, resp, keys, authHandler)
- if (cc === '*/*') {
- var h = this.byContentType[Object.keys(this.byContentType)[0]]
- } else {
- var h = this.byContentType[cc]
- }
- if (!h) return returnEarly(req, resp, keys, authHandler)
- if (resp._header) return // This response already started
- resp.setHeader('content-type', cc)
+Route.prototype.get = function (cb) {
+ return this.methods('GET', makeHandler(cb))
+}
- var run = function () {
- if (h.request) {
- return h.request(req, resp)
- }
- if (h.pipe) {
- req.pipe(h)
- h.pipe(resp)
- return
- }
- h.call(req.route, req, resp)
- }
+Route.prototype.put = function (cb) {
+ return this.methods('PUT', makeHandler(cb))
+}
- if (authHandler) {
- cap(req)
- authHandler(req, resp, function (user) {
- req.user = user
- if (self._must && self._must.indexOf('auth') !== -1 && !req.user) {
- if (resp._header) return // This response already started
- resp.statusCode = 403
- resp.setHeader('content-type', 'application/json')
- resp.end(JSON.stringify({error: 'This resource requires auth.'}))
- return
- }
- run()
- req.release()
- })
- } else {
- if (resp._header) return // This response already started
- if (self._must && self._must.indexOf('auth') !== -1) {
- resp.statusCode = 403
- resp.setHeader('content-type', 'application/json')
- resp.end(JSON.stringify({error: 'This resource requires auth.'}))
- return
- }
- run()
- }
+Route.prototype.post = function (cb) {
+ return this.methods('POST', makeHandler(cb))
+}
- } else {
- returnEarly(req, resp, keys, authHandler)
- }
+
+// r.accepts('application/json', sendJson).accepts('text/html', sendHTML)
+Route.prototype.accepts = function () {
+ var accepts = new Array(arguments.length)
+ for (var i = 0; i < accepts.length; i ++) {
+ accepts[i] = arguments[i]
}
- application.emit('newroute', self)
+ if (typeof accepts[accepts.length-1] !== 'string') {
+ var handler = makeHandler(accepts.pop())
+ }
+
+
+ this._accepts = this._accepts || []
+ this._accepts = this._accepts.concat(accepts)
+
+ var self = this
+ return this.on('request', function (req, res) {
+ var acc = req.accept(self._accepts.concat('*/*'))
+ if (!acc) {
+ res.error(406)
+ }
+ if (handler && !req.handler &&
+ (acc === '*/*' || accepts.indexOf(acc) !== -1)) {
+ // first hit! assign the desired handler.
+ // because of the !req.handler check, only the first one
+ // will take control.
+ res.setHeader('content-type', acc)
+ req.handler = handler
+ }
+ })
}
-util.inherits(Route, events.EventEmitter)
+
+
Route.prototype.json = function (cb) {
- if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'application/json')
- else if (typeof cb === 'object') cb = new BufferResponse(JSON.stringify(cb), 'application/json')
- else if (typeof cb === 'string') {
- if (cb[0] === '/') cb = filed(cb)
- else cb = new BufferResponse(cb, 'application/json')
- }
- this.byContentType['application/json'] = cb
- return this
+ return this.accepts('application/json', 'text/json', 'application/x-json', makeHandler(cb))
}
+
Route.prototype.html = function (cb) {
- if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'text/html')
- else if (typeof cb === 'string') {
- if (cb[0] === '/') cb = filed(cb)
- else cb = new BufferResponse(cb, 'text/html')
- }
- this.byContentType['text/html'] = cb
- return this
+ return this.accepts('text/html', makeHandler(cb))
}
+
Route.prototype.text = function (cb) {
- if (Buffer.isBuffer(cb)) cb = new BufferResponse(cb, 'text/plain')
- else if (typeof cb === 'string') {
- if (cb[0] === '/') cb = filed(cb)
- else cb = new BufferResponse(cb, 'text/plain')
- }
- this.byContentType['text/plain'] = cb
- return this
+ return this.accepts('text/plain', makeHandler(cb))
}
+
Route.prototype.file = function (filepath) {
this.on('request', function (req, resp) {
- var f = filed(filepath)
- req.pipe(f)
- f.pipe(resp)
+ req.handler = filed(filepath)
})
return this
}
+
Route.prototype.files = function (filepath) {
this.on('request', function (req, resp) {
- req.route.splats.unshift(filepath)
- var p = path.join.apply(path.join, req.route.splats)
+ req.match.splats.unshift(filepath)
+ var p = path.join.apply(path.join, req.match.splats)
if (p.slice(0, filepath.length) !== filepath) {
resp.statusCode = 403
return resp.end('Naughty Naughty!')
}
- var f = filed(p)
- req.pipe(f)
- f.pipe(resp)
+ req.handler = filed(p)
})
return this
}
-Route.prototype.auth = function (handler) {
- if (!handler) return this.authHandler
- this.authHandler = handler
- return this
-}
-Route.prototype.must = function () {
- this._must = Array.prototype.slice.call(arguments)
- return this
+
+Route.prototype.auth = function () {
+ return this.must('auth', 401)
}
-Route.prototype.methods = function () {
- this._methods = Array.prototype.slice.call(arguments)
+
+// must have auth, or else send a 401:
+// app.plugin('auth', authHandler)
+// app.route('/x').must('auth', 401)
+Route.prototype.must = function (thing, orelse) {
+ orelse = makeHandler(orelse)
+ return this.on('request', function (req, res) {
+ var when = req.app._plugins[thing] ? 'plugin:'+thing : 'pluginsDone'
+ req.on(when, function () {
+ if (!req[thing]) req.handler = orelse
+ })
+ })
return this
}
+
function ServiceError(msg) {
- Error.apply(this, arguments)
- this.message = msg
- this.stack = (new Error()).stack;
+ this.message = msg
+ Error.captureStackTrace(this, ServiceError);
}
-ServiceError.prototype = new Error()
-ServiceError.prototype.constructor = ServiceError
+util.inherits(ServiceError, Error)
ServiceError.prototype.name = 'ServiceError'
module.exports.ServiceError = ServiceError
+
+
+
+
+// XXX Used by tako.router(), but other instances of
+// 'Router' are from routes.Router. Can we get rid of
+// this somehow?
function Router (hosts, options) {
var self = this
self.hosts = hosts || {}
self.options = options || {}
-
+
function makeHandler (type) {
var handler = function (req, resp) {
var host = req.headers.host
@@ -790,22 +891,25 @@ function Router (hosts, options) {
}
return handler
}
-
+
self.httpServer = http.createServer()
self.httpsServer = https.createServer(self.options.ssl || {})
-
+
self.httpServer.on('request', makeHandler('request'))
self.httpsServer.on('request', makeHandler('request'))
-
+
self.httpServer.on('upgrade', makeHandler('upgrade'))
self.httpsServer.on('upgrade', makeHandler('upgrade'))
}
+
Router.prototype.host = function (host, app) {
this.hosts[host] = app
}
+
Router.prototype.default = function (app) {
this._default = app
}
+
Router.prototype.close = function (cb) {
var counter = 1
, self = this
@@ -824,13 +928,13 @@ Router.prototype.close = function (cb) {
self.httpsServer.once('close', end)
self.httpsServer.close()
}
-
+
for (i in self.hosts) {
counter++
process.nextTick(function () {
self.hosts[i].close(end)
})
-
+
}
end()
}
View
4 package.json
@@ -12,7 +12,9 @@
, "main" : "./index.html"
, "dependencies":
{ "filed":">= 0.0.6",
- "routes":"*"
+ "routes":"*",
+ "cookies":"0.2",
+ "keygrip":"0.2"
}
, "devDependencies": { "request": "2.9.x" }
, "scripts": { "test": "node tests/run.js" }
View
4 tests/test-basic.js
@@ -13,7 +13,7 @@ t
.html(function (req, resp) {
resp.end('<html><body>Hello World</body></html>')
})
- .on('request', function (req, resp) {
+ .default(function (req, resp) {
resp.statusCode = 200
resp.setHeader('content-type', 'text/plain')
resp.end('hello')
@@ -58,6 +58,7 @@ t.httpServer.listen(8000, function () {
request({url:url,headers:{'accept':'application/json'}}, function (e, resp, body) {
if (e) throw e
if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ console.error(resp.headers, body)
assert.equal(resp.headers['content-type'], 'application/json')
assert.equal(body, JSON.stringify({text:'hello world'}))
console.log('Passed json /')
@@ -105,6 +106,7 @@ t.httpServer.listen(8000, function () {
counter++
request({url:url+'static',headers:{'accept':'text/html'}}, function (e, resp, body) {
+ console.log('... html /static', resp.headers, body)
if (e) throw e
if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
assert.equal(resp.headers['content-type'], 'text/html')
View
116 tests/test-cookies.js
@@ -0,0 +1,116 @@
+var tako = require('../index')
+ , request = require('request')
+ , assert = require('assert')
+ , fs = require('fs')
+ , j = request.jar()
+ ;
+
+var t = tako({ keys: ["just testing"] })
+
+t
+ .route('/set')
+ .json(function (req, resp) {
+ resp.cookies.set("hello", "World", { signed: true })
+ resp.end({text:'Hello, World'})
+ })
+ .html(function (req, resp) {
+ resp.cookies.set("hello", "World", { signed: true })
+ resp.end('<html><body>Hello, World</body></html>')
+ })
+
+t
+ .route('/get')
+ .json(function (req, resp) {
+ assert(req.cookies.get("hello") === "World")
+ resp.end({text: "ok"})
+ })
+ .html(function (req, resp) {
+ assert(req.cookies.get("hello") === "World")
+ resp.end('<html><body>ok</body></html>')
+ })
+
+t
+ .route('/get-signed')
+ .json(function (req, resp) {
+ assert(req.cookies.get("hello", {signed: true}) === "World")
+ resp.end({text: "ok"})
+ })
+ .html(function (req, resp) {
+ assert(req.cookies.get("hello", {signed: true}) === "World")
+ resp.end('<html><body>ok</body></html>')
+ })
+
+
+var url = 'http://localhost:8000'
+var set = url + '/set'
+var get = url + '/get'
+var getSigned = url + '/get-signed'
+
+counter = 0
+
+function end (next) {
+ counter--
+ if (counter === 0) next ? next() : t.close()
+}
+
+t.httpServer.listen(8000, function () {
+ counter++
+ request({url:set,jar:j,headers:{'accept':'application/json'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'application/json')
+ assert.equal(body, JSON.stringify({text:'Hello, World'}))
+ console.log('Passed json /set')
+ end(testGet)
+ })
+
+ counter++
+ request({url:set,jar:j,headers:{'accept':'text/html'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'text/html')
+ assert.equal(body, '<html><body>Hello, World</body></html>')
+ console.log('Passed html /set')
+ end(testGet)
+ })
+
+ function testGet () {
+ counter++
+ request({url:get,jar:j,headers:{'accept':'application/json'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'application/json')
+ assert.equal(body, JSON.stringify({text:'ok'}))
+ console.log('Passed json /get')
+ end()
+ })
+
+ counter++
+ request({url:get,jar:j,headers:{'accept':'text/html'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'text/html')
+ console.log('Passed html /get')
+ end()
+ })
+
+ counter++
+ request({url:getSigned,jar:j,headers:{'accept':'application/json'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'application/json')
+ assert.equal(body, JSON.stringify({text:'ok'}))
+ console.log('Passed json /get-signed')
+ end()
+ })
+
+ counter++
+ request({url:getSigned,jar:j,headers:{'accept':'text/html'}}, function (e, resp, body) {
+ if (e) throw e
+ if (resp.statusCode !== 200) throw new Error('status code is not 200. '+resp.statusCode)
+ assert.equal(resp.headers['content-type'], 'text/html')
+ console.log('Passed html /get-signed')
+ end()
+ })
+ }
+})
View
8 tests/test-hostrouter.js
@@ -7,7 +7,7 @@ var tako = require('../index')
, router = tako.router()
, counter = 0
;
-
+
app1.route('/name').text('app1')
app2.route('/name').text('app2')
app3.route('/name').text('app3')
@@ -34,7 +34,7 @@ router.httpServer.listen(8080, function () {
assert.equal(resp.body, 'app1')
end()
})
-
+
counter++
request('http://localhost:8080/name', {headers:{host:'app2.localhost'}}, function (e, resp) {
assert.ok(!e)
@@ -42,7 +42,7 @@ router.httpServer.listen(8080, function () {
assert.equal(resp.body, 'app2')
end()
})
-
+
counter++
request('http://localhost:8080/name', {headers:{host:'unknown.localhost'}}, function (e, resp) {
assert.ok(!e)
@@ -50,4 +50,4 @@ router.httpServer.listen(8080, function () {
assert.equal(resp.body, 'app3')
end()
})
-})
+})
View
77 tests/test-methods.js
@@ -0,0 +1,77 @@
+var assert = require('assert')
+ , request = require('request')
+ , common = require('./common')
+ , tako = require('../')
+ , app = tako()
+
+var URL = common.URL
+ , HTML = '<h1>Hello world</h1>'
+ , getCalled = false
+ , putCalled = false
+ , postCalled = false
+
+app.route('/')
+ .get(function (req, res) {
+ console.error('GET handler called')
+ assert(!getCalled)
+ getCalled = true
+ res.end('ok')
+ })
+ .put(function (req, res) {
+ console.error('PUT handler called')
+ assert(!putCalled)
+ putCalled = true
+ res.end('ok')
+ })
+ .post(function (req, res) {
+ console.error('POST handler called')
+ assert(!postCalled)
+ postCalled = true
+ res.end('ok')
+ })
+ .default(function (req, res) {
+ console.error('default handler', req.method)
+ // should skip over this, because the 405 error took over.
+ throw new Error('should not be called ever')
+ })
+
+app.httpServer.listen(common.PORT, function () {
+ common.all(
+ [
+ [ function (cb) {
+ request.get({ url: URL }, cb)
+ }
+ , function (err, res, body) {
+ common.assertStatus(res, 200)
+ assert(getCalled)
+ }
+ ]
+ , [ function (cb) {
+ request.put({ url: URL, body:'ok' }, cb)
+ }
+ , function (err, res, body) {
+ common.assertStatus(res, 200)
+ assert(putCalled)
+ }
+ ]
+ , [ function (cb) {
+ request.del(URL, cb)
+ }
+ , function (err, res, body) {
+ // unsupported method
+ common.assertStatus(res, 405)
+ }
+ ]
+ , [ function (cb) {
+ request.post({ url: URL, body:'ok' }, cb);
+ }
+ ,
+ function (err, res, body) {
+ common.assertStatus(res, 200)
+ assert(postCalled)
+ }
+ ]
+ ]
+ , function () {app.close()})
+})
+
Something went wrong with that request. Please try again.