diff --git a/History.md b/History.md index a32696c..45890e6 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,9 @@ +1.0.0 / 2012-10-06 +================== + + * add 3x support... Closes #55 + 0.2.4 / 2011-12-28 ================== diff --git a/Makefile b/Makefile index adbde61..4e9c8d3 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ test: - @./node_modules/expresso/bin/expresso \ - -I support + @./node_modules/.bin/mocha \ + --require should \ + --reporter spec .PHONY: test \ No newline at end of file diff --git a/Readme.md b/Readme.md index 1334429..7b31eb7 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,7 @@ # Express Resource - express-resource provides resourceful routing to express. + express-resource provides resourceful routing to express. For Express 2.x + use a version __below__ 1.0, for Express 3.x use 1.x. ## Installation @@ -93,7 +94,7 @@ Resources have the concept of "auto-loading" associated data. For example we can app.resource('users', { show: ..., load: User.load }); - With the auto-loader defined, the `req.user` object will be available now be available to the actions automatically. We may pass the "load" option as the third param as well, although this is equivalent to above, but allows you to either export ".load" along with your actions, or passing it explicitly: + With the auto-loader defined, the `req.user` object will now be available to the actions automatically. We may pass the "load" option as the third param as well, although this is equivalent to above, but allows you to either export ".load" along with your actions, or passing it explicitly: app.resource('users', require('./user'), { load: User.load }); @@ -232,7 +233,7 @@ Then run the tests: The MIT License - Copyright (c) 2010-2011 TJ Holowaychuk + Copyright (c) 2010-2012 TJ Holowaychuk Copyright (c) 2011 Daniel Gasienica Permission is hereby granted, free of charge, to any person obtaining diff --git a/examples/content-negotiation.js b/examples/content-negotiation.js index a7cb3e4..e513479 100644 --- a/examples/content-negotiation.js +++ b/examples/content-negotiation.js @@ -1,54 +1,13 @@ -require.paths.unshift(__dirname + '/../support'); - -/** - * Module dependencies. - */ - var express = require('express') - , resource = require('../') - , app = express.createServer(); - -var db = ['tobi', 'loki', 'jane'] - , toys = ['ball', 'tunnel']; - -var pet = { - index: { - json: function(req, res){ - res.send(db); - }, - - default: function(req, res){ - res.send(db.join(', '), { 'Content-Type': 'text/plain' }); - } - } -}; - -var pets = app.resource('pets', pet); - -pets.load(function(id, fn){ - fn(null, db[id]); -}); - -// GET /pets/toys.xml -// this action must be defined above -// the one below as the :pet placeholder -// will otherwise match "/toys". - -pets.get('/toys', { - xml: function(req, res){ - res.send('' + toys.map(function(toy){ - return '' + toy + ''; - }).join('\n') + ''); - } -}); + , resource = require('..') + , app = express(); -// GET /pets/1.xml +var users = app.resource('users', require('./controllers/user')); -pets.get({ - xml: function(req, res){ - res.send('' + req.pet + ''); - } +app.get('/', function(req, res){ + res.send('View users'); }); -app.listen(3000); \ No newline at end of file +app.listen(3000); +console.log('Listening on :3000'); \ No newline at end of file diff --git a/examples/nesting.js b/examples/controllers.js similarity index 54% rename from examples/nesting.js rename to examples/controllers.js index c937946..9292fcb 100644 --- a/examples/nesting.js +++ b/examples/controllers.js @@ -1,16 +1,12 @@ -require.paths.unshift(__dirname + '/../support'); - -/** - * Module dependencies. - */ - var express = require('express') - , resource = require('../') - , app = express.createServer(); + , resource = require('..') + , app = express(); +var main = app.resource(require('./controllers/main')); var forums = app.resource('forums', require('./controllers/forum')); var threads = app.resource('threads', require('./controllers/thread')); forums.add(threads); -app.listen(3000); \ No newline at end of file +app.listen(3000); +console.log('Listening on :3000'); \ No newline at end of file diff --git a/examples/controllers/main.js b/examples/controllers/main.js new file mode 100644 index 0000000..1958a59 --- /dev/null +++ b/examples/controllers/main.js @@ -0,0 +1,4 @@ + +exports.index = function(req, res){ + res.send('main index'); +} \ No newline at end of file diff --git a/examples/controllers/user.js b/examples/controllers/user.js new file mode 100644 index 0000000..85bccf6 --- /dev/null +++ b/examples/controllers/user.js @@ -0,0 +1,67 @@ + +var users = []; + +users.push({ name: 'tobi' }); +users.push({ name: 'loki' }); +users.push({ name: 'jane' }); + +/** + * GET index. + */ + +exports.index = { + html: function(req, res){ + res.send('

