falcor-router
Version:
A router DataSource constructor for falcor that allows you to model all your cloud data sources as a single JSON resource.
226 lines (195 loc) • 6.38 kB
JavaScript
var Keys = require('../Keys');
var actionWrapper = require('./actionWrapper');
var pathSyntax = require('falcor-path-syntax');
var convertTypes = require('./convertTypes');
var prettifyRoute = require('./../support/prettifyRoute');
var errors = require('./../exceptions');
var cloneArray = require('./../support/cloneArray');
var ROUTE_ID = -3;
module.exports = function parseTree(routes) {
var pTree = {};
var parseMap = {};
routes.forEach(function forEachRoute(route) {
// converts the virtual string path to a real path with
// extended syntax on.
if (typeof route.route === 'string') {
route.prettyRoute = route.route;
route.route = pathSyntax(route.route, true);
convertTypes(route);
}
if (route.get) {
route.getId = ++ROUTE_ID;
}
if (route.set) {
route.setId = ++ROUTE_ID;
}
if (route.call) {
route.callId = ++ROUTE_ID;
}
setHashOrThrowError(parseMap, route);
buildParseTree(pTree, route, 0, []);
});
return pTree;
};
function buildParseTree(node, routeObject, depth) {
var route = routeObject.route;
var get = routeObject.get;
var set = routeObject.set;
var call = routeObject.call;
var el = route[depth];
el = !isNaN(+el) && +el || el;
var isArray = Array.isArray(el);
var i = 0;
do {
var value = el;
var next;
if (isArray) {
value = value[i];
}
// There is a ranged token in this location with / without name.
// only happens from parsed path-syntax paths.
if (typeof value === 'object') {
var routeType = value.type;
next = decendTreeByRoutedToken(node, routeType, value);
}
// This is just a simple key. Could be a ranged key.
else {
next = decendTreeByRoutedToken(node, value);
// we have to create a falcor-router virtual object
// so that the rest of the algorithm can match and coerse
// when needed.
if (next) {
route[depth] = {type: value, named: false};
}
else {
if (!node[value]) {
node[value] = {};
}
next = node[value];
}
}
// Continue to recurse or put get/set.
if (depth + 1 === route.length) {
// Insert match into routeSyntaxTree
var matchObject = next[Keys.match] || {};
if (!next[Keys.match]) {
next[Keys.match] = matchObject;
}
matchObject.prettyRoute = routeObject.prettyRoute;
if (get) {
matchObject.get = actionWrapper(route, get);
matchObject.getId = routeObject.getId;
}
if (set) {
matchObject.set = actionWrapper(route, set);
matchObject.setId = routeObject.setId;
}
if (call) {
matchObject.call = actionWrapper(route, call);
matchObject.callId = routeObject.callId;
}
} else {
buildParseTree(next, routeObject, depth + 1);
}
} while (isArray && ++i < el.length);
}
/**
* ensure that two routes of the same precedence do not get
* set in.
*/
function setHashOrThrowError(parseMap, routeObject) {
var route = routeObject.route;
var get = routeObject.get;
var set = routeObject.set;
var call = routeObject.call;
getHashesFromRoute(route).
map(function mapHashToString(hash) { return hash.join(','); }).
forEach(function forEachRouteHash(hash) {
if (get && parseMap[hash + 'get'] ||
set && parseMap[hash + 'set'] ||
call && parseMap[hash + 'call']) {
throw new Error(errors.routeWithSamePrecedence + ' ' +
prettifyRoute(route));
}
if (get) {
parseMap[hash + 'get'] = true;
}
if (set) {
parseMap[hash + 'set'] = true;
}
if (call) {
parseMap[hash + 'call'] = true;
}
});
}
/**
* decends the rst and fills in any naming information at the node.
* if what is passed in is not a routed token identifier, then the return
* value will be null
*/
function decendTreeByRoutedToken(node, value, routeToken) {
var next = null;
switch (value) {
case Keys.keys:
case Keys.integers:
case Keys.ranges:
next = node[value];
if (!next) {
next = node[value] = {};
}
break;
default:
break;
}
if (next && routeToken) {
// matches the naming information on the node.
next[Keys.named] = routeToken.named;
next[Keys.name] = routeToken.name;
}
return next;
}
/**
* creates a hash of the virtual path where integers and ranges
* will collide but everything else is unique.
*/
function getHashesFromRoute(route, depth, hashes, hash) {
/*eslint-disable no-func-assign, no-param-reassign*/
depth = depth || 0;
hashes = hashes || [];
hash = hash || [];
/*eslint-enable no-func-assign, no-param-reassign*/
var routeValue = route[depth];
var isArray = Array.isArray(routeValue);
var length = isArray && routeValue.length || 0;
var idx = 0;
var value;
if (typeof routeValue === 'object' && !isArray) {
value = routeValue.type;
}
else if (!isArray) {
value = routeValue;
}
do {
if (isArray) {
value = routeValue[idx];
}
if (value === Keys.integers || value === Keys.ranges) {
hash[depth] = '__I__';
}
else if (value === Keys.keys) {
hash[depth] ='__K__';
}
else {
hash[depth] = value;
}
// recurse down the routed token
if (depth + 1 !== route.length) {
getHashesFromRoute(route, depth + 1, hashes, hash);
}
// Or just add it to hashes
else {
hashes.push(cloneArray(hash));
}
} while (isArray && ++idx < length);
return hashes;
}