mojito
Version:
Mojito provides an architecture, components and tools for developers to build complex web applications faster.
284 lines (235 loc) • 8.09 kB
JavaScript
/*
* Copyright (c) 2011-2013, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
/*jslint anon:true, sloppy:true, regexp: true, nomen:true*/
/*global YUI,console*/
YUI.add('mojito-route-maker', function (Y, NAME) {
var CACHE = { routes: {} };
/**
Given a route with parametrized variable, this function will attempt to
resolve it based on a set of parameters.
@method resolveParams
@private
@param {Object} route
@param {Object} params
@return {Object|null} the route if there is a match, otherwise null
**/
function resolveParams(route, params) {
var keys = route.keys,
len = keys.length,
i;
if (len === 0) {
return route;
}
for (i = 0; i < len; i = i + 1) {
if (!params[keys[i].name]) {
return null;
}
}
return route;
}
/**
@class Maker
@namespace Y.mojito
@param {Object} routes routes configuration
@param {Boolean} init if true, reset the routes cache
**/
function Maker(routes, init) {
if (init) {
CACHE.routes = {};
}
CACHE.routes = routes;
}
Maker.prototype = {
// For unit tests only
// DO NOT USE
_resolveParams: resolveParams,
//
/**
Finds a route for a given method+URL
@method find
@param {String} url the URL to find a route for.
@param {String} verb the HTTP method.
@return {Object} the matched route object, or null
**/
find: function(uri, verb) {
var routes = CACHE.routes,
copy,
i,
j,
key,
len,
matches,
name,
names,
regex,
route;
for (name in routes) {
if (routes.hasOwnProperty(name)) {
matches = routes[name].regexp.exec(uri);
if (matches) {
route = routes[name];
break;
}
}
}
if (!route) {
return null;
}
// Return clone to caller
copy = Y.mojito.util.copy(route);
// Extract the url paramterized paths and store in `params`
copy.params = copy.params || {};
len = copy.keys.length;
for (i = 0, j = 1; i < len; i += 1, j += 1) {
key = copy.keys[i];
copy.params[key.name] = matches[j];
}
// Lookup the `call` by aliases
names = route.annotations.aliases;
key = verb + '#';
name = undefined;
for (i = 0; i < names.length; i += 1) {
if (names[i].indexOf(key) > -1) {
name = names[i];
break;
}
}
if (!name) {
// The route is misconfigured, return error;
// Most likely app.map() was not called with the
// correct alias for the route
return null;
}
copy.call = name.substring(key.length);
// Do "call" replacement
len = copy.keys.length;
for (i = 0; i < len; i += 1) {
key = copy.keys[i].name;
regex = new RegExp('{' + key + '}', 'g');
if (regex.test(copy.call)) {
copy.call = copy.call.replace(regex, copy.params[key]);
}
}
return copy;
},
/**
TODO How can we leverage express-map#pathTo ?
Returns the matching route path based on the route name.
@method linkTo
@param {String} name
@param {Object} params
@return {String|null} path
**/
linkTo: function(name, params) {
var route = CACHE.routes[name],
path,
keys,
key,
i,
len,
param,
regex;
if (!route) {
return null;
}
path = route.path;
keys = route.keys;
len = keys.length;
if (params && len > 0) {
for (i = 0; i < len; i += 1) {
key = keys[i];
param = key.name || key;
regex = new RegExp('[:*]' + param + '\\b');
path = path.replace(regex, params[param]);
}
}
// Replace missing params with empty strings.
return path.replace(/([:*])([\w\-]+)?/g, '');
},
/**
Generates a URL from a route query
NOTE: Adding query string to "query" (e.g. "foo.index?src=dot") is
deprecated feature.
@method make
@param {String} query string to convert to a URL
@param {String} verb http method
@param {Object} params object representing extra querystring
params. `query` might have querystring portion
portion, in which case they have priority.
@return {String} the generated path uri for the query, or null
**/
make: function(query, verb, params) {
var parts = query.split('?'),
call = parts[0],
residual = {},
i,
k,
key,
keys,
paramKeys,
route,
uri;
query = query || '';
verb = verb || 'get';
params = params || {};
if (parts[1]) {
params = Y.QueryString.parse(parts[1]);
}
key = Y.mojito.util.encodeRouteName(verb, call);
// Y.log('Lookup key: ' + key, 'debug', NAME);
route = CACHE.routes[key];
if (!route) {
return null;
}
// Check that the params is applicable to the route
route = resolveParams(route, params);
if (!route) {
return null;
}
uri = route.path;
keys = [];
Y.Array.each(route.keys, function (key) {
keys.push(key.name);
});
paramKeys = Y.Object.keys(params).sort();
// Y.log('---- paramKeys: ' + paramKeys, 'debug', NAME);
// handle left over params by appending them to query string
for (i = paramKeys.length - 1; i >= 0; i = i - 1) {
k = paramKeys[i];
if (params.hasOwnProperty(k)) {
if (keys.indexOf(k) > -1) {
uri = uri.replace(':' + k, params[k]);
// Y.log('---- replacing key : ' + k + '; uri = ' + uri,
// 'debug', NAME);
} else {
residual[k] = params[k];
}
}
}
if (!Y.Object.isEmpty(residual)) {
uri += '?' + Y.QueryString.stringify(residual);
}
return uri;
},
/**
* For optimization. Call this to get the computed routes that can be
* passed to the constructor to avoid recomputing the routes.
*
* @method getComputedRoutes
* @return {object} computed routes.
*/
getComputedRoutes: function() {
// NOTE: We used to copy() here. Research suggested that it was
// safe to drop.
return CACHE.routes;
}
};
Y.namespace('mojito').RouteMaker = Maker;
}, '0.1.0', { requires: [
'querystring-stringify-simple',
'querystring-parse',
'mojito-util'
]});