Users

' + + users.map(function(user, id){ + return '' + user.name + ''; + }).join('\n')); + }, + + json: function(req, res){ + res.send(users); + }, + + xml: function(req, res){ + res.send(users.map(function(user){ + return '' + user.name + '' + }).join('')); + } +}; + +/** + * POST create. + */ + +exports.create = function(req, res){ + res.send('created'); +}; + +/** + * GET show. + */ + +exports.show = { + html: function(req, res){ + res.send('

' + req.user.name + '

'); + }, + + json: function(req, res){ + res.send(req.user); + }, + + xml: function(req, res){ + res.send('' + req.user.name + ''); + } +}; + +/** + * Auto-load user re-source for actions. + */ + +exports.load = function(id, fn){ + var user = users[id]; + if (!user) return fn(new Error('not found')); + process.nextTick(function(){ + fn(null, user); + }); +} \ No newline at end of file diff --git a/examples/root.js b/examples/root.js deleted file mode 100644 index 2571f53..0000000 --- a/examples/root.js +++ /dev/null @@ -1,22 +0,0 @@ - -require.paths.unshift(__dirname + '/../support'); - -/** - * Module dependencies. - */ - -var express = require('express') - , resource = require('../') - , app = express.createServer(); - -app.resource({ - index: function(req, res){ - res.send('index page'); - }, - - show: function(req, res){ - res.send('item ' + req.params.id); - } -}); - -app.listen(3000); \ No newline at end of file diff --git a/examples/user.js b/examples/user.js deleted file mode 100644 index f98cf49..0000000 --- a/examples/user.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Module dependencies. - */ - -var express = require('express') - , resource = require('../') - , app = express.createServer(); - -var users = ['tobi', 'loki', 'jane']; - -var user = { - index: function(req, res){ - switch (req.format) { - case 'json': - res.send(users); - break; - default: - res.contentType('txt'); - res.send(users.join(', ')); - } - }, - - show: function(req, res){ - var user = users[req.params.user]; - res.send(user); - }, - - edit: function(req, res){ - res.send('editing ' + req.params.user); - }, - - destroy: function(req, res){ - delete users[req.params.user]; - res.send('removed ' + req.params.user); - }, - - login: function(req, res){ - res.send('logged in ' + req.params.user); - }, - - logout: function(req, res){ - res.send('logged out'); - } -}; - -var userResource = app.resource('users', user); -userResource.map('get', 'login', user.login); // relative path accesses element (/users/1/login) -userResource.map('get', '/logout', user.logout); // absolute path accesses collection (/users/logout) - -app.listen(3000); \ No newline at end of file diff --git a/index.js b/index.js index d6f3e73..c4d654b 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /*! * Express - Resource - * Copyright(c) 2010-2011 TJ Holowaychuk + * Copyright(c) 2010-2012 TJ Holowaychuk * Copyright(c) 2011 Daniel Gasienica * MIT Licensed */ @@ -11,7 +11,10 @@ */ var express = require('express') + , methods = require('methods') + , debug = require('debug')('express-resource') , lingo = require('lingo') + , app = express.application , en = lingo.en; /** @@ -20,28 +23,26 @@ var express = require('express') var orderedActions = [ 'index' // GET / - ,'new' // GET /new - ,'create' // POST / - ,'show' // GET /:id - ,'edit' // GET /edit/:id - ,'update' // PUT /:id - ,'destroy' // DEL /:id + , 'new' // GET /new + , 'create' // POST / + , 'show' // GET /:id + , 'edit' // GET /edit/:id + , 'update' // PUT /:id + , 'destroy' // DEL /:id ]; var defaultMiddleware = { 'index': null - ,'new': null - ,'create': null - ,'show': null - ,'edit': null - ,'update': null - ,'destroy': null - ,'*': null + , 'new': null + , 'create': null + , 'show': null + , 'edit': null + , 'update': null + , 'destroy': null + , '*': null }; -function isArray(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; -} +module.exports = Resource; /** * Initialize a new `Resource` with the given `name` and `actions`. @@ -52,11 +53,9 @@ function isArray(obj) { * @api private */ -var Resource = module.exports = function Resource(name, actions, app, opts) { +function Resource(name, actions, app, opts) { this.name = name; this.app = app; - this.options = opts || {}; - this.middleware = this.options.middleware || defaultMiddleware; this.routes = {}; actions = actions || {}; this.base = actions.base || '/'; @@ -65,8 +64,12 @@ var Resource = module.exports = function Resource(name, actions, app, opts) { this.id = actions.id || this.defaultId; this.param = ':' + this.id; + this.options = opts || {}; + this.middleware = this.options.middleware || defaultMiddleware; + + // default actions - for (var i=0, key; i < orderedActions.length; i++) { + for (var i = 0, key; i < orderedActions.length; i++) { key = orderedActions[i]; if (actions[key]) this.mapDefaultAction(key, actions[key]); } @@ -97,7 +100,7 @@ Resource.prototype.load = function(fn){ req[id] = obj; next(); }; - + // Maintain backward compatibility if (2 == fn.length) { fn(req.params[id], callback); @@ -142,7 +145,7 @@ Resource.prototype.map = function(method, path, fnmap){ if ('function' == typeof fnmap) { middleware = []; fn = fnmap; - } else if (isArray(fnmap) && (fnmap.length > 1) && ('0' in Object(fnmap))) { + } else if (Array.isArray(fnmap) && (fnmap.length > 1) && ('0' in Object(fnmap))) { middleware = fnmap.slice(0, fnmap.length-1); fn = fnmap[fnmap.length-1]; } else if (('object' == typeof fnmap) && fnmap.hasOwnProperty('fn')) { @@ -151,12 +154,15 @@ Resource.prototype.map = function(method, path, fnmap){ } else { middleware = []; fn = fnmap; - } + } - if (isArray(fn) && (fn.length > 1) && ('0' in Object(fn)) && (middleware.length == 0)) { + if (Array.isArray(fn) && (fn.length > 1) && ('0' in Object(fn)) && (middleware.length == 0)) { middleware = middleware.concat(fn.slice(0, fn.length-1)); fn = fn[fn.length-1]; } else if (('object' == typeof fn) && fn.hasOwnProperty('fn') && fn.hasOwnProperty('middleware')) { + if (!Array.isArray(middleware)) { + middleware = [middleware]; + } middleware = middleware.concat(fn.middleware); fn = fn.fn; } @@ -189,14 +195,12 @@ Resource.prototype.map = function(method, path, fnmap){ // apply the route this.app[method](route, middleware, function(req, res, next){ req.format = req.params.format || req.format || self.format; - if (req.format) res.contentType(req.format); + if (req.format) res.type(req.format); if ('object' == typeof fn) { - if (req.format && fn[req.format]) { + if (fn[req.format]) { fn[req.format](req, res, next); - } else if (fn.default) { - fn.default(req, res, next); } else { - res.send(406); + res.format(fn); } } else { fn(req, res, next); @@ -231,8 +235,13 @@ Resource.prototype.add = function(resource){ for (var key in routes) { route = routes[key]; delete routes[key]; - app[method](key).remove(); - resource.map(route.method, route.orig, route.fnmap); + if (method == 'del') method = 'delete'; + app.routes[method].forEach(function(route, i){ + if (route.path == key) { + app.routes[method].splice(i, 1); + } + }) + resource.map(route.method, route.orig, route.fn); } } @@ -279,11 +288,11 @@ Resource.prototype.mapDefaultAction = function(key, fn){ * Setup http verb methods. */ -express.router.methods.concat(['del', 'all']).forEach(function(method){ +methods.concat(['del', 'all']).forEach(function(method){ Resource.prototype[method] = function(path, fn){ if ('function' == typeof path || 'object' == typeof path) fn = path, path = ''; - var middleware = this.middleware[method] || []; + var middleware = this.middleware[method] || []; //?? this.map(method, path, fn); return this; } @@ -297,11 +306,9 @@ express.router.methods.concat(['del', 'all']).forEach(function(method){ * @return {Resource} * @api public */ - -express.HTTPServer.prototype.resource = -express.HTTPSServer.prototype.resource = function(name, actions, opts){ +app.resource = function(name, actions, opts){ var options = actions || {}; - if ('object' == typeof name) actions = name, name = null; + if ('object' == typeof name) opts = actions, actions = name, name = null; if (options.id) actions.id = options.id; this.resources = this.resources || {}; if (!actions) return this.resources[name] || new Resource(name, null, this, opts); diff --git a/package.json b/package.json index 332f1c3..e0a107e 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,45 @@ -{ "name": "express-resource" - , "description": "Resourceful routing for express" - , "version": "0.2.4" - , "homepage": "https://github.com/visionmedia/express-resource" - , "author": "TJ Holowaychuk " - , "contributors": [ - { "name": "Daniel Gasienica", "email": "daniel@gasienica.ch" } - ] - , "dependencies": { "lingo": ">= 0.0.4" } - , "devDependencies": { - "connect": "1.8.x" - , "express": "2.5.x" - , "ejs": "0.4.x" - , "expresso": "0.9.x" - , "qs": "0.1.x" - , "should": "*" +{ + "name": "express-resource-middleware", + "description": "Resourceful routing for express", + "version": "1.0.0", + "homepage": "https://github.com/visionmedia/express-resource", + "author": "TJ Holowaychuk ", + "contributors": [ + { + "name": "Daniel Gasienica", + "email": "daniel@gasienica.ch" + }, + { + "name": "Marco Pantaleoni", + "email": "marco.pantaleoni@gmail.com" + }, + { + "name": "Michael Weibel", + "email": "michael.weibel@gmail.com" + } + ], + "dependencies": { + "lingo": ">= 0.0.4", + "methods": "0.0.1", + "debug": "*" + }, + "devDependencies": { + "connect": "2.x", + "express": "3.x", + "ejs": "0.4.x", + "qs": "0.1.x", + "mocha": "*", + "should": "*", + "supertest": "0.1.2" + }, + "keywords": [ + "express", + "rest", + "resource", + "middleware" + ], + "main": "index", + "engines": { + "node": "*" } - , "keywords": ["express", "rest", "resource"] - , "main": "index" - , "engines": { "node": ">= 0.2.0" } -} \ No newline at end of file +} diff --git a/test/resource.content-negotiation.test.js b/test/resource.content-negotiation.test.js index c6d8d90..702bf79 100644 --- a/test/resource.content-negotiation.test.js +++ b/test/resource.content-negotiation.test.js @@ -1,158 +1,137 @@ -/** - * Module dependencies. - */ var assert = require('assert') , express = require('express') - , should = require('should') - , Resource = require('../'); + , request = require('supertest') + , batch = require('./support/batch') + , Resource = require('..'); -module.exports = { - 'test content-negotiation via extension': function(){ - var app = express.createServer(); - - app.resource('pets', require('./fixtures/pets'), { format: 'json' }); - - assert.response(app, - { url: '/pets.html' }, - { body: 'Not Acceptable' - , status : 406 }); - - assert.response(app, - { url: '/pets' }, - { body: '["tobi","jane","loki"]' }); - - assert.response(app, - { url: '/pets.xml' }, - { body: 'tobijaneloki' - , headers: { 'Content-Type': 'application/xml' }}); - - assert.response(app, - { url: '/pets.json' }, - { body: '["tobi","jane","loki"]' - , headers: { 'Content-Type': 'application/json; charset=utf-8' }}); - - assert.response(app, - { url: '/pets/1.json' }, - { body: '"jane"' }); - - assert.response(app, - { url: '/pets/0.xml' }, - { body: 'tobi' }); - - assert.response(app, - { url: '/pets/0.xml', method: 'DELETE' }, - { body: 'pet removed' }); - - assert.response(app, - { url: '/pets/0.json', method: 'DELETE' }, - { body: '{"message":"pet removed"}' }); - }, - - 'test content-negotiation via format method': function(){ - var app = express.createServer(); - +describe('app.resource()', function(){ + it('should support content-negotiation via extension', function(done){ + var app = express(); + var next = batch(done); + + app.set('json spaces', 0); + app.resource('pets', require('./fixtures/pets')); + + request(app) + .get('/pets.html') + .expect(406, next()); + + request(app) + .get('/pets.json') + .expect('["tobi","jane","loki"]', next()); + + request(app) + .get('/pets.xml') + .expect('tobijaneloki', next()); + + request(app) + .get('/pets/1.json') + .expect('"jane"', next()); + + request(app) + .get('/pets/0.xml') + .expect('tobi', next()); + + request(app) + .del('/pets/0.xml') + .expect('pet removed', next()); + + request(app) + .del('/pets/0.json') + .expect('{"message":"pet removed"}', next()); + }) + + it('should support format methods', function(done){ + var app = express(); + var next = batch(done); + app.set('json spaces', 0); app.resource('pets', require('./fixtures/pets.format-methods')); - assert.response(app, - { url: '/pets.xml' }, - { body: 'tobijaneloki' - , headers: { 'Content-Type': 'application/xml' }}); - - assert.response(app, - { url: '/pets.json' }, - { body: '["tobi","jane","loki"]' - , headers: { 'Content-Type': 'application/json; charset=utf-8' }}); - - assert.response(app, - { url: '/pets' }, - { body: 'Unsupported format', status: 406 }); - }, - - 'test content-negotiation via format method without default': function(){ - var app = express.createServer(); - - app.resource('pets', require('./fixtures/pets.format-methods-without-default')); - - assert.response(app, - { url: '/pets.xml' }, - { body: 'tobijaneloki' - , headers: { 'Content-Type': 'application/xml' }}); - - assert.response(app, - { url: '/pets.json' }, - { body: '["tobi","jane","loki"]' - , headers: { 'Content-Type': 'application/json; charset=utf-8' }}); - - assert.response(app, - { url: '/pets' }, - { body: 'Not Acceptable', status: 406 }); - }, - - 'test content-negotiation via map()': function(){ - var app = express.createServer(); + request(app) + .get('/pets.xml') + .expect('tobijaneloki', next()); + request(app) + .get('/pets.json') + .expect('["tobi","jane","loki"]', next()); + + request(app) + .get('/pets') + .expect('["tobi","jane","loki"]', next()); + }) +}) + +describe('app.VERB()', function(){ + it('should map additional routes', function(done){ + var app = express(); + + app.set('json spaces', 0); app.use(express.bodyParser()); - - var pets = app.resource('pets') - , toys = app.resource('toys') - , toysDB = ["balls", "platforms", "tunnels"]; - toys.get('/types', function(req, res){ - res.send(toysDB); - }); + var pets = app.resource('pets'); + var toys = app.resource('toys'); + var values = ['balls', 'platforms', 'tunnels']; - toys.get('/', { - json: function(req, res){ - res.send(toysDB); - } - }); - - toys.get({ - json: function(req, res){ - res.send('"' + toysDB[req.params.toy] + '"'); - } + toys.get('/types', function(req, res){ + res.send(values); }); - - pets.add(toys); - - pets.get('/', { + + request(app) + .get('/toys/types') + .expect('["balls","platforms","tunnels"]', done); + }) + + it('should map format objects', function(done){ + var app = express(); + var next = batch(done); + + app.set('json spaces', 0); + app.use(express.bodyParser()); + + var toys = app.resource('toys'); + var values = ['balls', 'platforms', 'tunnels']; + + toys.get('/types', { json: function(req, res){ - res.send({ name: 'tobi' }); + res.send(values); + }, + + 'text/plain': function(req, res){ + res.send(values.join('\n')); + }, + + xml: function(req, res){ + res.send(''); } }); - - assert.response(app, - { url: '/pets/0/toys/types' }, - { body: '["balls","platforms","tunnels"]' }); - assert.response(app, - { url: '/pets/0/toys/2.json' }, - { body: '"tunnels"' }); + request(app) + .get('/toys/types') + .expect('["balls","platforms","tunnels"]', next()); + + request(app) + .get('/toys/types') + .set('Accept', 'text/*') + .expect('balls\nplatforms\ntunnels', next()); + + request(app) + .get('/toys/types.xml') + .expect('', next()); + }) +}) + +describe('Resource#add(resource)', function(){ + it('should support nested resources', function(done){ + var app = express(); + app.set('json spaces', 0); - assert.response(app, - { url: '/pets/0/toys.json' }, - { body: '["balls","platforms","tunnels"]' }); - - assert.response(app, - { url: '/pets.json' }, - { body: '{"name":"tobi"}' }); - - assert.response(app, - { url: '/pets' }, - { status: 406 }); - }, - - 'test nested content-negotiation': function(){ - var app = express.createServer() - , pets = ['tobi', 'jane', 'loki']; - var users = app.resource('users'); var pets = app.resource('pets', require('./fixtures/pets')); users.add(pets); - - assert.response(app, - { url: '/users/1/pets.json' }, - { body: '["tobi","jane","loki"]' }); - } -}; \ No newline at end of file + + request(app) + .get('/users/1/pets.json') + .expect('["tobi","jane","loki"]', done); + }) +}) \ No newline at end of file diff --git a/test/resource.test.js b/test/resource.test.js index 89668fa..4693f62 100644 --- a/test/resource.test.js +++ b/test/resource.test.js @@ -1,332 +1,196 @@ -/** - * Module dependencies. - */ var assert = require('assert') , express = require('express') - , should = require('should') - , Resource = require('../'); + , Resource = require('..') + , request = require('supertest') + , batch = require('./support/batch'); -module.exports = { - 'test app.resource()': function(){ - var app = express.createServer(); - - var ret = app.resource('forums', require('./fixtures/forum')); - ret.should.be.an.instanceof(Resource); - - assert.response(app, - { url: '/forums' }, - { body: 'forum index' }); - - assert.response(app, - { url: '/forums/new' }, - { body: 'new forum' }); - - assert.response(app, - { url: '/forums', method: 'POST' }, - { body: 'create forum' }); - - assert.response(app, - { url: '/forums/5' }, - { body: 'show forum 5' }); - - assert.response(app, - { url: '/forums/5/edit' }, - { body: 'edit forum 5' }); - - assert.response(app, - { url: '/forums/5', method: 'PUT' }, - { body: 'update forum 5' }); - - assert.response(app, - { url: '/forums/5', method: 'DELETE' }, - { body: 'destroy forum 5' }); - }, - - 'test top-level app.resource()': function(){ - var app = express.createServer(); - - var ret = app.resource(require('./fixtures/forum'), { id: 'forum' }); - ret.should.be.an.instanceof(Resource); - - assert.response(app, - { url: '/' }, - { body: 'forum index' }); - - assert.response(app, - { url: '/new' }, - { body: 'new forum' }); - - assert.response(app, - { url: '/', method: 'POST' }, - { body: 'create forum' }); - - assert.response(app, - { url: '/5' }, - { body: 'show forum 5' }); - - assert.response(app, - { url: '/5/edit' }, - { body: 'edit forum 5' }); - - assert.response(app, - { url: '/5', method: 'PUT' }, - { body: 'update forum 5' }); - - assert.response(app, - { url: '/5', method: 'DELETE' }, - { body: 'destroy forum 5' }); - }, - - 'test app.resource() id option': function(){ - var app = express.createServer(); - - app.resource('users', { - id: 'uid', - show: function(req, res){ - res.send(req.params.uid); - } - }); - - assert.response(app, - { url: '/users' }, - { status: 404 }); - - assert.response(app, - { url: '/users/10' }, - { body: '10' }); - }, - - 'test fetching a resource object': function(){ - var app = express.createServer(); +describe('app.resource(name)', function(){ + it('should return a pre-defined resource', function(){ + var app = express(); app.resource('users', { index: function(){} }); app.resource('users').should.be.an.instanceof(Resource); app.resource('foo').should.be.an.instanceof(Resource); - }, - - 'test http methods': function(){ - var app = express.createServer(); - - var users = app.resource('users'); + }) +}) - users.get('/', function(req, res){ - res.send('index'); - }).get('/online', function(req, res){ - res.send('users online'); - }); +describe('app.resource()', function(){ + it('should map CRUD actions', function(done){ + var app = express(); + var next = batch(done); - users.get(function(req, res){ - res.send(req.params.user); - }).get('online', function(req, res){ - res.send('no'); - }); - - assert.response(app, - { url: '/users/online' }, - { body: 'users online' }); - - assert.response(app, - { url: '/users' }, - { body: 'index' }); - - assert.response(app, - { url: '/users/0' }, - { body: '0' }); + var ret = app.resource('forums', require('./fixtures/forum')); + ret.should.be.an.instanceof(Resource); - assert.response(app, - { url: '/users/0/online' }, - { body: 'no' }); - }, + request(app) + .get('/forums') + .expect('forum index', next()); + + request(app) + .get('/forums/new') + .expect('new forum', next()); + + request(app) + .post('/forums') + .expect('create forum', next()); + + request(app) + .get('/forums/5') + .expect('show forum 5', next()); + + request(app) + .get('/forums/5/edit') + .expect('edit forum 5', next()); + + request(app) + .del('/forums/5') + .expect('destroy forum 5', next()); + }) + + it('should support root resources', function(done){ + var app = express(); + var next = batch(done); + var forum = app.resource(require('./fixtures/forum')); + var thread = app.resource('threads', require('./fixtures/thread')); + forum.map(thread); - 'test shallow nesting': function(){ - var app = express.createServer(); - - var forum = app.resource('forums', require('./fixtures/forum')); - var thread = app.resource('threads', require('./fixtures/thread')); - forum.map(thread); - - assert.response(app, - { url: '/forums' }, - { body: 'forum index' }); - - assert.response(app, - { url: '/forums/12' }, - { body: 'show forum 12' }); - - assert.response(app, - { url: '/forums/12/threads' }, - { body: 'thread index of forum 12' }); - - assert.response(app, - { url: '/forums/1/threads/50' }, - { body: 'show thread 50 of forum 1' }); - }, + request(app) + .get('/') + .expect('forum index', next()); - 'test deep nesting': function(){ - var app = express.createServer(); - - var user = app.resource('users', { index: function(req, res){ res.end('users'); } }); - var forum = app.resource('forums', require('./fixtures/forum')); - var thread = app.resource('threads', require('./fixtures/thread')); - - var ret = user.add(forum); - ret.should.equal(user); - - var ret = forum.add(thread); - ret.should.equal(forum); - - assert.response(app, - { url: '/forums/20' }, - { status: 404 }); - - assert.response(app, - { url: '/users' }, - { body: 'users' }); - - assert.response(app, - { url: '/users/5/forums' }, - { body: 'forum index' }); - - assert.response(app, - { url: '/users/5/forums/12' }, - { body: 'show forum 12' }); - - assert.response(app, - { url: '/users/5/forums/12/threads' }, - { body: 'thread index of forum 12' }); - - assert.response(app, - { url: '/users/5/forums/1/threads/50' }, - { body: 'show thread 50 of forum 1' }); - }, - - 'test root nesting': function(){ - var app = express.createServer(); - - var forum = app.resource(require('./fixtures/forum')); - var thread = app.resource('threads', require('./fixtures/thread')); - forum.map(thread); - - assert.response(app, - { url: '/' }, - { body: 'forum index' }); - - assert.response(app, - { url: '/12' }, - { body: 'show forum 12' }); - - assert.response(app, - { url: '/12/threads' }, - { body: 'thread index of forum 12' }); - - assert.response(app, - { url: '/1/threads/50' }, - { body: 'show thread 50 of forum 1' }); - }, - - 'test shallow auto-loading': function(){ - var app = express.createServer(); - var Forum = require('./fixtures/forum').Forum; - - var actions = { show: function(req, res){ - res.end(req.forum.title); - }}; - - actions.load = Forum.get; - var forum = app.resource('forum', actions); - - assert.response(app, - { url: '/forum/12' }, - { body: 'Ferrets' }); - }, + // request(app) + // .get('/12') + // .expect('show forum 12', next()); - 'test deep auto-loading': function(){ - var app = express.createServer(); - var Forum = require('./fixtures/forum').Forum - , Thread = require('./fixtures/thread').Thread; - - var actions = { show: function(req, res){ - res.end(req.forum.title + ': ' + req.thread.title); - }}; - - var forum = app.resource('forum', { load: Forum.get }); - var threads = app.resource('thread', actions, { load: Thread.get }); - - forum.add(threads); - - assert.response(app, - { url: '/forum/12/thread/1' }, - { body: 'Ferrets: Tobi rules' }); - }, + // request(app) + // .get('/12/threads') + // .expect('thread index of forum 12', next()); - 'test .load(fn)': function(){ - var app = express.createServer(); - var Forum = require('./fixtures/forum').Forum; - - var actions = { show: function(req, res){ - res.end(req.forum.title); - }}; - - var forum = app.resource('forum', actions); - forum.load(Forum.get); - - assert.response(app, - { url: '/forum/12' }, - { body: 'Ferrets' }); - }, + // request(app) + // .get('/1/threads/50') + // .expect('show thread 50 of forum 1', next()); + }) - 'test auto-loading no resource': function(){ - var app = express.createServer(); - - function load(id, fn) { fn(); } - var actions = { show: function(){ - assert.fail('called show when loader failed'); - }}; - - app.resource('pets', actions, { load: load }); - - assert.response(app, - { url: '/pets/0' }, - { body: 'Not Found', status: 404 }); - }, + describe('"id" option', function(){ + it('should allow overriding the default', function(done){ + var app = express(); + var next = batch(done); + + app.resource('users', { + id: 'uid', + show: function(req, res){ + res.send(req.params.uid); + } + }); - 'test custom route configuration': function(){ - var app = express.createServer(); - var Forum = require('./fixtures/forum').Forum; - - function load(id, fn) { fn(null, "User"); } - var actions = { - login: function(req, res){ - res.end('login'); - }, - logout: function(req, res){ - res.end('logout'); - } - }; - - var users = app.resource('users', actions, { load: load }); - users.map('get', 'login', actions.login); - users.map('get', '/logout', actions.logout); - - assert.response(app, - { url: '/users/1/login' }, - { body: 'login' }); - - assert.response(app, - { url: '/users/logout' }, - { body: 'logout' }); - }, + request(app) + .get('/users') + .expect(404, next()); - 'test several segments': function(){ - var app = express.createServer(); - var cat = app.resource('api/cat', require('./fixtures/cat')); - - assert.response(app, - { url: '/api/cat' }, - { body: 'list of cats' }); - - assert.response(app, - { url: '/api/cat/new' }, - { body: 'new cat' }); - } -}; \ No newline at end of file + request(app) + .get('/users/10') + .expect('10', next()); + }) + }) + + describe('with several segments', function(){ + it('should work', function(done){ + var app = express(); + var next = batch(done); + var cat = app.resource('api/cat', require('./fixtures/cat')); + + request(app) + .get('/api/cat') + .expect('list of cats', next()); + + request(app) + .get('/api/cat/new') + .expect('new cat', next()); + }) + }) + + it('should allow configuring routes', function(done){ + var app = express(); + var next = batch(done); + var Forum = require('./fixtures/forum').Forum; + + function load(id, fn) { fn(null, "User"); } + + var actions = { + login: function(req, res){ + res.end('login'); + }, + logout: function(req, res){ + res.end('logout'); + } + }; + + var users = app.resource('users', actions, { load: load }); + users.map('get', 'login', actions.login); + users.map('get', '/logout', actions.logout); + + request(app) + .get('/users/1/login') + .expect('login', next()); + + request(app) + .get('/users/logout') + .expect('logout', next()); + }) + + describe('autoloading', function(){ + describe('when no resource is found', function(){ + it('should not invoke the callback', function(done){ + var app = express(); + + function load(id, fn) { fn(); } + var actions = { show: function(){ + assert.fail('called show when loader failed'); + }}; + + app.resource('pets', actions, { load: load }); + + request(app) + .get('/pets/0') + .expect(404, done); + }) + }) + + describe('when a resource is found', function(){ + it('should invoke the callback', function(done){ + var app = express(); + var Forum = require('./fixtures/forum').Forum; + + var actions = { show: function(req, res){ + res.end(req.forum.title); + }}; + + var forum = app.resource('forum', actions); + forum.load(Forum.get); + + request(app) + .get('/forum/12') + .expect('Ferrets', done); + }) + + it('should work recursively', function(done){ + var app = express(); + var Forum = require('./fixtures/forum').Forum; + var Thread = require('./fixtures/thread').Thread; + + var actions = { show: function(req, res){ + res.end(req.forum.title + ': ' + req.thread.title); + }}; + + var forum = app.resource('forum', { load: Forum.get }); + var threads = app.resource('thread', actions, { load: Thread.get }); + + forum.add(threads); + + request(app) + .get('/forum/12/thread/1') + .expect('Ferrets: Tobi rules', done); + }) + }) + }) +}) \ No newline at end of file diff --git a/test/support/batch.js b/test/support/batch.js new file mode 100644 index 0000000..0e9ec5c --- /dev/null +++ b/test/support/batch.js @@ -0,0 +1,13 @@ + +module.exports = function(done) { + var pending = 0; + var finished = false; + return function(){ + ++pending; + return function(err){ + if (finished) return; + if (err) return finished = true, done(err); + --pending || done(); + } + } +}; \ No newline at end of file