Skip to content

Commit

Permalink
Merge pull request #160 from michaelbpaulson/on-unhandled-set
Browse files Browse the repository at this point in the history
feat(unhandled*): The set feature for unhandled paths.
  • Loading branch information
ThePrimeagen committed Dec 15, 2015
2 parents 8948460 + a7d959d commit 64f47b3
Show file tree
Hide file tree
Showing 6 changed files with 566 additions and 129 deletions.
42 changes: 27 additions & 15 deletions src/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ var Keys = require('./Keys');
var parseTree = require('./parse-tree');
var matcher = require('./operations/matcher');
var recurseMatchAndExecute = require('./run/recurseMatchAndExecute');
var runSetAction = require('./run/set/runSetAction');
var runCallAction = require('./run/call/runCallAction');
var set = 'set';
var call = 'call';
var pathUtils = require('falcor-path-utils');
var collapse = pathUtils.collapse;
Expand Down Expand Up @@ -39,7 +37,7 @@ Router.prototype = {
/**
* Performs the get algorithm on the router.
* @param {PathSet[]} paths -
* @returns {JSONGraphEnvelope}
* @returns {Observable.<JSONGraphEnvelope>}
*/
get: require('./router/get'),

Expand All @@ -54,19 +52,33 @@ Router.prototype = {
this._unhandled.get = unhandled(this, unhandledHandler);
},

set: function(jsong) {
/**
* Takes in a jsonGraph and outputs a Observable.<jsonGraph>. The set
* method will use get until it evaluates the last key of the path inside
* of paths. At that point it will produce an intermediate structure that
* matches the path and has the value that is found in the jsonGraph env.
*
* One of the requirements for interaction with a dataSource is that the
* set message must be optimized to the best of the incoming sources
* knowledge.
*
* @param {JSONGraphEnvelope} jsonGraph -
* @returns {Observable.<JSONGraphEnvelope>}
*/
set: require('./router/set'),

var jsongCache = {};
var action = runSetAction(this, jsong, jsongCache);
return run(this._matcher, action, jsong.paths, set, this, jsongCache).

// Turn it(jsongGraph, invalidations, missing, etc.) into a
// jsonGraph envelope
map(function(details) {
return {
jsonGraph: details.jsonGraph
};
});
/**
* Takes in a function to call that has the same return inteface as any
* route that will be called in the event of "unhandledPaths" on a set.
*
* What will come into the set function will be the subset of the jsonGraph
* that represents the unhandledPaths of set.
*
* @param {Function} unhandledHandler -
* @returns {undefined}
*/
onUnhandledSet: function(unhandledHandler) {
this._unhandled.set = unhandled(this, unhandledHandler);
},

call: function(callPath, args, suffixes, paths) {
Expand Down
27 changes: 27 additions & 0 deletions src/operations/matcher/intersection/hasIntersectionWithTree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @param {PathSet} path - A simple path
* @param {Object} tree - The tree should have `null` leaves to denote a
* leaf node.
*/
module.exports = function hasIntersectionWithTree(path, tree) {
return _hasIntersection(path, tree, 0);
};

function _hasIntersection(path, node, depth) {

// Exit / base condition. We have reached the
// length of our path and we are at a node of null.
if (depth === path.length && node === null) {
return true;
}

var key = path[depth];
var next = node[key];

// If its not undefined, then its a branch.
if (node !== undefined) {
return _hasIntersection(path, next, depth + 1);
}

return false;
}
132 changes: 132 additions & 0 deletions src/router/set.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
var set = 'set';
var recurseMatchAndExecute = require('./../run/recurseMatchAndExecute');
var runSetAction = require('./../run/set/runSetAction');
var materialize = require('../run/materialize');
var Observable = require('rx').Observable;
var spreadPaths = require('./../support/spreadPaths');
var pathValueMerge = require('./../cache/pathValueMerge');
var optimizePathSets = require('./../cache/optimizePathSets');
var hasIntersectionWithTree =
require('./../operations/matcher/intersection/hasIntersectionWithTree');
var getValue = require('./../cache/getValue');
var normalizePathSets = require('../operations/ranges/normalizePathSets');
var pathUtils = require('falcor-path-utils');
var collapse = pathUtils.collapse;

/**
* @returns {Observable.<JSONGraph>}
* @private
*/
module.exports = function routerSet(jsonGraph) {
var jsongCache = {};
var router = this;
var action = runSetAction(router, jsonGraph, jsongCache);
jsonGraph.paths = normalizePathSets(jsonGraph.paths);
return recurseMatchAndExecute(router._matcher, action, jsonGraph.paths,
set, router, jsongCache).

// Takes the jsonGraphEnvelope and extra details that comes out of the
// recursive matching algorithm and either attempts the fallback
// options or returns the built jsonGraph.
flatMap(function(details) {
var out = {
jsonGraph: details.jsonGraph
};

// If there is an unhandler then we should call that method and
// provide the subset of jsonGraph that represents the missing
// routes.
if (details.unhandledPaths.length && router._unhandled.set) {
var unhandledPaths = details.unhandledPaths;
var jsonGraphFragment = {};

// PERFORMANCE: We know this is a potential performance downfall
// PERFORMANCE: but we want to see if its even a corner case.
// PERFORMANCE: Most likely this will not be hit, but if it does
// PERFORMANCE: then we can take care of it
// Set is interesting. This is what has to happen.
// 1. incoming paths are spread so that each one is simple.
// 2. incoming path, one at a time, are optimized by the
// incoming jsonGraph.
// 3. test intersection against incoming optimized path and
// unhandledPathSet
// 4. If 3 is true, build the jsonGraphFragment by using a
// pathValue of optimizedPath and vale from un-optimized
// path and original jsonGraphEnvelope.
var jsonGraphEnvelope = {jsonGraph: jsonGraphFragment};
var unhandledPathsTree = unhandledPaths.
reduce(function(acc, path) {
pathValueMerge(acc, {path: path, value: null});
return acc;
}, {});

// 1. Spread
var pathIntersection = spreadPaths(jsonGraph.paths).

// 2.1 Optimize. We know its one at a time therefore we
// just pluck [0] out.
map(function(path) {
return [
// full path
path,

// optimized path
optimizePathSets(details.jsonGraph, [path],
router.maxRefFollow)[0]]
}).

// 2.2 Remove all the optimized paths that were found in
// the cache.
filter(function(x) { return x[1]; }).

// 3.1 test intersection.
map(function(pathAndOPath) {
var oPath = pathAndOPath[1];
var hasIntersection = hasIntersectionWithTree(
oPath, unhandledPathsTree);

// Creates the pathValue if there are a path
// intersection
if (hasIntersection) {
var value =
getValue(jsonGraph.jsonGraph, pathAndOPath[0]);

return {
path: oPath,
value: value
};
}

return null;
}).

// 3.2 strip out nulls (the non-intersection paths).
filter(function(x) { return x !== null; });

// 4. build the optimized JSONGraph envelope.
pathIntersection.
reduce(function(acc, pathValue) {
pathValueMerge(acc, pathValue);
return acc;
}, jsonGraphFragment);

jsonGraphEnvelope.paths = collapse(
pathIntersection.map(function(pV) {
return pV.path;
}));

return router._unhandled.set(out, unhandledPaths,
jsonGraphEnvelope);
}

return Observable.return(out);
}).

// We will continue to materialize over the whole jsonGraph message.
// This makes sense if you think about pathValues and an API that
// if ask for a range of 10 and only 8 were returned, it would not
// materialize for you, instead, allow the router to do that.
map(function(jsonGraphEnvelope) {
return materialize(router, jsonGraph.paths, jsonGraphEnvelope);
});
};
114 changes: 114 additions & 0 deletions test/unit/functional/unhandled.get.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
var R = require('../../../src/Router');
var noOp = function() {};
var chai = require('chai');
var expect = chai.expect;
var sinon = require('sinon');
var pathValueMerge = require('./../../../src/cache/pathValueMerge');
var Observable = require('rx').Observable;
var $atom = require('./../../../src/support/types').$atom;

describe('#get', function() {
it('should return an empty Observable and just materialize values.', function(done) {
var router = new R([]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return Observable.empty();
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', 'summary']]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: {$type: $atom}
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});
it('should call the onUnhandledGet when the route completely misses a route.', function(done) {
var router = new R([]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return paths.reduce(function(jsonGraph, path) {
pathValueMerge(jsonGraph.jsonGraph, {
path: path,
value: 'missing'
});
return jsonGraph;
}, {jsonGraph: {}});
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', 'summary']]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: 'missing'
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});

it('should call the onUnhandledGet when the route partially misses a route.', function(done) {
var router = new R([{
route: 'videos.length',
get: function() {
return {
path: ['videos', 'length'],
value: 5
};
}
}]);
var onUnhandledPaths = sinon.spy(function convert(paths) {
return paths.reduce(function(jsonGraph, path) {
pathValueMerge(jsonGraph.jsonGraph, {
path: path,
value: 'missing'
});
return jsonGraph;
}, {jsonGraph: {}});
});
router.onUnhandledGet(onUnhandledPaths);

var obs = router.
get([['videos', ['length', 'summary']]]);
var onNext = sinon.spy();
obs.
doAction(onNext, noOp, function() {
expect(onNext.calledOnce).to.be.ok;
expect(onNext.getCall(0).args[0]).to.deep.equals({
jsonGraph: {
videos: {
summary: 'missing',
length: 5
}
}
});
expect(onUnhandledPaths.calledOnce).to.be.ok;
expect(onUnhandledPaths.getCall(0).args[0]).to.deep.equals([
['videos', 'summary']
]);
}).
subscribe(noOp, done, done);
});
});
Loading

0 comments on commit 64f47b3

Please sign in to comment.