jaredhanson/locomotive

View on GitHub
lib/application.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Module dependencies.
 */
var fs = require('fs')
  , bootable = require('bootable')
  , Router = require('actionrouter').Router
  , Resolver = require('./resolver')
  , Instantiator = require('./instantiator')
  , DispatchError = require('./errors/dispatcherror')
  , utils = require('./utils')
  , debug = require('debug')('locomotive');


/**
 * `Locomotive` constructor.
 *
 * A default `Locomotive` singleton is exported via the module.  Applications
 * should not need to construct additional instances, and are advised against
 * doing so.
 *
 * @api public
 */
function Application() {
  this.__router = new Router();
  this.__initializer = new bootable.Initializer();
  this.__controllers = {};
  this._formats = {};
  this._helpers = {};
  this._dynamicHelpers = {};
  this.__datastores = [];
  
  this.__controllerResolver = new Resolver();
  this.__controllerInstantiator = new Instantiator();
  this.__viewResolver = new Resolver();
  
  // Sugary API
  this.controllers = {};
  this.controllers.resolve = { use: this.__controllerResolver.use.bind(this.__controllerResolver) };
  this.controllers.instantiate = { use: this.__controllerInstantiator.use.bind(this.__controllerInstantiator) };

  this.views = {};
  this.views.resolve = this.__viewResolver.resolve.bind(this.__viewResolver);
  this.views.resolve.use = this.__viewResolver.use.bind(this.__viewResolver);
}

/**
 * Initialize application.
 *
 * @api private
 */
Application.prototype.init = function(env) {
  env = env || process.env.NODE_ENV || 'development';
  require('./underlay/express').call(this, env);
  
  this.env = env;
  this.helpers(require('./helpers'));
  this.dynamicHelpers(require('./helpers/dynamic'));
};

/**
 * Register rendering `options` for format `fmt`.
 *
 * Locomotive provides support for content negotiation, allowing a single route
 * to respond with multiple formats.  For example, a route handler might respond
 * with JSON or XML for API requests, and HTML for requests from a browser.
 * `Controller.respond()` is used to respond according to the aceptable types
 * indicated by the client.
 *
 * Rather than specifying the engine used to render the response as an option to
 * each `render` or `respond` invocation, the engine can be specified once as
 * an application-level option (typically in `environments/all.js`).
 *
 *     this.format('xml', { engine: 'xmlb' })
 *
 * The above specifies that [xmlb](https://github.com/jaredhanson/xmlb) is used
 * as the template engine for XML formatted responses.
 *
 * When rendering from a controller, the application-level engine option is in
 * effect:
 *
 *     this.render('atom', { format: 'xml' });
 *     //=> renders `atom.xml.xmlb`
 *
 * By default, Locomotive looks for template files using a convention of
 * name.format.engine.  If this convention is not natural for the template
 * engine being used, it can be overridden by registering an extension for the
 * format.
 *
 * For example, [Jade](http://jade-lang.com/) is a popular template engine.
 * Jade expects layouts to end with `.jade`.  Using template names of
 * `action.html.jade` results in mixed conventions that can cause confusion.
 * When this happens, map an explicit extension to the format.
 *
 *     this.format('html', { extension: '.jade' })
 *
 * Now, rendering will use the mapped extension instead of the `.format.engine`
 * convention.
 *
 *     this.render('show');
 *     //=> renders `show.jade`
 *
 * @param {String} fmt
 * @param {Object} options
 * @return {Locomotive} for chaining
 * @api public
 */
Application.prototype.format = function(fmt, options) {
  fmt = utils.extensionizeType(fmt);
  if (typeof options == 'string') {
    options = { engine: options };
  }
  this._formats[fmt] = options;
  return this;
};

/**
 * Register helper function(s).
 *
 * Helper functions can be registered by passing an object as an argument, in
 * which case each function of that object will be registered as a helper.  As
 * a convienience, if the object contains a property named `dynamic`, each
 * function attached to that property will be registered as a dynamic helper.
 *
 * Helper functions can also be registered by passing a `name`, which `fn` will
 * be registered as.
 *
 * @param {String|Object} obj
 * @param {Function} fn
 * @api public
 */
Application.prototype.helper =
Application.prototype.helpers = function(name, fn) {
  var helpers = name;
  if (fn) {
    helpers = {};
    helpers[name] = fn;
  }

  for (var key in helpers) {
    if (key === 'dynamic') {
      this.dynamicHelpers(helpers[key]);
    } else {
      this._helpers[key] = helpers[key];
    }
  }
  return this;
};

/**
 * Register dynamic helper function(s).
 *
 * Helper functions can be registered by passing an object as an argument, in
 * which case each function of that object will be registered as a helper.
 *
 * Helper functions can also be registered by passing a `name`, which `fn` will
 * be registered as.
 *
 * @param {String|Object} obj
 * @param {Function} fn
 * @api public
 */
Application.prototype.dynamicHelper =
Application.prototype.dynamicHelpers = function(name, fn) {
  var helpers = name;
  if (fn) {
    helpers = {};
    helpers[name] = fn;
  }

  for (var key in helpers) {
    this._dynamicHelpers[key] = helpers[key];
  }
  return this;
};

