Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
joakimbeng committed Nov 10, 2013
0 parents commit d95d21b
Show file tree
Hide file tree
Showing 9 changed files with 653 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
node_modules/
*.sublime-*
Empty file added .jshintrc
Empty file.
274 changes: 274 additions & 0 deletions index.js
@@ -0,0 +1,274 @@

/**
* DEPENDENCIES
*/
var glob = require('glob'),
path = require('path'),
_ = require('underscore.string');

/**
* CONSTANTS
*/
var MAGIC_ACTIONS = {
"create": {method: "post", args: null},
"read": {method: "get", args: [":id"]},
"update": {method: "put", args: [":id"]},
"del": {method: "delete", args: [":id"]},
"search": {method: "get", args: null},
"index": {method: "get", args: null},
"list": {method: "get", args: null}
};

/**
* PUBLIC API
*/

/**
* Attach an express.js app
*
* @param {Function} app
*/
exports.bind = function (app) {
this.app = app;
};

/**
* Automatic router middleware
*
* @param {Function} app An express app
*/
exports.load = function (app, options) {
var self = this;

if (typeof app === 'object' && typeof options !== 'object') {
options = app;
app = null;
}

if (app) {
this.bind(app);
}

options = options || {};

if (!options.pattern) {
throw new Error('You must specify a controller matching pattern! E.g. "controllers/*.js"');
}

// Make this sync because we want this to be executed where we intend:
glob.sync(options.pattern).forEach(function (file) {
var controller = require(file),
controllerName = getControllerName(file, options.nameRegExp),
mountPoint = stripUnwantedSlashes(options.prefix) + '/' + (controllerName.toLowerCase() !== 'home' ? controllerName : '');

self.mount(mountPoint, controller);
});
};

/**
* Mount a given controller
*
* @param {String} name Used as mount point
* @param {Object} controller
*/
exports.mount = function (name, controller) {
if (!this.app) {
throw new Error('An Express.js app must be bound to exctrl before mounting a controller!');
}

if (typeof name === 'object' && typeof name.name === 'string') {
controller = name;
name = controller.name;
}

var app = this.app;

Object.keys(controller).forEach(function (action) {

if (action === 'name' && controller[action] === name) {
return;
}

var magic = MAGIC_ACTIONS[action] || {},
route = {
method: magic.method || getMethod(action) || "get",
url: [],
args: magic.args || [],
handler: controller[action]
};

if (name) {
route.url.push(stripUnwantedSlashes(name));
}

if (action === 'init') {
controller[action](app, route.url.join('/'));
return;
}

if (!magic.method) {
// Custom actions (i.e. not magic) needs its name without HTTP method added to the url:
var urlPart = stripUnwantedSlashes(getUrlPart(action));
if (urlPart) {
route.url.push(urlPart);
}
}

// Is the action an array?
if (Array.isArray(route.handler)) {
if (route.handler.length > 1) {
// All preceeding elements in the array are url params/chunks/arguments e.g. ":id":
route.args = route.handler.splice(0, route.handler.length - 1);
}
// The last element in the array is the real handler:
route.handler = route.handler.pop();
}

if (typeof route.handler !== 'function') {
throw new Error('Bad handler type for action: ' + name + ':' + action + ', expected: "function", but was: "' + typeof(route.handler) + '"');
}

// Add the url params/chunks/arguments to the route.url:
arrayAdd(route.url, getUrlChunks(route.args));

if (app.get('env') === 'development') {
console.log('CONTROLLER-ACTION:ADD', route.method.toUpperCase() + ' ' + getUrl(route.url));
}

// Add the route to app:
app[route.method].apply(app,
[getUrl(route.url)]
.concat(getMiddlewares(route.args))
.concat(route.handler.bind(controller))
);
});
};

/**
* PRIVATE API
*/

/**
* Add one array's elements to another
*
* @param {Array} dest
* @param {Array} src
*/
function arrayAdd (dest, src) {
dest.push.apply(dest, src);
}

