Skip to content
Browse files

added unit tests for subflows, allowed subflows to run on one single …

…result
  • Loading branch information...
1 parent 355b590 commit 5643b59ab985f7955f6bf1ee2f4adf52e1c39bd8 @dfenster dfenster committed Mar 26, 2013
Showing with 281 additions and 19 deletions.
  1. +8 −0 README.md
  2. +29 −13 lib/fnFlow.js
  3. +244 −6 tests/001-flow.js
View
8 README.md
@@ -116,6 +116,14 @@ fnFlow.flow([
```
This does the exact same thing as the above example, but does it once for Brandon Sanderson and once for Jack Vance, in parallel.
+
+### flow.subFlow(dataName, tasks)
+
+This function pairs with the original _flow_ function to execute a sub flow within a parent. Given the name of a task or data from the parent flow,
+and a set of new tasks, it will execute the equivalent of one flow call from within another. It is most handy when you must invoke operations on each
+item in an array of results from a previous task in the flow, though, it will work on a single result as well.
+
+
## Authors
This library was developed by David Fenster at [Shutterstock](http://www.shutterstock.com)
View
42 lib/fnFlow.js
@@ -23,13 +23,20 @@ module.exports = {
var flowExec = function flowExec(data, tasks, contextName, callback) {
if (data instanceof Array) {
return async.map(data, function (dataItem, cb) {
- flowGo(dataItem, tasks, dataItem[contextName], function(err, results){
+ var context;
+ if(contextName) context = dataItem[contextName]
+ flowGo(dataItem, tasks, context, function(err, results){
if(contextName) cb(err, us.pick(results, Object.keys(tasks)));
else cb(err, results);
})
}, callback);
} else {
- return flowGo(data, tasks, null, callback);
+ var context;
+ if(contextName) context = data[contextName];
+ return flowGo(data, tasks, context, function(err, results){
+ if(contextName) callback(err, us.pick(results, Object.keys(tasks)));
+ else callback(err, results);
+ });
}
}
@@ -52,7 +59,7 @@ var interpretTask = function interpretTask(taskArgs, taskName, tasks, data, cont
if(taskArgs instanceof SubFlow) {
var subFlow = taskArgs;
- if(!tasks.hasOwnProperty(subFlow.dataName) && (!data || !data.hasOwnProperty(subFlow.dataName))) throw new FlowTaskError(taskName, "SubFlow data '" + subFlow.dataName + "' does not exist.");
+ if(!tasks.hasOwnProperty(subFlow.dataName) && (!data || !data.hasOwnProperty(subFlow.dataName)) || subFlow.dataName == taskName) throw new FlowTaskError(taskName, "SubFlow data '" + subFlow.dataName + "' does not exist. Provide the name of a task or data from the parent flow. Possible values include: " + Object.keys(tasks).concat(Object.keys(data)).filter(function(name){ return name != taskName }).join(', '));
fn = { receiverName: subFlow.dataName, tasks: subFlow.tasks };
var prereqs = scanForPrereqs(subFlow.tasks, us.extend({}, data, tasks));
autoTask.push(subFlow.dataName);
@@ -68,9 +75,9 @@ var interpretTask = function interpretTask(taskArgs, taskName, tasks, data, cont
fnIndex = i;
} else if(taskArg instanceof SubFlow) {
var subFlow = taskArg;
- if(fn) throw new FlowTaskError(taskName, "More than one function specified (at index " + fnIndex + " and " + i + ").");
+ if(fn) throw new FlowTaskError(taskName, "A task may have a SubFlow (index " + i + ") or a function call (index " + i + "), but not both. " + fnIndex + " and " + i + ").");
if(i < taskArgs.length - 1) throw new FlowTaskError(taskName, "SubFlows must be the at the last index.");
- if(!tasks.hasOwnProperty(subFlow.dataName) && (!data || !data.hasOwnProperty(subFlow.dataName))) throw new FlowTaskError(taskName, "SubFlow data '" + subFlow.dataName + "' does not exist.");
+ if(!tasks.hasOwnProperty(subFlow.dataName) && (!data || !data.hasOwnProperty(subFlow.dataName)) || subFlow.dataName == taskName) throw new FlowTaskError(taskName, "SubFlow data '" + subFlow.dataName + "' does not exist. Provide the name of a task or data from the parent flow. Possible values include: " + Object.keys(tasks).concat(Object.keys(data)).filter(function(name){ return name != taskName }).join(', '));
fn = { receiverName: subFlow.dataName, tasks: subFlow.tasks };
fnIndex = i;
var prereqs = scanForPrereqs(subFlow.tasks, us.extend({}, data, tasks));
@@ -86,7 +93,6 @@ var interpretTask = function interpretTask(taskArgs, taskName, tasks, data, cont
if(!fn.receiverName) throw new FlowTaskError(taskName, "Unknown symbol at index '" + i + "' must be either the name of a task, the name of data, or be the name of a function on the result of a task or data");
}
}
- if(!fn) console.log(taskArgs)
if(!fn) throw new FlowTaskError(taskName, "Function required.");
} else {
throw new FlowTaskError(taskName, "Invalid flow type. Must be function, or array.");
@@ -100,14 +106,21 @@ var flowCall = function flowCall(taskPlan, taskName) {
var args = taskPlan.args;
var autoTask = taskPlan.autoTask;
- if(typeof fn == 'object' && fn.tasks) {
+ if(typeof fn == 'object' && fn.tasks) { //subflows
autoTask.push(function(cb, results){
- var arrayData = results[fn.receiverName];
- var newData = arrayData.map(function(dataItem){
- var newDataItem = us.extend({}, results);
- newDataItem[fn.receiverName] = dataItem;
- return newDataItem;
- });
+ var rawResultData = results[fn.receiverName];
+ if(rawResultData === null || rawResultData === undefined) {
+ throw new FlowTaskError(taskName, "Result of '" + fn.receiverName + "' returned no data. Could not start SubFlow.");
+ } else if(Array.isArray(rawResultData)) {
+ var newData = rawResultData.map(function(dataItem){
+ var newDataItem = us.extend({}, results);
+ newDataItem[fn.receiverName] = dataItem;
+ return newDataItem;
+ });
+ } else {
+ var newData = us.extend({}, results);
+ newData[fn.receiverName] = rawResultData;
+ }
flowExec(newData, fn.tasks, fn.receiverName, cb);
});
} else {
@@ -163,9 +176,12 @@ var scanForPrereqs = function scanForPrereqs(tasks, allTasks) {
}
+
var SubFlow = function(dataName, tasks){
this.dataName = dataName;
this.tasks = tasks;
+ if(!dataName) throw new Error("SubFlow error: No data given for subFlow. Provide the name of a task or data from the parent flow.");
+ if(!tasks) throw new Error("SubFlow error: No tasks given for subFlow.");
};
module.exports.flow.subFlow = function(dataName, tasks) {
return new SubFlow(dataName, tasks);
View
250 tests/001-flow.js
@@ -664,9 +664,9 @@ module.exports["array result data execution"] = function(test){
}, {
getGenre: [Genre.getByName, 'genreName'],
getBooksByGenre: ['getGenre', 'getBooks'],
- getAuthors: ['getBooksByGenre', {
+ getAuthors: flow.subFlow('getBooksByGenre', {
getBookAuthor: ['getBooksByGenre', 'getAuthor']
- }]
+ })
}, function(err, results){
test.ok(!err, 'no error');
test.deepEqual(results, {
@@ -696,9 +696,9 @@ module.exports["array result data execution with context"] = function(test){
}, {
getGenre: [Genre.getByName, 'genreName'],
getBooksByGenre: ['getGenre', 'getBooks'],
- getAuthors: ['getBooksByGenre', {
+ getAuthors: flow.subFlow('getBooksByGenre', {
getBookAuthor: [Author.getById, 'authorId']
- }]
+ })
}, function(err, results){
test.ok(!err, 'no error');
test.deepEqual(results, {
@@ -762,9 +762,9 @@ module.exports["array result data execution with prereqs using subFlow"] = funct
}, {
getGenre: [Genre.getByName, 'genreName'],
getBooksByGenre: ['getGenre', 'getBooks'],
- getAuthors: ['getBooksByGenre', {
+ getAuthors: flow.subFlow('getBooksByGenre', {
getHambly2: [Author.getById, 'getHambly']
- }],
+ }),
getHambly: [Author.getByName, 'authorName']
}, function(err, results){
test.ok(!err, 'no error');
@@ -867,6 +867,244 @@ module.exports["two nested subflows with prereqs"] = function(test){
}
+module.exports["subflow with empty name"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: flow.subFlow('', {
+ getBookAuthor: ['getBooksByGenre', 'getAuthor']
+ })
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e) {
+ test.ok(e, 'got an error');
+ test.equals(e.name, "Error", "got Error");
+ test.equals(e.message, "SubFlow error: No data given for subFlow. Provide the name of a task or data from the parent flow.", "error message match")
+ test.done();
+ }
+}
+
+module.exports["subflow with no tasks"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: flow.subFlow('getBooksByGenre', null)
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e) {
+ test.ok(e, 'got an error');
+ test.equals(e.name, "Error", "got Error");
+ test.equals(e.message, "SubFlow error: No tasks given for subFlow.", "error message match")
+ test.done();
+ }
+}
+
+
+module.exports["subflow with bad data name"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: flow.subFlow('asdf', {
+ getBookAuthor: ['getBooksByGenre', 'getAuthor']
+ })
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e) {
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getAuthors': SubFlow data 'asdf' does not exist. Provide the name of a task or data from the parent flow. Possible values include: getGenre, getBooksByGenre, genreName", "error message match")
+ test.done();
+ }
+}
+
+module.exports["subflow task with own data name"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: flow.subFlow('getAuthors', {
+ getBookAuthor: ['getBooksByGenre', 'getAuthor']
+ })
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e) {
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getAuthors': SubFlow data 'getAuthors' does not exist. Provide the name of a task or data from the parent flow. Possible values include: getGenre, getBooksByGenre, genreName", "error message match")
+ test.done();
+ }
+}
+
+
+module.exports["subflow with explicit prereq"] = function(test){
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ assertGenreExistence: [Genre.assertExistence, 'getGenre'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: ['assertGenreExistence', flow.subFlow('getBooksByGenre', {
+ getBookAuthor: [Author.getById, 'authorId']
+ })]
+ }, function(err, results){
+ test.ok(!err, 'no error');
+ test.deepEqual(results, {
+ genreName: 'Fantasy',
+ assertGenreExistence: true,
+ getGenre: Genre.all[1],
+ getBooksByGenre: [Book.all[7], Book.all[8], Book.all[9], Book.all[10], Book.all[11]],
+ getAuthors: [{
+ getBookAuthor: Author.all[6]
+ }, {
+ getBookAuthor: Author.all[6]
+ }, {
+ getBookAuthor: Author.all[5]
+ }, {
+ getBookAuthor: Author.all[5]
+ }, {
+ getBookAuthor: Author.all[5]
+ }]
+ });
+ test.done();
+ });
+}
+
+module.exports["subflow out of order"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ assertGenreExistence: [Genre.assertExistence, 'getGenre'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: [flow.subFlow('getBooksByGenre', {
+ getBookAuthor: [Author.getById, 'authorId']
+ }), 'assertGenreExistence']
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e){
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getAuthors': SubFlows must be the at the last index.", "error message match")
+ test.done();
+ }
+}
+
+
+module.exports["subflow in task array with bad data name"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ assertGenreExistence: [Genre.assertExistence, 'getGenre'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: ['assertGenreExistence', flow.subFlow('asdf', {
+ getBookAuthor: [Author.getById, 'authorId']
+ })]
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e){
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getAuthors': SubFlow data 'asdf' does not exist. Provide the name of a task or data from the parent flow. Possible values include: getGenre, assertGenreExistence, getBooksByGenre, genreName", "error message match")
+ test.done();
+ }
+}
+
+
+module.exports["subflow in task array with own data name"] = function(test){
+ try {
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ assertGenreExistence: [Genre.assertExistence, 'getGenre'],
+ getBooksByGenre: ['getGenre', 'getBooks'],
+ getAuthors: ['assertGenreExistence', flow.subFlow('getAuthors', {
+ getBookAuthor: [Author.getById, 'authorId']
+ })]
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e){
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getAuthors': SubFlow data 'getAuthors' does not exist. Provide the name of a task or data from the parent flow. Possible values include: getGenre, assertGenreExistence, getBooksByGenre, genreName", "error message match")
+ test.done();
+ }
+}
+
+
+
+module.exports["single data subflow execution"] = function(test){
+ flow({
+ genreName: 'Fantasy'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: flow.subFlow('getGenre', {
+ getBooksByGenre: [Book.findByGenreId, 'id']
+ })
+ }, function(err, results){
+ test.ok(!err, 'no error');
+ test.deepEqual(results, {
+ genreName: 'Fantasy',
+ getGenre: Genre.all[1],
+ getBooksByGenre: {
+ getBooksByGenre: [Book.all[7], Book.all[8], Book.all[9], Book.all[10], Book.all[11]]
+ }
+ });
+ test.done();
+ });
+}
+
+
+module.exports["single null data subflow execution"] = function(test){
+ try {
+ flow({
+ genreName: 'Chainsaw slashers'
+ }, {
+ getGenre: [Genre.getByName, 'genreName'],
+ getBooksByGenre: flow.subFlow('getGenre', {
+ getBooksByGenre2: [Book.findByGenreId, 'id']
+ })
+ }, function(err, results){
+ test.fail(null, null, "no error received");
+ test.done();
+ });
+ } catch(e){
+ test.ok(e, 'got an error');
+ test.equals(e.name, "FlowTaskError", "got Error");
+ test.equals(e.message, "Flow error in 'getBooksByGenre': Result of 'getGenre' returned no data. Could not start SubFlow.", "error message match")
+ test.done();
+ }
+}
+
+
// module.exports["array result data execution"] = function(test){
// var authors, fantasyAuthors;
// fantasyAuthors = new FnFlow({

0 comments on commit 5643b59

Please sign in to comment.
Something went wrong with that request. Please try again.