/**
 * Register datastore `store`.
 *
 * To facilitate mapping models to controllers, Locomotive introspects models
 * in order to determine their type.  By default, the constructor name is used;
 * for example, an instance of `Song` maps to `SongsController`.  However, most
 * datastores have APIs that don't conform to this (admittedly, rather
 * simplistic) heuristic.  To accomodate such datastores, adapters can and
 * should be registered with Locomotive to provide the necessary introspection
 * logic.
 *
 * @param {Module} store
 * @api public
 */
Application.prototype.datastore = function(store) {
  this.__datastores.push(store);
};

/**
 * Register a boot phase.
 *
 * When an application boots, it proceeds through a series of phases, ultimately
 * resulting in a listening server ready to handle requests.
 *
 * A phase can be either synchronous or asynchronous.  Synchronous phases have
 * following function signature
 *
 *     function myPhase() {
 *       // perform initialization
 *     }
 *
 * Asynchronous phases have the following function signature.
 *
 *     function myAsyncPhase(done) {
 *       // perform initialization
 *       done();  // or done(err);
 *     }
 *
 * @param {Function} fn
 * @api public
 */
Application.prototype.phase = function(fn) {
  this.__initializer.phase(fn);
  return this;
};

/**
 * Middleware that will extend `req` with an `invoke()` function.
 *
 * Once `invoke()` is exposed on a request, it can be called in order to invoke
 * a specific controller and action in this Locomotive appliction.  This is
 * typically done to call into a Locmotive application from middleware or routes
 * that exist outside of the application itself.
 *
 * To use this middleware, place the following line in `config/environments/all.js`:
 *
 *     this.use(locomotive.invoke());
 *
 * If you are mounting multiple Locomotive applications in a single server, a
 * `name` option can be passed, in order to avoid collisions when extending the
 * request multiple times.
 *
 *     this.use(oneApp.invoke({ name: 'invokeOneApp' }));
 *     this.use(twoApp.invoke({ name: 'invokeTwoApp' }));
 *
 * @param {Object} options
 * @return {Function} middleware
 * @api public
 */
Application.prototype.invokable = function(options) {
  return require('./middleware/invokable')(this, options);
};

/**
 * Instantiate controller with given `id`.
 *
 * Controller's are auto-required by Locomotive.  The value exported by the
 * module is used to construct an instance of the controller, using either a
 * constructor or prototype creation pattern.
 *
 * @param {String} id
 * @param {Function} cb
 * @api protected
 */
Application.prototype._controller = function(id, cb) {
  var mod = this.__controllers[id]
    , mpath;
  
  if (!mod) {
    // No controller module was found in the cache.  Attempt auto-load.
    debug('autoload controller ' + id);
    try {
      mpath = this.__controllerResolver.resolve(id);
    } catch (_) {
      return cb(new DispatchError("Unable to resolve controller '" + id + "'"));
    }
  
    try {
      mod = require(mpath);
    } catch (ex) {
      if (ex instanceof SyntaxError) {
        // Helpful error with file name and line number
        var check = require('syntax-error');
        
        var src = fs.readFileSync(mpath)
          , err = check(src, mpath);
        if (err) { return cb(err); }
      }
      return cb(ex);
    }
    
    // cache the controller module
    this.__controllers[id] = mod;
  }
  
  this.__controllerInstantiator.instantiate(mod, id, function(err, inst) {
    if (err) { return cb(err); }
    return cb(null, inst);
  });
};

/**
 * Find route to given controller action.
 *
 * Locomotive uses an MVC-style router, where routes map from a URL pattern to
 * a controller action, and vice versa.
 *
 * @param {String} controller
 * @param {String} action
 * @return {Entry}
 * @api protected
 */
Application.prototype._routeTo = function(controller, action) {
  return this.__router.find(controller, action);
};

/**
 * Returns a string indicating the type of record of `obj`.
 *
 * @param {Object} obj
 * @return {String}
 * @api protected
 */
Application.prototype._recordOf = function(obj) {
  for (var i = 0, len = this.__datastores.length; i < len; i++) {
    var ds = this.__datastores[i];
    var kind = ds.recordOf(obj);
    if (kind) { return kind; }
  }
  return undefined;
};

/**
 * Boot `Locomotive` application.
 *
 * Locomotive builds on Express, providing a set of conventions for how to
 * organize code and resources on the file system as well as an MVC architecture
 * for structuring code.
 *
 * When booting a Locomotive application, the file system conventions are used
 * to initialize modules, configure the environment, register controllers, and
 * draw routes.  When complete, `callback` is invoked with an initialized
 * `express` instance that can listen for requests or be mounted in a larger
 * application.
 *
 * @param {String} dir
 * @param {String} env
 * @param {Object} options
 * @param {Function} callback
 * @api public
 */
Application.prototype.boot = function(env, cb) {
  if (typeof env == 'function') {
    cb = env;
    env = undefined;
  }
  
  this.init(env);
  this.__initializer.run(cb, this);
};


/**
 * Expose `Application`.
 */
module.exports = Application;