/**
* Get controller name
*
* Gets the controller name from a filename
* If `extractorRegExp` is provided and it matches
* the filename, the first match group will be returned.
* Otherwise the filename without extension will be returned.
*
* Example 1:
* file: /var/tmp/super.controller.js
* extractorRegExp: /([^\/\\]+).controller.js$/
* Returns: "super"
*
* Example 2:
* file: /var/tmp/controllers/super.js
* extractorRegExp: NULL
* Returns: "super"
*
* @param {String} file
* @param {RegExp} extractorRegExp
* @returns {String}
*/
function getControllerName (file, extractorRegExp) {
var name = extractorRegExp ? file.match(extractorRegExp)[1] : null;
return name || path.basename(file).slice(0, -path.extname(file).length);
}

/**
* Get an URL from an array of chunks
*
* Joins the chunks with "/" and prepends it as well
*
* @param {Array} urlChunks
* @returns {String}
*/
function getUrl (urlChunks) {
return '/' + urlChunks.join('/');
}

/**
* Get all middlewares
*
* Returns all middlewares, i.e. functions, in an array
*
* @param {Array} arr
* @returns {Array}
*/
function getMiddlewares (arr) {
return arr.filter(function (e) {
return typeof e === 'function';
});
}

/**
* Get all url chunks
*
* Returns all url chunks, i.e. not functions, in an array
*
* @param {Array} arr
* @returns {Array}
*/
function getUrlChunks (arr) {
return arr.filter(function (e) {
return typeof e !== 'function';
});
}

/**
* Removes leading and trailing slashes in url
*
* @param {String} url
* @returns {String}
*/
function stripUnwantedSlashes (url) {
return _.trim(url || '', '/');
}

/**
* Get HTTP method from action name
*
* Expects action name to be camelCased, e.g:
* - postThing -> post
* - getSuperThingy -> get
* - weirdAction -> NULL
*
* @param {String} action
* @returns {String}
*/
function getMethod (action) {
var method = action.split(/[^a-z]/)[0];
if (['post', 'head', 'get', 'delete', 'put'].indexOf(method) >= 0) {
return method;
}
return null;
}

/**
* Get url part from action name
*
* Expects action name to be camelCased, e.g:
* - postThing -> thing
* - getSuperThingy -> super-thingy
* - myAction -> my-action
*
* @param {String} action
* @returns {String}
*/
function getUrlPart (action) {
var method = getMethod(action);
if (method) {
action = action.slice(method.length);
}
return _.ltrim(_.dasherize(action), '-');
}
31 changes: 31 additions & 0 deletions package.json
@@ -0,0 +1,31 @@
{
"name": "exctrl",
"version": "0.1.0",
"description": "Flexible controllers for express.js apps",
"main": "index.js",
"scripts": {
"test": "node_modules/.bin/mocha -R spec tests"
},
"repository": {
"type": "git",
"url": "https://github.com/klei-dev/exctrl"
},
"keywords": [
"controller",
"express",
"router"
],
"author": "Joakim Bengtson <joakim@klei.se>",
"license": "MIT",
"bugs": {
"url": "https://github.com/klei-dev/exctrl/issues"
},
"devDependencies": {
"mocha": "~1.14.0",
"chai": "~1.8.1"
},
"dependencies": {
"glob": "~3.2.7",
"underscore.string": "~2.3.3"
}
}
19 changes: 19 additions & 0 deletions tests/.jshintrc
@@ -0,0 +1,19 @@
{
"node": true,
"esnext": true,
"globals": {
"describe": false,
"it": false,
"before": false,
"beforeEach": false,
"after": false,
"afterEach": false
},
"globalstrict": false,
"quotmark": false,
"smarttabs": true,
"trailing": true,
"undef": true,
"unused": true,
"strict": false
}
4 changes: 4 additions & 0 deletions tests/controllers/home.js
@@ -0,0 +1,4 @@

exports.index = function (req, res) {
res.send('index');
};
8 changes: 8 additions & 0 deletions tests/controllers/user.js
@@ -0,0 +1,8 @@

exports.read = function (req, res) {
res.send('a user');
};

exports.list = function (req, res) {
res.send('all users');
};

0 comments on commit d95d21b

Please sign in to comment.