diff --git a/index.js b/index.js index ef3befe..0dd2b29 100644 --- a/index.js +++ b/index.js @@ -118,6 +118,9 @@ Resource.prototype.map = function(method, path, fn){ route += path; route += '.:format?'; + // add convenience functions + this.createRouteHelper(path, route); + // register the route so we may later remove it (this.routes[method] = this.routes[method] || {})[route] = { method: method @@ -125,7 +128,7 @@ Resource.prototype.map = function(method, path, fn){ , orig: orig , fn: fn }; - + // apply the route this.app[method](route, function(req, res, next){ req.format = req.params.format || self.format; @@ -142,7 +145,7 @@ Resource.prototype.map = function(method, path, fn){ fn(req, res, next); } }); - + return this; }; @@ -173,6 +176,11 @@ Resource.prototype.add = function(resource){ resource.map(route.method, route.orig, route.fn); } } + + // delete previous route helpers + for(var methodName in resource.paths) { + delete resourceAccess.path[methodName]; + } return this; }; @@ -211,6 +219,66 @@ Resource.prototype.mapDefaultAction = function(key, fn){ } }; +/** + * Create URL path generators. + * + * @param {String} orig + * @api private + */ + +Resource.prototype.createRouteHelper = function(mapPath, route) { + resourceAccess.path = resourceAccess.path || {}; + resourceAccess.path.idField = resourceAccess.path.idField || 'id'; + + var methodName = this.name || "roots"; + var argCount = 0; + + route = route.replace(/\.:format\?$/, ''); + route = route.replace(/\/null/, ''); // Happens with a nameless top level resource + + // Check to see how many levels deep we are and clean up the route + if(this.base.length > 1) { + // build up in reverse + var params = this.base.match(/:(\w+)/g); + params.reverse().forEach(function(param) { + if(param === ':id') { param = ':root'; } + methodName = param.slice(1) + '_' + methodName; + argCount += 1; + }); + } + + if(mapPath === 'new') { + methodName = 'new_' + en.singularize(methodName); + } else if(mapPath.indexOf(this.param) !== (-1)) { + var actionName = mapPath.slice(this.param.length); + if (actionName[0] == '/') { actionName = actionName.slice(1); } + + methodName = en.singularize(methodName); + if(actionName.length > 0) { + methodName = actionName + '_' + methodName; + } + argCount++; + } else if (mapPath.length > 0) { + // custom collection action + methodName = mapPath + '_' + methodName; + } + + // Add paths to this resource so we can remove them later if we become nested (see #add) + this.paths = this.paths || []; + this.paths.push(methodName); + + // Add to the app.resource object + resourceAccess.path[methodName + '_path'] = resourceAccess.path[methodName] || function() { + var localRoute = route; + Array.prototype.forEach.call(arguments, function(arg) { + var id = arg[resourceAccess.path.idField] || arg; + localRoute = localRoute.replace(/:\w+/, id); + }); + return localRoute; + }; +} + + /** * Setup http verb methods. */ @@ -233,6 +301,7 @@ express.router.methods.concat(['del', 'all']).forEach(function(method){ * @api public */ +var resourceAccess = express.HTTPServer.prototype.resource = express.HTTPSServer.prototype.resource = function(name, actions, opts){ var options = actions || {}; diff --git a/test/resource.path.test.js b/test/resource.path.test.js new file mode 100644 index 0000000..2f17a4f --- /dev/null +++ b/test/resource.path.test.js @@ -0,0 +1,133 @@ + +/** + * Module dependencies. + */ +var assert = require('assert') + , express = require('express') + , should = require('should') + , Resource = require('../'); + +module.exports = { + 'test resource': function(){ + var app = express.createServer(); + var ret = app.resource('forums', require('./fixtures/forum')); + + assert.strictEqual(app.resource.path.forums_path(), '/forums'); + assert.strictEqual(app.resource.path.new_forum_path(), '/forums/new'); + assert.strictEqual(app.resource.path.forum_path({id: 5}), '/forums/5'); + + assert.strictEqual(app.resource.path.forum_path({id: 10}), '/forums/10'); + + assert.strictEqual(app.resource.path.edit_forum_path({id: 5}), '/forums/5/edit'); + }, + 'test shallow nesting': function(){ + var app = express.createServer(); + + var forumObj = {id: 5}; + var threadObj = {id: 50}; + + var forum = app.resource('forums', require('./fixtures/forum')); + var thread = app.resource('threads', require('./fixtures/thread')); + forum.map(thread); + + assert.strictEqual(app.resource.path.forums_path(), '/forums'); + assert.strictEqual(app.resource.path.new_forum_path(), '/forums/new'); + assert.strictEqual(app.resource.path.forum_path(forumObj), '/forums/5'); + assert.strictEqual(app.resource.path.edit_forum_path(forumObj), '/forums/5/edit'); + + assert.strictEqual(app.resource.path.forum_threads_path(forumObj), '/forums/5/threads'); + assert.strictEqual(app.resource.path.new_forum_thread_path(forumObj), '/forums/5/threads/new'); + assert.strictEqual(app.resource.path.forum_thread_path(forumObj, threadObj), '/forums/5/threads/50'); + assert.strictEqual(app.resource.path.edit_forum_thread_path(forumObj, threadObj), '/forums/5/threads/50/edit'); + }, + 'test top level resource nesting': function(){ + var app = express.createServer(); + + var forumObj = {id: 5}; + var threadObj = {id: 50}; + + var forum = app.resource(require('./fixtures/forum')); + var thread = app.resource('threads', require('./fixtures/thread')); + forum.map(thread); + + assert.strictEqual(app.resource.path.roots_path(), '/'); + assert.strictEqual(app.resource.path.new_root_path(), '/new'); + assert.strictEqual(app.resource.path.root_path(forumObj), '/5'); + assert.strictEqual(app.resource.path.edit_root_path(forumObj), '/5/edit'); + + assert.strictEqual(app.resource.path.root_threads_path(forumObj), '/5/threads'); + assert.strictEqual(app.resource.path.new_root_thread_path(forumObj), '/5/threads/new'); + assert.strictEqual(app.resource.path.root_thread_path(forumObj, threadObj), '/5/threads/50'); + assert.strictEqual(app.resource.path.edit_root_thread_path(forumObj, threadObj), '/5/threads/50/edit'); + }, + 'test deep resource nesting': function(){ + var app = express.createServer(); + + var userObj = {id: 1}; + var forumObj = {id: 5}; + var threadObj = {id: 50}; + + 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.strictEqual(app.resource.path.users_path(), '/users'); + + assert.strictEqual(app.resource.path.user_forums_path(userObj), '/users/1/forums'); + assert.strictEqual(app.resource.path.new_user_forum_path(userObj), '/users/1/forums/new'); + assert.strictEqual(app.resource.path.user_forum_path(userObj, forumObj), '/users/1/forums/5'); + assert.strictEqual(app.resource.path.edit_user_forum_path(userObj, forumObj), '/users/1/forums/5/edit'); + + assert.strictEqual(app.resource.path.user_forum_threads_path(userObj, forumObj), '/users/1/forums/5/threads'); + assert.strictEqual(app.resource.path.new_user_forum_thread_path(userObj, forumObj), '/users/1/forums/5/threads/new'); + assert.strictEqual(app.resource.path.user_forum_thread_path(userObj, forumObj, threadObj), '/users/1/forums/5/threads/50'); + assert.strictEqual(app.resource.path.edit_user_forum_thread_path(userObj, forumObj, threadObj), '/users/1/forums/5/threads/50/edit'); + }, + 'test resource with custom actions': function(){ + var app = express.createServer(); + var ret = app.resource('forums', require('./fixtures/forum')); + + var actions = { + lock: function(req, res){ + res.end('login'); + }, + design: function(req, res){ + res.end('logout'); + } + }; + + ret.map('get', 'lock', actions.lock); + ret.map('get', '/design', actions.design); + + assert.strictEqual(app.resource.path.design_forums_path(), '/forums/design'); + assert.strictEqual(app.resource.path.lock_forum_path({id: 5}), '/forums/5/lock'); + }, + 'test resource with direct id value': function(){ + var app = express.createServer(); + var ret = app.resource('forums', require('./fixtures/forum')); + + assert.strictEqual(app.resource.path.forum_path(5), '/forums/5'); + assert.strictEqual(app.resource.path.edit_forum_path(5), '/forums/5/edit'); + + assert.strictEqual(app.resource.path.forum_path("5"), '/forums/5'); + assert.strictEqual(app.resource.path.edit_forum_path("5"), '/forums/5/edit'); + }, + 'test resource with custom id field': function(){ + var app = express.createServer(); + var ret = app.resource('forums', require('./fixtures/forum')); + + // NOTE: because this is set across all resources, this test should be run last or it will need + // to be reset for each test. If this is too confusing, we could change to set in on a per-resource basis. + app.resource.path.idField = '_id'; + + assert.strictEqual(app.resource.path.forum_path({_id: 5}), '/forums/5'); + assert.strictEqual(app.resource.path.edit_forum_path({_id: 5}), '/forums/5/edit'); + }, + +}; \ No newline at end of file