From f093235bd6117ae0af046258ae4bbbd505e22f26 Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Sat, 6 Oct 2012 14:44:55 -0700 Subject: [PATCH 1/7] add 3x support... Closes #55 --- Makefile | 5 +- Readme.md | 2 +- examples/content-negotiation.js | 55 +-- examples/{nesting.js => controllers.js} | 14 +- examples/controllers/main.js | 4 + examples/controllers/user.js | 67 +++ examples/root.js | 22 - examples/user.js | 50 --- index.js | 51 ++- package.json | 55 ++- test/resource.content-negotiation.test.js | 257 ++++++----- test/resource.test.js | 494 ++++++++-------------- test/support/batch.js | 13 + 13 files changed, 463 insertions(+), 626 deletions(-) rename examples/{nesting.js => controllers.js} (54%) create mode 100644 examples/controllers/main.js create mode 100644 examples/controllers/user.js delete mode 100644 examples/root.js delete mode 100644 examples/user.js create mode 100644 test/support/batch.js 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 ca673e5..07fe1e8 100644 --- a/Readme.md +++ b/Readme.md @@ -171,7 +171,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 6f58147..e5fd172 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; /** @@ -19,15 +22,21 @@ 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 + 'index' // GET / + , 'new' // GET /new + , 'create' // POST / + , 'show' // GET /:id + , 'edit' // GET /edit/:id + , 'update' // PUT /:id + , 'destroy' // DEL /:id ]; - + +/** + * Expose `Resource`. + */ + +module.exports = Resource; + /** * Initialize a new `Resource` with the given `name` and `actions`. * @@ -37,7 +46,7 @@ var orderedActions = [ * @api private */ -var Resource = module.exports = function Resource(name, actions, app) { +function Resource(name, actions, app) { this.name = name; this.app = app; this.routes = {}; @@ -49,7 +58,7 @@ var Resource = module.exports = function Resource(name, actions, app) { this.param = ':' + this.id; // 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]); } @@ -143,14 +152,12 @@ Resource.prototype.map = function(method, path, fn){ // apply the route this.app[method](route, 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); @@ -185,7 +192,12 @@ Resource.prototype.add = function(resource){ for (var key in routes) { route = routes[key]; delete routes[key]; - app[method](key).remove(); + 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); } } @@ -231,7 +243,7 @@ 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 = ''; @@ -249,8 +261,7 @@ express.router.methods.concat(['del', 'all']).forEach(function(method){ * @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 (options.id) actions.id = options.id; diff --git a/package.json b/package.json index 332f1c3..9ebb71a 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,36 @@ -{ "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", + "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", + "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" + ], + "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 From 118a9918223f97b438fc320709615ef1cd109c19 Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Sat, 6 Oct 2012 14:46:25 -0700 Subject: [PATCH 2/7] Release 1.0.0 --- History.md | 5 +++++ Readme.md | 3 ++- package.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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/Readme.md b/Readme.md index 07fe1e8..2641a40 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 diff --git a/package.json b/package.json index 9ebb71a..0a4afc2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-resource", "description": "Resourceful routing for express", - "version": "0.2.4", + "version": "1.0.0", "homepage": "https://github.com/visionmedia/express-resource", "author": "TJ Holowaychuk ", "contributors": [ From 6d03a3bbf68b75d347475fcb91e0776bfe176e8e Mon Sep 17 00:00:00 2001 From: Nicholas Poorman Date: Tue, 23 Oct 2012 20:23:56 -0400 Subject: [PATCH 3/7] fixed typo --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 2641a40..7122798 100644 --- a/Readme.md +++ b/Readme.md @@ -94,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 }); From f0d9c96aa26dfa86028b965d53ff01e9cc343b3f Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Wed, 27 Mar 2013 16:15:35 +0100 Subject: [PATCH 4/7] Express 3.x support I first merged the visionmedia master in order to get all updates from them. After this I reapplied the changes needed for express-resource-middleware. --- index.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 591df78..b7647bf 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,7 @@ var express = require('express') */ var orderedActions = [ - 'index' // GET / + 'index' // GET / , 'new' // GET /new , 'create' // POST / , 'show' // GET /:id @@ -31,9 +31,16 @@ var orderedActions = [ , 'destroy' // DEL /:id ]; -/** - * Expose `Resource`. - */ +var defaultMiddleware = { + 'index': null + , 'new': null + , 'create': null + , 'show': null + , 'edit': null + , 'update': null + , 'destroy': null + , '*': null +}; module.exports = Resource; @@ -46,11 +53,9 @@ module.exports = Resource; * @api private */ -function Resource(name, actions, app) { +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 || '/'; @@ -59,8 +64,12 @@ function Resource(name, actions, app) { 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]); } @@ -136,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')) { @@ -147,10 +156,13 @@ Resource.prototype.map = function(method, path, fnmap){ 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; } @@ -280,7 +292,7 @@ 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; } @@ -294,7 +306,6 @@ methods.concat(['del', 'all']).forEach(function(method){ * @return {Resource} * @api public */ - app.resource = function(name, actions, opts){ var options = actions || {}; if ('object' == typeof name) actions = name, name = null; @@ -304,4 +315,4 @@ app.resource = function(name, actions, opts){ for (var key in opts) options[key] = opts[key]; var res = this.resources[name] = new Resource(name, actions, this, opts); return res; -}; +}; \ No newline at end of file From ee02e9bc06f3a475057ee7841dfdb1b99cd93fb5 Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Wed, 27 Mar 2013 16:26:11 +0100 Subject: [PATCH 5/7] Fix package.json --- package.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0a4afc2..f4ef56f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "express-resource", + "name": "express-resource-middleware", "description": "Resourceful routing for express", "version": "1.0.0", "homepage": "https://github.com/visionmedia/express-resource", @@ -8,7 +8,15 @@ { "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", @@ -27,7 +35,8 @@ "keywords": [ "express", "rest", - "resource" + "resource", + "middleware" ], "main": "index", "engines": { From 9c9359df5d007d2cf9f140db7a4e662ba0856e4a Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Wed, 27 Mar 2013 16:27:07 +0100 Subject: [PATCH 6/7] Well - fix typo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4ef56f..e0a107e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ { "name": "Michael Weibel", "email": "michael.weibel@gmail.com" - }, + } ], "dependencies": { "lingo": ">= 0.0.4", From 802e18c89eb3d443e5d1721c7e0f329468ecfdca Mon Sep 17 00:00:00 2001 From: Michael Weibel Date: Thu, 28 Mar 2013 17:36:07 +0100 Subject: [PATCH 7/7] Fix call without a name --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index b7647bf..c4d654b 100644 --- a/index.js +++ b/index.js @@ -308,11 +308,11 @@ methods.concat(['del', 'all']).forEach(function(method){ */ 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); for (var key in opts) options[key] = opts[key]; var res = this.resources[name] = new Resource(name, actions, this, opts); return res; -}; \ No newline at end of file +};