Chain Commander
Chain commander is a library based on bluebird library, to encapsulate
business rules logic in form of javascript objects or JSON.
Since it uses promises, it can keep going until no more "then"s are found, while mutating the value it started with.
You can store your commands in JSON or in a POJO (plain old javascript object), and execute them whenever you need,
asynchronously, associating them with a context.
You can change the business logic in your database/json definition files, and don't ever have to touch your frontend code,
it all depends on the context and your defined functions.
Usage
Install from NPM:
npm install chain-commander
Require it in your node:
var
Q = require('bluebird'),
cc = require('chain-commander')(Q);
or browser, after Bluebird:
<script src="bluebird.min.js"></script>
<script src="chain-commander.js"></script>
Business rules
Let's pretend you have a lot of users and each user have their own triggers and conditional, so they can
execute their own "code" in your app, like give instructions to your app to execute code in order they are meant to.
Basically, your definitions should not be aware of your object data, but your context functions should.
That means that if your object has an id, a name and some definitions, and if you want your function to
know the id, you should pass your objects to the execute
function or a literal argument.
You can store anything in the the rules, from coordinates, sequence of commands, conditional statements, pairs of data.
The rules are meant to be "non verbose", to be easy to write and adapt, when testing with new rules on the database or json file
for example. It's also meant to feel natural and familiar to javascript (if
and else
, brackets and strings).
They offload the dynamic nature of your applcation to where it belongs: your data. The application code should only be prepared
to deal with whatever you throw at it.
var cmds = [
{
if: {
check: [
['check', 'first'],
['check', 'second']
],
exec : [
['multiply', 10]
],
else : {
if : {
check: [
['check', 'third']
],
exec : [
['alert', {msg: 'aww yeah'}]
]
},
exec: [
['add', 1],
['alert', {msg: 'else'}]
]
}
}
},
{
exec: [
['alert', {msg: 'Doodidoo'}]
]
},
{
exec: [
['startTimer']
]
},
{
exec: [
['goOn']
]
}
];
Given the above object/JSON, you will create a new context that will deal with all the data in order:
var instance = {
myvalue : 3,
check : function (type, value){
if (type === 'first') {
return value > 0;
} else if (type === 'second') {
return value < 30;
}
return true;
},
getValue : function (){
return this.myvalue === 3;
},
multiply : function (number, value){
return value * number;
},
add : function (number, value){
return value + number;
},
log : function (args, value){
console.log(args.msg + ': ' + value);
return value;
},
goOn : function (value){
if (!this.secondPass) {
this.secondPass = true;
return cc.execute(value, instance);
}
},
startTimer: function (args, value){
var self = this;
return new Q(function(resolve, reject) {
if (self.secondPass === false) {
setTimeout(function (){
resolve('Oops');
}, 1000);
} else {
reject();
}
});
}
};
cc = CC(cmds);
cc.execute(10, instance).done(function (value){
console.log(value);
done();
});
The above is a "silly" example (although a long one).
For a full-fledged practical example, check the example
folder, there's a "build your plan with combos and many activities"
type of app created with Chain Commander and AngularJS (do a bower install
inside that folder first)
API
new ChainCommander(definitions: Array|string, options: Object = null)
Creates a new predefined chain that will execute your code when you call execute
var cc = new ChainCommander([
{
"exec":[
["oops"]
]
}
]);
When creating a ChainCommander
with the member
function, all executed functions will be passed as the last
parameter the current bound object, like so:
var
context = {
oops: function(value, thisobj){
}
},
obj = {
some:'data',
defs:[
{
"exec":[
["oops"]
]
}
]
}, cc = new ChainCommander(obj, {debug: true, throws: true, member: 'defs'});
cc.execute(0, context);
ChainCommander.prototype.execute(initialValue: any, context: Object|Function)
Executes your definitions on the given context. Returns a promise.
var
cc = new ChainCommander([
{
"exec":[
["oops"]
]
}
], {debug: true, throws: true}),
context = {
oops: function(obj){
obj.initial = 'b';
obj.value++;
return obj;
}
};
cc.execute({initial: 'a', value: 0}, context).done(function(value){
console.log(value);
});
ChainCommander.all(initialValue: any, arr: ChainCommander[], context: Object|Function, tap: Function = void 0)
Taking that you have an array with many Chain Commanders, that you want to execute in order, with the same context
returning a promise in the end:
var arrayOfCommanders = [
new ChainCommander(defs),
new ChainCommander(defs2),
new ChainCommander(defs3),
];
ChainCommander.all('initial value', arrayOfCommanders, context).done(function(value){
});
the arrayOfCommanders also allow an array of array of Commanders, that means, you can use it like this:
function getSelectedAndReturnArray(arr){
return arr
.filter(function(i){ return i.selected; })
.map(function(i){ return i.cmds; });
}
var arrayOfItemsThatGotCommanders1 = [createCommander(0), createCommander(1) ];
ChainCommander.all('initial value', [
getSelectedAndReturnArray(arrayOfItemsThatGotCommanders1),
commanderInstance
], context).then(function(value){
});
The optional tap
parameter is a functions that after each item, will be called. It's a side effect
callback and you can use it to "spy" on the current commands being executed
ChainCommander.all('initial value', arrayofcmds, context, console.log);
Debug
The definition and the executing code is "forgiving", it means, if you try to use a function that doesnt exists,
it will fail silently and continue in the chain.
You can watch the flow in a verbose mode setting the debug
option to true
var cc = ChainCommander(definitionArray, {debug: true, throws: true});
then watch your console. If you want to spot undefined functions or mismatched arguments, set throws
to true
.
Never use any of those in production, since they might generate a lot of unresolved promises.