Source: router.js

/**
 * @module  router
 * @author  Flavio De Stefano <flavio.destefano@caffeinalab.com>
 */

/**
 * @property config
 * @property {String} config.protocol  			Force all protocol different from this to be discarded
 */
exports.config = _.extend({
	protocol: null,
}, Alloy.CFG.T ? Alloy.CFG.T.router : {});

var Util = require('T/util');
var Flow = require('T/flow');
var Q = require('T/ext/q');

var routeRegistry = [];

/**
 * @property queue
 * The route queue
 * @type {Array}
 */
exports.queue = [];

/**
 * @property bypassQueue
 * Set this property to `true` to dispatch the enqueued url instantly.
 * @type {Boolean}
 */
exports.bypassQueue = false;

/**
 * @property currentUrl
 * Latest URL dispatched
 * @type {String}
 */
exports.currentUrl = null;

/**
 * @property currentRoute
 * Latest Route (not URL) dispatched
 * @type {Object}
 */
exports.currentRoute = null;

/**
 * @property stack
 * All routes in a stack
 * @type {Array}
 */
exports.stack = [];

/**
 * Register a route with defined callbacks
 *
 * @param {Object} key The route name.
 * It can be:
 *
 * * `String`: (exact route match)
 * * `RegExp`: is evaluated with the argument and the matches are passed to the callback
 * * `Function`: must return a `non-undefined` value to be executed. That value is passed to the callback
 *
 * @param  {Function}	callback  		The callback
 */
exports.on = function() {
	var middlewares = _.toArray(arguments);
	var key = middlewares.shift();
	var callback = middlewares.pop();

	routeRegistry.push({
		key: key,
		callback: callback,
		middlewares: middlewares
	});
};

/**
 * Dispatch the router
 *
 * This function call the defined function with `Router.on`
 *
 * The param `this` represents an XCallback-URL Object for selected route.
 *
 * See https://github.com/FokkeZB/UTiL/blob/master/XCallbackURL/XCallbackURL.js for more details.
 *
 * The arguments passed are the matches for your regex definition (if present)
 *
 * @param  {String} url 		The route
 */
exports.dispatch = function(url, data) {
	Ti.API.debug('Router: dispatching <' + url + '>');

	var callbackURL = Util.parseAsXCallbackURL(url);
	callbackURL.path = callbackURL.path.replace(/\/$/g, '');

	if (callbackURL.protocol && exports.config.protocol) {
		if (exports.config.protocol !== callbackURL.protocol) {
			Ti.API.warn('Router: protocol mismatch');
			return false;
		}
	}

	callbackURL.data = data;

	var run = false;
	var matches = null;
	var routeDefinition = null;

	// Check the route to dispatch
	for (var i in routeRegistry) {
		routeDefinition = routeRegistry[i];

		if (_.isString(routeDefinition.key)) {
			// Regular string equals
			run = (routeDefinition.key === callbackURL.path);
		} else if (_.isRegExp(routeDefinition.key)) {
			// Regular expression complex match
			matches = callbackURL.path.match(routeDefinition.key);
			run = !!(matches);
			if (matches) matches.shift();
		} else if (_.isFunction(routeDefinition.key)) {
			// Function match
			matches = routeDefinition.key(callbackURL.path);
			run = (matches !== undefined);
		}

		if (run === true) break;
	}

	if (run === true) {
		Ti.API.debug('Router: matched on <' + routeDefinition.key + ', ' + JSON.stringify(matches) + '>');

		if (_.isFunction(routeDefinition.callback)) {

			exports.stack.push(url);
			exports.currentUrl = url;
			exports.currentRoute = routeDefinition;

			if (routeDefinition.middlewares.length > 0) {

				routeDefinition.middlewares.reduce(function(soFar, f) {
					return soFar.then( f.bind(callbackURL) );
				}, Q(matches))
				.then(function() {
					routeDefinition.callback.apply(callbackURL, matches);
				})
				.catch(function(err) {
					Ti.API.error('Router: error during route dispatcher', err);
				});

			} else {
				routeDefinition.callback.apply(callbackURL, matches);
			}

		} else if (_.isObject(routeDefinition.callback)) {

			if (routeDefinition.callback.alias != null) {
				exports.dispatch(routeDefinition.callback.alias);
			}

		}
	} else {
		Ti.API.warn('Router: no match for <' + url + '>');
	}

	return run;
};

/**
 * @link #dispatch
 */
exports.go = exports.dispatch;

/**
 * @param  {String} url The route
 */
exports.enqueue = function(url) {
	Ti.API.debug('Router: enqueuing <' + url + '>');

	if (exports.bypassQueue) {
		exports.dispatch(url);
	} else {
		exports.queue.push(url);
	}
};

/**
 * @param  {Array} array
 */
exports.appendToQueue = function(array) {
	exports.queue = exports.queue.concat(array);
};

/**
 * Dispatch the queue
 * @param  {Boolean} bypassFromNow Indicate if (from now) should not enqueue routes but dispatch directly
 */
exports.dispatchQueue = function(bypassFromNow) {
	var e = null;
	while ((e = exports.queue.shift()) != null) {
		Ti.API.debug('Router: dequeuing <' + e + '>');
		exports.dispatch(e);
	}
	exports.bypassQueue = !!bypassFromNow;
};

/**
 * Make an alias route
 * @param  {String} url
 * @param  {String} newUrl
 */
exports.alias = function(url, newUrl) {
	exports.on(url, {
		alias: newUrl
	});
};

/**
 * Create the routes for a model
 * @param  {String} single The name for the model
 * @param  {String} [plural] The name for the model, plural.
 */
exports.autoMapModel = function(single, plural) {
	plural = plural || single+'s';

	exports.on('/' + plural, function() {
		Flow.open(plural, {}, {}, this.source);
	});

	exports.on(new RegExp('/' + plural + '/([0-9]+)'), function(id) {
		Flow.open(single, {
			id: id
		}, {}, this.source);
	});
};