Business Rules for JavaScript
When you're working in a business environment (especially an "enterprisy" environment), business rules make the world go 'round. Business-level decision makers need to be able to alter application behavior and logic with minimal technical invasiveness.
Usually, business rule engines come packaged as $100K Java installations and a gaggle of business consultants. Let's just call this a lighter-weight solution.
The goal of this library is to:
- Give JavaScript developers drop-in jQuery UI widgets for building business rule interfaces.
- Give developers a rule engine that can be ported to any server-side language for running business rules built with the UI tools.
Examples
Checkout the examples directory if you just want to dive in, it demonstrates all the features pretty well!
$.fn.conditionsBuilder()
The $.fn.conditionsBuilder()
method has two forms, the first being:
$("#myDiv").conditionsBuilder({fields: [...], data: {...});
The first form creates a ConditionsBuilder
object for the given DOM element with the passed fields and data.
The fields
param is an array of objects that define the factors that can be used in conditional statements.
Each has a label
, name
and an array of operators
. It may also have an options
array of objects
with label
and name
.
Each operator is an object with label
, name
and fieldType
. The fieldType
can be:
"none"
- The operator does not require further data entry (ie"present"
,"blank"
)."text"
- User will be presented with an input oftype=text
."textarea"
- User will be presented with a textarea."select"
- User will be presented with a select dropdown populated with the parent field'soptions
array.
The data
param is an object that will be used to initially populate the UI (ie if business rules have already
been created and the user wants to edit them). If the data
option is not passed, the UI will be
generated without any initial conditions.
The object passed as data
should be a "conditional object", meaning it has a single key of all
or any
and a value of an array of nodes. These nodes can either be rule objects or nested conditional objects.
A rule object has name
, operator
and value
strings. The name
should match a field's name
property,
the operator
should match an operator's name
property, and the value
is an arbitrary string value
entered by the user in the UI.
Once the UI has been built by the ConditionsBuilder
and the user has entered information, the data can
be retrieved by using the second form of the conditionsBuilder
method:
var data = $("#myDiv").conditionsBuilder("data");
This will serialize the entered conditionals into a data object. This object can be persisted and then later
used to create a new ConditionsBuilder
for editing. This data object will also be used to instantiate
a BusinessRules.RuleEngine
object for running the conditional logic.
$.fn.actionsBuilder()
The $.fn.actionsBuilder
has an identical API to $.fn.conditionsBuilder
, but it uses a different data structure.
The fields
property should be an array of action objects. Each action object has a label
and name
.
An action object may have a fields
property that is an array of action objects, allowing for nested action data.
All action objects that are not "top level" should also have a fieldType
of text
, textarea
or select
.
Here's an example of what a "Send Email" action could look like:
$("#myDiv").actionsBuilder({fields: [
{label: "Send Email", value: "sendEmail", fields: [
{label: "To", name: "to", fieldType: "text"},
{label: "CC", name: "cc", fieldType: "text"},
{label: "BCC", name: "bcc", fieldType: "text"},
{label: "Subject", name: "subject", fieldType: "text"},
{label: "Body", name: "body", fieldType: "textarea"}
]}
]});
Action objects with a fieldType
of select
should not have a fields
property -- rather they have an options
property with a label
and name
for each option. That option object, however, can have a fields
property.
This allows you to specify nested fields that will only be displayed if the given option has been selected.
Building on the last example, this allows the user to specify an email template, or use a custom Subject and Body:
$("#myDiv").actionsBuilder({fields: [
{label: "Send Email", value: "sendEmail", fields: [
{label: "To", name: "to", fieldType: "text"},
{label: "CC", name: "cc", fieldType: "text"},
{label: "BCC", name: "bcc", fieldType: "text"},
{label: "Email Template", name: "template", fieldType: "select", options: [
{label: "Welcome Email", name: "welcomeEmail"},
{label: "Followup Email", name: "followupEmail"},
{label: "Custom Email", name: "customEmail", fields: [
{label: "Subject", name: "subject", fieldType: "text"},
{label: "Body", name: "body", fieldType: "textarea"}
]}
]}
]}
]});
In this example, the "Subject" and "Body" fields will only be displayed if the user has selected the "Custom Email" template option.
To get the data out of the UI, run the actionsBuilder
method with "data"
:
var data = $("#myDiv").actionsBuilder("data");
Each action data object has a name
that matches the corresponding field's value
, and a value
property with the
user-entered value. It may also have a fields
array of nested action data objects, which correspond to the nested
field structure of the builder.
BusinessRules.RuleEngine
While the ConditionsBuilder
and ActionsBuilder
give us a UI to build business rule configurations, we still need
something to interpret the configuration, apply the logic and conditionally run the actions. This is where the
BusinessRules.RuleEngine
comes in.
The RuleEngine
is initialized with a conditions
object and an actions
array, just as they would be when fetched
from the UI using conditionsBuilder("data")
and actionsBuilder("data")
. This would be a common way of instantiating
a RuleEngine
:
var engine = new BusinessRules.RuleEngine({
conditions: $("#myConditions").conditionsBuilder("data"),
actions: $("#myActions").actionsBuilder("data")
});
Once your engine has been instantiated, you can use the #run
method to apply the conditional logic to a set of data,
and then conditionally run the actions. Since the engine is only responsible for running logic and shouldn't have to
be aware of the actual data, you need to pass in an object that represents the context to run conditionals on,
and another object with functions that map to the actions. For example:
var engine = new BusinessRules.RuleEngine({
conditions: {all: [{name: "name", operator: "present", value: ""}, {name: "age", operator: "greaterThanEqual", value: "21"}]},
actions: [{name: "action-select", value: "giveDrink", fields: [{name: "drinkType", value: "martini"}]}]
});
var conditionsAdapter = {name: "Joe", age: 22};
var actionsAdapter = {giveDrink: function(data) { alert("Gave user a " + data.find("drinkType")); } };
engine.run(conditionsAdapter, actionsAdapter);
Values used in the conditionsAdapter
can be simple strings and numbers, but it can also be a function that will be lazily executed.
For example, this adapter would pull the name and age from fields on the page (a more likely scenario than hard coded values):
var conditionsAdapter = {
name: function() { $("#nameField").val(); },
age: function() { $("#ageField").val(); }
};
It is also possible to use asynchronous functions in your conditionsAdapter. To do so, have your function accept a callback function and call it when you have your value.
var conditionsAdapter = {
logoVisible: function(done) {
// Cannot determine if logo is visible until DOM ready
$(function() {
var visible = $("#logo").is(":visible");
done(visible);
});
}
};
The BusinessRules.RuleEngine
object can be used in either the browser or in a server environment (ie Node.js). It could also
be ported to another language simply enough, or run inside a JavaScript runtime within Ruby, Java, etc.
Conditional Operators
The RuleEngine
comes with the following standard operators that can be used inside conditionals:
- present
- blank
- equalTo
- notEqualTo
- greaterThan
- greaterThanEqual
- lessThan
- lessThanEqual
- includes
- matchesRegex
Custom operators can be added to a RuleEngine
using the addOperators
method:
var engine = new BusinessRules.RuleEngine({
conditions: {all: [{name: "password", operator: "longerThan", value: "6"}]},
actions: []
});
engine.addOperators({
longerThan: function(actual, length) {
return actual.length > parseInt(length, 10);
}
});
It is also possible to create asynchronous operators if your logic cannot be run synchronously. To do so, simply have your operator function accept a third callback param and call it when you have your result:
engine.addOperators({
delayedOperator: function(actual, target, done) {
setTimeout(function() { done(true); }, 1000);
}
});
The addOperators
method also allows you to override the standard operators if, heaven forbid, you find that necessary.
Action Functions
When a function on your actionsAdapter
object is called, it is passed a Finder
object. The Finder
object has a
data
property that will return the action's data structure so that you can traverse it yourself. This data
structure can look something like:
[{name: "drinkType", value: "martini", fields: [
{name: "oliveCount", value: "3"},
{name: "shaken", value: "yes"}
]}]
While you certainly can traverse this structure, chances are you just want to quickly access the values. This is why
the Finder
gives you the find
convenience method, which takes one or more names and returns the matching value:
var actionsAdapter = {
giveDrink: function(data) {
var drinkType = data.find("drinkType"),
oliveCount = data.find("drinkType", "oliveCount"),
shaken = data.find("drinkType", "shaken");
console.log(drinkType, oliveCount, shaken); // "martini", "3", "yes"
}
};