diff --git a/lib/compile/index.js b/lib/compile/index.js
index 234975c..095488e 100644
--- a/lib/compile/index.js
+++ b/lib/compile/index.js
@@ -94,7 +94,7 @@ var createRuleFromObject = (function () {
function parseRule(rule, conditions, identifiers, defined, name) {
if (rule.length) {
var r0 = rule[0];
- if (r0 === "not" || r0 === "exists") {
+ if (r0 === "not" || r0 === "exists" || r0 === "collect" ) {
var temp = [];
rule.shift();
__resolveRule(rule, identifiers, temp, defined, name);
@@ -135,7 +135,10 @@ var createRuleFromObject = (function () {
forEach(constraints, function (rule) {
parseRule(rule, conditions, identifiers, defined, name);
});
- return rules.createRule(name, options, conditions, parseAction(action, identifiers, defined, scope));
+ var ruleScope = merge(defined, scope);
+ var copyOptions = merge({}, options);
+ copyOptions.scope = ruleScope;
+ return rules.createRule(name, copyOptions, conditions, parseAction(action, identifiers, defined, scope));
};
})();
@@ -158,7 +161,7 @@ exports.compile = function (flowObj, options, cb, Container) {
throw new Error("Name must be present in JSON or options");
}
var flow = new Container(name);
- var defined = merge({Array: Array, String: String, Number: Number, Boolean: Boolean, RegExp: RegExp, Date: Date, Object: Object}, options.define || {});
+ var defined = merge({Array: Array, String: String, Number: Number, Boolean: Boolean, RegExp: RegExp, Date: Date, Object: Object}, ( (options.define || options.defines || options.defined) || {}));
if (typeof Buffer !== "undefined") {
defined.Buffer = Buffer;
}
diff --git a/lib/context.js b/lib/context.js
index cc876aa..28b8ac9 100644
--- a/lib/context.js
+++ b/lib/context.js
@@ -5,6 +5,8 @@ var extd = require("./extended"),
indexOf = extd.indexOf,
pPush = Array.prototype.push;
+var Match = require('./match.js');
+
function createContextHash(paths, hashCode) {
var ret = "",
i = -1,
@@ -16,77 +18,28 @@ function createContextHash(paths, hashCode) {
return ret;
}
-function merge(h1, h2, aliases) {
- var i = -1, l = aliases.length, alias;
- while (++i < l) {
- alias = aliases[i];
- h1[alias] = h2[alias];
- }
-}
-
-function unionRecency(arr, arr1, arr2) {
- pPush.apply(arr, arr1);
- var i = -1, l = arr2.length, val, j = arr.length;
+function createPathstHash(paths, hashCode) {
+ var ret = "",
+ i = -1,
+ l = paths.length;
while (++i < l) {
- val = arr2[i];
- if (indexOf(arr, val) === -1) {
- arr[j++] = val;
- }
+ ret += paths[i].id + ":";
}
+ return ret;
}
-var Match = declare({
- instance: {
- isMatch: true,
- hashCode: "",
- facts: null,
- factIds: null,
- factHash: null,
- recency: null,
- aliases: null,
- constructor: function () {
- this.facts = [];
- this.factIds = [];
- this.factHash = {};
- this.recency = [];
- this.aliases = [];
- },
-
- addFact: function (assertable) {
- pPush.call(this.facts, assertable);
- pPush.call(this.recency, assertable.recency);
- pPush.call(this.factIds, assertable.id);
- this.hashCode = this.factIds.join(":");
- return this;
- },
-
- merge: function (mr) {
- var ret = new Match();
- ret.isMatch = mr.isMatch;
- pPush.apply(ret.facts, this.facts);
- pPush.apply(ret.facts, mr.facts);
- pPush.apply(ret.aliases, this.aliases);
- pPush.apply(ret.aliases, mr.aliases);
- ret.hashCode = this.hashCode + ":" + mr.hashCode;
- merge(ret.factHash, this.factHash, this.aliases);
- merge(ret.factHash, mr.factHash, mr.aliases);
- unionRecency(ret.recency, this.recency, mr.recency);
- return ret;
- }
- }
-});
var Context = declare({
instance: {
- match: null,
- factHash: null,
- aliases: null,
- fact: null,
- hashCode: null,
- paths: null,
- pathsHash: null,
+ match: null,
+ factHash: null,
+ aliases: null,
+ fact: null,
+ hashCode: null,
+ paths: null,
+ pathsHash: null,
constructor: function (fact, paths, mr) {
this.fact = fact;
diff --git a/lib/extended.js b/lib/extended.js
index 3468567..9e800d9 100644
--- a/lib/extended.js
+++ b/lib/extended.js
@@ -1,4 +1,5 @@
var arr = require("array-extended"),
+ is = require("is-extended"),
unique = arr.unique,
indexOf = arr.indexOf,
map = arr.map,
@@ -126,6 +127,27 @@ function diffHash(h1, h2) {
function union(arr1, arr2) {
return unique(arr1.concat(arr2));
}
+
+function findIndex(coll, item) {
+ var target = -1;
+ if( 'function' === typeof item ) {
+ coll.some(function(e, i) {
+ if(item(e,i)) {
+ target = i;
+ return true;
+ };
+ });
+ }
+ else {
+ coll.some(function(e, i) {
+ if(is.deepEqual(e, item)) {
+ target = i;
+ return true;
+ };
+ });
+ }
+ return target;
+}
module.exports = require("extended")()
.register(require("date-extended"))
@@ -145,5 +167,6 @@ module.exports = require("extended")()
.register("HashTable", require("ht"))
.register("declare", require("declare.js"))
.register(require("leafy"))
+ .register('findIndex', findIndex)
.register("LinkedList", require("./linkedList"));
diff --git a/lib/match.js b/lib/match.js
new file mode 100644
index 0000000..c819cbb
--- /dev/null
+++ b/lib/match.js
@@ -0,0 +1,73 @@
+"use strict";
+var extd = require("./extended"),
+ isBoolean = extd.isBoolean,
+ declare = extd.declare,
+ indexOf = extd.indexOf,
+ pPush = Array.prototype.push;
+
+
+function merge(h1, h2, aliases) {
+ var i = -1, l = aliases.length, alias;
+ while (++i < l) {
+ alias = aliases[i];
+ h1[alias] = h2[alias];
+ }
+}
+
+function unionRecency(arr, arr1, arr2) {
+ pPush.apply(arr, arr1);
+ var i = -1, l = arr2.length, val, j = arr.length;
+ while (++i < l) {
+ val = arr2[i];
+ if (indexOf(arr, val) === -1) {
+ arr[j++] = val;
+ }
+ }
+}
+
+var Match = declare({
+ instance: {
+
+ isMatch: true,
+ hashCode: "",
+ facts: null,
+ factIds: null,
+ factHash: null,
+ recency: null,
+ aliases: null,
+
+ constructor: function () {
+ this.facts = [];
+ this.factIds = [];
+ this.factHash = {};
+ this.recency = [];
+ this.aliases = [];
+ },
+
+ addFact: function (assertable) {
+ pPush.call(this.facts, assertable);
+ pPush.call(this.recency, assertable.recency);
+ pPush.call(this.factIds, assertable.id);
+ this.hashCode = this.factIds.join(":");
+ return this;
+ },
+ merge: function (mr) {
+ var ret = new Match();
+ ret.isMatch = mr.isMatch;
+ pPush.apply(ret.facts, this.facts);
+ pPush.apply(ret.facts, mr.facts);
+ pPush.apply(ret.factIds, this.factIds);
+ pPush.apply(ret.factIds, mr.factIds);
+ pPush.apply(ret.aliases, this.aliases);
+ pPush.apply(ret.aliases, mr.aliases);
+ ret.hashCode = this.hashCode + ":" + mr.hashCode;
+ merge(ret.factHash, this.factHash, this.aliases);
+ merge(ret.factHash, mr.factHash, mr.aliases);
+ unionRecency(ret.recency, this.recency, mr.recency);
+ return ret;
+ }
+ }
+}).as(module);
+
+
+
diff --git a/lib/nodes/collectNode.js b/lib/nodes/collectNode.js
new file mode 100644
index 0000000..258dddb
--- /dev/null
+++ b/lib/nodes/collectNode.js
@@ -0,0 +1,264 @@
+var extd = require("../extended");
+var FromNode = require("./fromNode");
+var Context = require('../context.js');
+var Match = require('../match.js');
+var extdObj = require("object-extended");
+
+var DEFAULT_MATCH = {
+ isMatch: function () {
+ return false;
+ }
+};
+
+/**
+*/
+FromNode.extend({
+ instance: {
+ nodeType: "CollectNode",
+ //
+ constructor: function (pattern, wm) {
+ this._super([pattern, wm]);
+ this.setAliases = pattern.setAliases;
+ this.collectionHash = {};
+ this.isCollectionObject = this.type({}) ? true : false
+ this.fnCollectionSrc = pattern.fnCollectionSrc;
+ },
+
+ __createMatches: function (lc) {
+ var me = this
+ ,fc = me.getFilteredContext(lc)
+ ,rc = me.collectionHash[fc.hashCode]
+ ,lcFh = lc.factHash
+ ,verb = 'assert'
+ ,createdContext, collection, collFact, match, setHash;
+ //
+ if(!rc) {
+ // either an array [obj, obj,...] or { key: [], key2: [] }
+ if( me.fnCollectionSrc ) {
+ collFact = me.fnCollectionSrc(me.workingMemory);
+ collection = collFact.object;
+ }
+ else {
+ collection = me.isCollectionObject ? {} : [];
+ collFact = me.workingMemory.getFactHandle(collection);
+ }
+ rc = new Context(collFact, null, null);
+ rc.set(me.alias, collection);
+ rc.match = fc.match.merge(rc.match); // the match doesn't change
+ rc.hashCode = rc.match.hashCode;
+ //
+ me.collectionHash[fc.hashCode] = rc;
+ //
+ var fm = this.fromMemory[collFact.id]; // some bookeeping for modify / retract, revisit, not sure we need this
+ if (!fm) {
+ fm = this.fromMemory[collFact.id] = {};
+ }
+ }
+ else {
+ verb = 'modify'
+ collection = rc.fact.object;
+ }
+ //
+ // build up the collection, the simplest case an array that accumulates a single var
+ me.setAliases.forEach( function(alias, i) {
+ var lcValue = lcFh[alias], idx;
+ //
+ if( me.isCollectionObject ) { // { key1: [values], key2: [values], ... }
+ collection[alias] = collection[alias] || [];
+ idx = extd.findIndex(collection[alias], lcValue);
+ -1 === idx ? collection[alias].push(lcValue) : undefined;
+ }
+ else { // the collection's an array
+ idx = extd.findIndex(collection, lcValue); // don't reproduce values already in the collection
+ if( 1 === me.setAliases.length ) {
+ -1 === idx ? collection.push(lcValue) : undefined;
+ }
+ else { // [ {alias:val, key2: val}, {...}, ... ]
+ if( !setHash ) {
+ setHash = {};
+ setHash.filteredHashCode = fc.hashCode; // tag this so we can find it on retractions
+ collection.push(setHash); // [{...}, {...}...]
+ }
+ setHash[alias] = lcValue;
+ }
+ }
+ });
+ //
+ createdContext = me._createMatch(lc, fc, rc);
+ if (createdContext.isMatch() ) {
+ this.__propagate(verb, createdContext.clone());
+ }
+ }
+ /*
+ We are called with the original lc, the 'pruned' version of the lc and the rc.
+ */
+ ,_createMatch: function (lc, fc, rc) {
+ var me = this
+ ,match = rc.match //fc.match.merge(rc.match) // the derived match
+ ,collFact = rc.fact
+ ,collection = rc.fact.object
+ ,eqConstraints = this.__equalityConstraints
+ ,i = -1,l = eqConstraints.length
+ ,createdContext, fh;
+ //
+ if( match.hashCode !== (fc.hashCode + ':' + rc.fact.id) ) {
+ throw new Error('invalid match hashCode');
+ }
+ fh = Object.create(lc.factHash); //_.merge({}, lc.factHash);
+ fh[me.alias] = collection;
+ // check to see if any condition expressions pass before we propagate anything
+ while (++i < l) {
+ if (!eqConstraints[i](fh, fh)) { // fh: all the 'regular' bindings plus a single collection binding
+ createdContext = DEFAULT_MATCH; // this is a non-match; e.g. lc.isMatch() => false
+ break;
+ }
+ }
+ if (!createdContext) {
+ lc.fromMatches[fc.hashCode] = createdContext = lc.clone(rc.fact, null, match);
+ }
+ this.fromMemory[fc.hashCode] = [lc, createdContext];
+ return createdContext;
+ }
+ //
+ ,modifyLeft: function (lc, retract) {
+ var me = this
+ ,ctx = this.removeFromLeftMemory(lc)
+ ,fc = this.getFilteredContext(lc)
+ ,rc = me.collectionHash[fc.hashCode] // a collection for each combo of non-collection binding(s)
+ ,match = rc.match // fc.match.merge(rc.match) // the rc.fact is the collection
+ ,collFact = rc.fact
+ ,collection = collFact.object
+ ,empty = true
+ ,oldLcFh, newLcFh, lcOldValue, setHash, matchContext;
+ if (ctx) {
+ ctx = ctx.data;
+ if(!retract) {
+ this.__addToLeftMemory(lc);
+ lc.fromMatches = {};
+ }
+ oldLcFh = ctx.factHash;
+ newLcFh = lc.factHash;
+ rightMatches = ctx.fromMatches;
+ //
+ // it's a modify all i have to do is replace the set variables in the collection with their modified values
+ try {
+ me.setAliases.forEach(function(alias) {
+ var theArray, idx;
+ lcOldValue = oldLcFh[alias];
+ lcNewValue = newLcFh[alias];
+ if( me.isCollectionObject ) { // a hash of sets -> { key: [values], key2: [values], ... }
+ theArray = collection[alias];
+ idx = extd.findIndex(theArray, lcOldValue);
+ retract ? delete theArray[idx] : theArray[idx] = lcNewValue;
+ if(retract && empty) { empty = theArray.length ? false : true }
+ }
+ else { // an array either of single values OR [{hash of single values}, {...},...]
+ if( 1 === me.setAliases.length ) { // a simple array
+ idx = extd.findIndex(collection, lcOldValue);
+ retract ? delete collection[idx] : collection[idx] = lcNewValue;
+ }
+ else { // we have a hash remove the entire thing from the array
+ collection.some(function(x,i) { if( x.filteredHashCode === fc.hashCode ) { idx = i; return true }});
+ collection.splice(idx);
+ if(!retract) {
+ if(!setHash) {
+ setHash = {}; setHash.filteredHashCode = fc.hashCode;
+ collection[idx] = setHash;
+ setHash[alias] = lcNewValue;
+ }
+ else {
+ setHash[alias] = lcNewValue;
+ }
+ }
+ else {
+ if( empty ) { empty = collection.length ? false : true }
+ }
+ }
+ }
+ });
+ //
+ if( retract && empty ) {
+ for (var i in ctx.fromMatches) {
+ matchContext = ctx.fromMatches[i];
+ this.removeFromFromMemory(matchContext);
+ this.__propagate("retract", matchContext.clone());
+ }
+ }
+ else {
+ lc.fromMatches[fc.hashCode] = createdContext = lc.clone(collFact, null, match);
+ this.fromMemory[fc.hashCode] = [lc, createdContext];
+ this.__propagate("modify", createdContext.clone());
+ }
+ }
+ catch(e) {
+ throw new Error(e);
+ }
+ }
+ else {
+ this.assertLeft(lc);
+ }
+ }
+ //
+ ,retractLeft: function(lc) {
+ return this.modifyLeft(lc, true);
+ }
+ //
+ // clone the (left) context and remove the set oriented variables
+ //
+ ,getFilteredContext: function(lc) {
+ var me = this
+ ,match = new Match()
+ ,clone;
+ // the aliases array is in order so the filtered aliases are also in order
+ lc.match.aliases.forEach(function(alias, i) {
+ var idx = extd.findIndex(me.setAliases, function(alias) { return function(x) { return x == alias; }}(alias))
+ ,fact;
+ if( idx === -1 ) {
+ fact = lc.match.facts[i];
+ match.addFact(fact);
+ match.factHash[alias] = fact.object;
+ }
+ });
+ clone = lc.clone(null, null, match); // with set variables removed
+ extdObj(match.factHash).forEach( function(val, key) {clone.set(key, val);});
+ return clone;
+ }
+ //
+ // clone the (left) context and remove the set oriented variables
+ //
+ ,getFilteredHashCode: function(context) {
+ var me = this
+ ,facts = []
+ ,tmp;
+ //
+ context.match.aliases.forEach(function(alias, i) {
+ if( extd.findIndex(me.setAliases, function(x) { return x == alias; } ) === -1) {
+ facts.push(context.factHash[alias]);
+ }
+ });
+ return facts.map(function(x){ return x.id; }).reduce(function(prev, cur, idx, array) { return prev ? ( prev + ':' + cur ) : ('' + cur) });
+ }
+
+ }
+}).as(module);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/nodes/index.js b/lib/nodes/index.js
index 2f4e6ed..0a23294 100644
--- a/lib/nodes/index.js
+++ b/lib/nodes/index.js
@@ -11,7 +11,8 @@ var extd = require("../extended"),
FromExistsPattern = pattern.FromExistsPattern,
NotPattern = pattern.NotPattern,
CompositePattern = pattern.CompositePattern,
- InitialFactPattern = pattern.InitialFactPattern,
+ InitialFactPattern = pattern.InitialFactPattern,
+ CollectPattern = pattern.CollectPattern,
constraints = require("../constraint"),
HashConstraint = constraints.HashConstraint,
ReferenceConstraint = constraints.ReferenceConstraint,
@@ -28,7 +29,9 @@ var extd = require("../extended"),
RightAdapterNode = require("./rightAdapterNode"),
TypeNode = require("./typeNode"),
TerminalNode = require("./terminalNode"),
- PropertyNode = require("./propertyNode");
+ PropertyNode = require("./propertyNode"),
+ CollectNode = require("./collectNode");
+
function hasRefernceConstraints(pattern) {
return some(pattern.constraints || [], function (c) {
@@ -113,7 +116,7 @@ declare({
}
}
},
-
+
__checkEqual: function (node) {
var constraints = this.constraints, i = constraints.length - 1;
for (; i >= 0; i--) {
@@ -163,6 +166,8 @@ declare({
joinNode = new ExistsFromNode(pattern.rightPattern, this.workingMemory);
} else if (pattern.rightPattern instanceof ExistsPattern) {
joinNode = new ExistsNode();
+ } else if (pattern.rightPattern instanceof CollectPattern ) {
+ joinNode = new CollectNode(pattern.rightPattern, this.workingMemory);
} else if (pattern.rightPattern instanceof FromNotPattern) {
joinNode = new FromNotNode(pattern.rightPattern, this.workingMemory);
} else if (pattern.rightPattern instanceof FromPattern) {
diff --git a/lib/parser/nools/nool.parser.js b/lib/parser/nools/nool.parser.js
index 71d4012..3b6b571 100644
--- a/lib/parser/nools/nool.parser.js
+++ b/lib/parser/nools/nool.parser.js
@@ -1,6 +1,6 @@
"use strict";
-var tokens = require("./tokens.js"),
+var tokens = require("./tokens.js").topLevelTokens,
extd = require("../../extended"),
keys = extd.hash.keys,
utils = require("./util.js");
diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js
index 3ac7080..2ead85f 100644
--- a/lib/parser/nools/tokens.js
+++ b/lib/parser/nools/tokens.js
@@ -5,7 +5,7 @@ var utils = require("./util.js"),
extd = require("../../extended"),
filter = extd.filter,
indexOf = extd.indexOf,
- predicates = ["not", "or", "exists"],
+ predicates = ["not", "or", "exists"],
predicateRegExp = new RegExp("^(" + predicates.join("|") + ") *\\((.*)\\)$", "m"),
predicateBeginExp = new RegExp(" *(" + predicates.join("|") + ") *\\(", "g");
@@ -38,8 +38,82 @@ var splitRuleLineByPredicateExpressions = function (ruleLine) {
return ret.join(";");
};
-var ruleTokens = {
+/**
+*/
+var ruleRegExp = /^(\$?\w+)\s*:\s*(\w+)(.*)/;
+var collectRuleRegExp = /^(\$?\w+)\s*:\s*(\w+)\s*(.*)from\s*collect\s*\((.*)\)/;
+var hashRegExp = /(\{ *(?:["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']? *(?:, *["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']?)*)+ *\})/;
+var fromRegExp = /(\bfrom\s+.*)/;
+var parseRules = function (str) {
+ var rules = [];
+ var ruleLines = str.split(";"), l = ruleLines.length, ruleLine, alias, constraints, parts, collectRule;
+ for (var i = 0; i < l && (ruleLine = ruleLines[i].replace(/^\s*|\s*$/g, "").replace(/\n/g, "")); i++) {
+ if (!isWhiteSpace(ruleLine)) {
+ var rule = [];
+ if (predicateRegExp.test(ruleLine)) {
+ var m = ruleLine.match(predicateRegExp);
+ var pred = m[1].replace(/^\s*|\s*$/g, "");
+ rule.push(pred);
+ ruleLine = m[2].replace(/^\s*|\s*$/g, "");
+ if (pred === "or") {
+ rule = rule.concat(parseRules(splitRuleLineByPredicateExpressions(ruleLine)));
+ rules.push(rule);
+ continue;
+ }
+ }
+ else if( collectRuleRegExp.test(ruleLine) ) {
+ parts = ruleLine.match(collectRuleRegExp);
+ if(parts && parts.length ) {
+ rule.push('collect');
+ rule.push(parts[2], parts[1]);
+ constraints = parts[3].trim();
+ rule.push( (constraints && !isWhiteSpace(constraints)) ? constraints : undefined );
+ collectRule = parseRules(parts[4]);
+ rule.push(collectRule[0]);
+ }
+ rules.push(rule);
+ continue;
+ }
+ parts = ruleLine.match(ruleRegExp);
+ if (parts && parts.length) {
+ rule.push(parts[2], parts[1]);
+ constraints = parts[3].replace(/^\s*|\s*$/g, "");
+ var hashParts = constraints.match(hashRegExp), from = null, fromMatch;
+ if (hashParts) {
+ var hash = hashParts[1], constraint = constraints.replace(hash, "");
+ if (fromRegExp.test(constraint)) {
+ fromMatch = constraint.match(fromRegExp);
+ from = fromMatch[0];
+ constraint = constraint.replace(fromMatch[0], "");
+ }
+ if (constraint) {
+ rule.push(constraint.replace(/^\s*|\s*$/g, ""));
+ }
+ if (hash) {
+ rule.push(eval("(" + hash.replace(/(\$?\w+)\s*:\s*(\$?\w+)/g, '"$1" : "$2"') + ")"));
+ }
+ } else if (constraints && !isWhiteSpace(constraints)) {
+ if (fromRegExp.test(constraints)) {
+ fromMatch = constraints.match(fromRegExp);
+ from = fromMatch[0];
+ constraints = constraints.replace(fromMatch[0], "");
+ }
+ rule.push(constraints);
+ }
+ if (from) {
+ rule.push(from);
+ }
+ rules.push(rule);
+ } else {
+ throw new Error("Invalid constraint " + ruleLine);
+ }
+ }
+ }
+ return rules;
+};
+exports.parseRules = parseRules;
+var ruleTokens = {
salience: (function () {
var salienceRegexp = /^(salience|priority)\s*:\s*(-?\d+)\s*[,;]?/;
return function (src, context) {
@@ -93,7 +167,7 @@ var ruleTokens = {
}
};
})(),
-
+
"agenda-group": function () {
return this.agendaGroup.apply(this, arguments);
},
@@ -109,66 +183,6 @@ var ruleTokens = {
when: (function () {
/*jshint evil:true*/
- var ruleRegExp = /^(\$?\w+) *: *(\w+)(.*)/;
-
- var constraintRegExp = /(\{ *(?:["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']? *(?:, *["']?\$?\w+["']?\s*:\s*["']?\$?\w+["']?)*)+ *\})/;
- var fromRegExp = /(\bfrom\s+.*)/;
- var parseRules = function (str) {
- var rules = [];
- var ruleLines = str.split(";"), l = ruleLines.length, ruleLine;
- for (var i = 0; i < l && (ruleLine = ruleLines[i].replace(/^\s*|\s*$/g, "").replace(/\n/g, "")); i++) {
- if (!isWhiteSpace(ruleLine)) {
- var rule = [];
- if (predicateRegExp.test(ruleLine)) {
- var m = ruleLine.match(predicateRegExp);
- var pred = m[1].replace(/^\s*|\s*$/g, "");
- rule.push(pred);
- ruleLine = m[2].replace(/^\s*|\s*$/g, "");
- if (pred === "or") {
- rule = rule.concat(parseRules(splitRuleLineByPredicateExpressions(ruleLine)));
- rules.push(rule);
- continue;
- }
-
- }
- var parts = ruleLine.match(ruleRegExp);
- if (parts && parts.length) {
- rule.push(parts[2], parts[1]);
- var constraints = parts[3].replace(/^\s*|\s*$/g, "");
- var hashParts = constraints.match(constraintRegExp), from = null, fromMatch;
- if (hashParts) {
- var hash = hashParts[1], constraint = constraints.replace(hash, "");
- if (fromRegExp.test(constraint)) {
- fromMatch = constraint.match(fromRegExp);
- from = fromMatch[0];
- constraint = constraint.replace(fromMatch[0], "");
- }
- if (constraint) {
- rule.push(constraint.replace(/^\s*|\s*$/g, ""));
- }
- if (hash) {
- rule.push(eval("(" + hash.replace(/(\$?\w+)\s*:\s*(\$?\w+)/g, '"$1" : "$2"') + ")"));
- }
- } else if (constraints && !isWhiteSpace(constraints)) {
- if (fromRegExp.test(constraints)) {
- fromMatch = constraints.match(fromRegExp);
- from = fromMatch[0];
- constraints = constraints.replace(fromMatch[0], "");
- }
- rule.push(constraints);
- }
- if (from) {
- rule.push(from);
- }
- rules.push(rule);
- } else {
- throw new Error("Invalid constraint " + ruleLine);
- }
- }
- }
- return rules;
- };
-
return function (orig, context) {
var src = orig.replace(/^when\s*/, "").replace(/^\s*|\s*$/g, "");
if (utils.findNextToken(src) === "{") {
@@ -324,7 +338,7 @@ var topLevelTokens = {
}
},
- "rule": function (orig, context, parse) {
+ "rule": function (orig, context, parse) {
var src = orig.replace(/^rule\s*/, "");
var name = src.match(/^([a-zA-Z_$][0-9a-zA-Z_$]*|"[^"]*"|'[^']*')/);
if (name) {
@@ -343,8 +357,7 @@ var topLevelTokens = {
} else {
throw new Error("missing name");
}
-
}
};
-module.exports = topLevelTokens;
+exports.topLevelTokens = topLevelTokens;
diff --git a/lib/pattern.js b/lib/pattern.js
index c776e2f..d18f238 100644
--- a/lib/pattern.js
+++ b/lib/pattern.js
@@ -99,6 +99,45 @@ ObjectPattern.extend().as(exports, "NotPattern");
ObjectPattern.extend().as(exports, "ExistsPattern");
FromPattern.extend().as(exports, "FromExistsPattern");
+var CollectPattern = FromPattern.extend({
+ static: {
+ fromLiteral: function(pojo, prototype) {
+ var me = this._super([pojo, prototype || AccumulatePattern.prototype]);
+ return me;
+ }
+ },
+ instance: {
+ type: "CollectPattern"
+ ,asLiteral: function(flow) {
+ var def = this._super(arguments);
+ def.$className = 'AccumulatePattern';
+ return def;
+ },
+ //
+ constructor: function (type, alias, conditions, store, options) {
+ //this._super([type, alias, conditions, store, options]);
+ ObjectPattern.prototype.constructor.call(this, type, alias, conditions, store, options);
+ // source expression is baked into CollectNode
+ this.from = {
+ asLiteral: function() {
+ return {}
+ }
+ ,assert: function() {
+ throw new Error('the collection is implicitly provided');
+ }
+ };
+ },
+ //
+ getAccumulation: function() {
+ return this.from.accumulation;
+ }
+ //
+ ,block: function(b) {
+ this.from.blocked = b;
+ }
+ }
+}).as(exports, "CollectPattern");
+
Pattern.extend({
instance: {
diff --git a/lib/rule.js b/lib/rule.js
index 75a73d8..72dba38 100644
--- a/lib/rule.js
+++ b/lib/rule.js
@@ -8,14 +8,17 @@ var extd = require("./extended"),
format = extd.format,
parser = require("./parser"),
pattern = require("./pattern"),
+ parseRules = require('./parser/nools/tokens.js').parseRules,
ObjectPattern = pattern.ObjectPattern,
FromPattern = pattern.FromPattern,
NotPattern = pattern.NotPattern,
ExistsPattern = pattern.ExistsPattern,
FromNotPattern = pattern.FromNotPattern,
FromExistsPattern = pattern.FromExistsPattern,
- CompositePattern = pattern.CompositePattern;
+ CompositePattern = pattern.CompositePattern,
+ CollectPattern = pattern.CollectPattern;
+//
var parseConstraint = function (constraint) {
if (typeof constraint === 'function') {
// No parsing is needed for constraint functions
@@ -130,7 +133,19 @@ var getParamType = function getParamType(type, scope) {
return _getParamType(type);
};
+//
+var collectRegExp = /collect\s*\((.*)\)$/;
+//
+function mergePatterns(patterns, merged, setAliases) {
+ var flattened = extd(patterns).flatten().value();
+ flattened.forEach(function (thePattern) {
+ setAliases.push(thePattern.alias);
+ merged.push([thePattern]);
+ });
+ return merged;
+}
+//
var parsePattern = extd
.switcher()
.containsAt("or", 0, function (condition) {
@@ -192,22 +207,113 @@ var parsePattern = extd
];
}
})
+ .containsAt('collect', 0, function(condition) {
+ var scope = condition.scope,
+ type = condition[1],
+ alias = condition[2] || "__~PBV~__",
+ constraint = condition[3] || "true",
+ parsedCollect = condition[4],
+ validType = getParamType(type, scope),
+ parsedConstraint = parseConstraint(constraint),
+ collectHash = parsedCollect[3],
+ setAliases = [], merged = [], emptyHash = {},
+ collectPattern, parsedConstraint, setConditions, patterns, merged, isRules, compPat;
+ //
+ collectPattern = new CollectPattern(validType, alias, parsedConstraint, emptyHash, {scope: scope, pattern: constraint});
+ collectPattern.collectHash = collectHash;
+ //
+ isRules = extd.every(parsedCollect, function (cond) {
+ return isArray(cond);
+ });
+ if (isRules && parsedCollect.length === 1) {
+ parsedCollect = parsedCollect[0];
+ isRules = false;
+ }
+ //
+ if( isRules ) {
+ parsedCollect.map(function(cond) {
+ cond.scope = scope;
+ patterns = parsePattern(cond);
+ mergePatterns(patterns, merged, setAliases);
+ });
+ //
+ collectPattern.setAliases = setAliases;
+ merged.push([collectPattern]);
+ return merged;
+ }
+ else { // there was a single condition inside of from collect( expr );
+ parsedCollect.scope = scope;
+ patterns = parsePattern(parsedCollect);
+ mergePatterns(patterns, merged, setAliases);
+ collectPattern.setAliases = setAliases;
+ merged.push([collectPattern]);
+ return merged;
+ }
+ })
.def(function (condition) {
if (typeof condition === 'function') {
return [condition];
}
condition = normailizeConstraint(condition);
if (condition[4] && condition[4].from) {
- return [
- new FromPattern(
- getParamType(condition[0], condition.scope),
- condition[1] || "m",
- parseConstraint(condition[2] || "true"),
- condition[3] || {},
- parseConstraint(condition[4].from),
- {scope: condition.scope, pattern: condition[2]}
- )
- ];
+ var fromArg = condition[4].from
+ ,merged,setAliases,collectExpr, setConditions, patterns, merged, isRules, collectPattern,compPat;
+ //
+ if( 'string' === typeof fromArg && fromArg.match( collectRegExp ) ) {
+ collectExpr = fromArg.match(collectRegExp)[1];
+ //
+ // every alias from these setConditions is an SOV: Set Oriented Variable
+ setConditions = parseRules(collectExpr);
+ setAliases = []; merged = [];
+ //
+ isRules = extd.every(setConditions, function (cond) {
+ return isArray(cond);
+ });
+ if (isRules && setConditions.length === 1) {
+ setConditions = setConditions[0];
+ isRules = false;
+ }
+ //
+ collectPattern = new CollectPattern(
+ getParamType(condition[0], condition.scope), // type; Array or Object
+ condition[1] || "__~PBV~__", // missing pattern binding must be detected
+ parseConstraint(condition[2] || "true"), // expression using from
+ condition[3] || {}, // store
+ {scope: condition.scope, pattern: condition[2]} // misc...
+ );
+ //
+ if( isRules ) {
+ setConditions.map(function(cond) {
+ cond.scope = condition.scope;
+ patterns = parsePattern(cond);
+ mergePatterns(patterns, merged, setAliases);
+ });
+ //
+ collectPattern.setAliases = setAliases;
+ merged.push([collectPattern]);
+ return merged;
+ }
+ else { // there was a single condition inside of from collect( expr );
+ setConditions.scope = condition.scope;
+ patterns = parsePattern(setConditions);
+ mergePatterns(patterns, merged, setAliases);
+ collectPattern.setAliases = setAliases;
+ merged.push([collectPattern]);
+ return merged;
+ }
+ }
+ else {
+ return [
+ new FromPattern(
+ getParamType(condition[0], condition.scope),
+ condition[1] || "m",
+ parseConstraint(condition[2] || "true"),
+ condition[3] || {},
+ parseConstraint(condition[4].from),
+ {scope: condition.scope, pattern: condition[2]}
+ )
+ ];
+ }
} else {
return [
new ObjectPattern(
@@ -218,15 +324,18 @@ var parsePattern = extd
{scope: condition.scope, pattern: condition[2]}
)
];
- }
+ }
}).switcher();
var Rule = declare({
+
instance: {
+
constructor: function (name, options, pattern, cb) {
this.name = name;
this.pattern = pattern;
this.cb = cb;
+ this.noLoop = options.noLoop;
if (options.agendaGroup) {
this.agendaGroup = options.agendaGroup;
this.autoFocus = extd.isBoolean(options.autoFocus) ? options.autoFocus : false;
@@ -236,7 +345,7 @@ var Rule = declare({
fire: function (flow, match) {
var ret = new Promise(), cb = this.cb;
- try {
+ try {
if (cb.length === 3) {
cb.call(flow, match.factHash, flow, ret.resolve);
} else {
@@ -249,67 +358,123 @@ var Rule = declare({
}
}
});
+exports.Rule = Rule;
+//
+function _mergePatterns (patterns) {
+ //
+ return function (patt, i) {
+ // [pattern], [pattern], ... in arrays of length 1
+ // we wish to build a single array in order of lhs progression
+ if( isArray(patt) ) {
+ if( patt.length === 1 ) {
+ patt = patt[0];
+ i = 0;
+ }
+ else {
+ throw new Error('invalid pattern structure');
+ }
+ }
+ if (!patterns[i]) {
+ patterns[i] = i === 0 ? [] : patterns[i - 1].slice();
+ //remove dup
+ if (i !== 0) {
+ patterns[i].pop();
+ }
+ patterns[i].push(patt);
+ } else {
+ extd(patterns).forEach(function (p) {
+ p.push(patt);
+ });
+ }
+ };
+}
+//
+//
function createRule(name, options, conditions, cb) {
- if (isArray(options)) {
- cb = conditions;
- conditions = options;
- } else {
- options = options || {};
- }
- var isRules = extd.every(conditions, function (cond) {
- return isArray(cond);
- });
- if (isRules && conditions.length === 1) {
- conditions = conditions[0];
- isRules = false;
- }
- var rules = [];
- var scope = options.scope || {};
- conditions.scope = scope;
- if (isRules) {
- var _mergePatterns = function (patt, i) {
- if (!patterns[i]) {
- patterns[i] = i === 0 ? [] : patterns[i - 1].slice();
- //remove dup
- if (i !== 0) {
- patterns[i].pop();
- }
- patterns[i].push(patt);
- } else {
- extd(patterns).forEach(function (p) {
- p.push(patt);
- });
- }
-
- };
- var l = conditions.length, patterns = [], condition;
- for (var i = 0; i < l; i++) {
- condition = conditions[i];
- condition.scope = scope;
- extd.forEach(parsePattern(condition), _mergePatterns);
-
- }
- rules = extd.map(patterns, function (patterns) {
- var compPat = null;
- for (var i = 0; i < patterns.length; i++) {
- if (compPat === null) {
- compPat = new CompositePattern(patterns[i++], patterns[i]);
- } else {
- compPat = new CompositePattern(compPat, patterns[i]);
- }
- }
- return new Rule(name, options, compPat, cb);
- });
- } else {
- rules = extd.map(parsePattern(conditions), function (cond) {
- return new Rule(name, options, cond, cb);
- });
- }
- return rules;
+ var rules = [], scope, patterns, isComposite;
+ function processConditions(conditions, scope) {
+ var l = conditions.length,
+ merged = [],
+ fnMerge = _mergePatterns(merged),
+ isRules = extd.every(conditions, function (cond) {return isArray(cond);}),
+ condition, rules, patterns;
+ //
+ if( isRules && conditions.length === 1 ) {
+ isRules = false;
+ conditions = conditions[0];
+ }
+ //
+ function isSinglePattern(patterns) {
+ var ret = true;
+ if( patterns.length > 1 ) {
+ if( isArray(patterns[0]) ) {
+ ret = false;
+ }
+ // else it's OR [ p, p,...] which we treat as a single rule which results in multiple rules
+ }
+ return ret;
+ }
+ //
+ function patternFromCondition(condition, scope) {
+ var patterns;
+ condition.scope = scope;
+ patterns = parsePattern(condition);
+ return patterns;
+ }
+ //
+ function compositePattern(patterns) {
+
+ return extd.map(merged, function (patterns) {
+ var compPat = null;
+ for (var i = 0; i < patterns.length; i++) {
+ if (compPat === null) {
+ compPat = new CompositePattern(patterns[i++], patterns[i]);
+ } else {
+ compPat = new CompositePattern(compPat, patterns[i]);
+ }
+ }
+ return new Rule(name, options, compPat, cb);
+ });
+ }
+ //
+ function singlePattern(pattern) {
+ return extd.map(patterns, function (cond) {
+ return new Rule(name, options, cond, cb);
+ });
+ }
+ //
+ if( isRules ) {
+ for (var i = 0; i < l; i++) {
+ condition = conditions[i];
+ condition.scope = scope;
+ patterns = patternFromCondition(condition, scope);
+ extd.forEach( patterns, fnMerge );
+ }
+ rules = compositePattern(merged);
+ }
+ else {
+ patterns = patternFromCondition(conditions, scope);
+ if( isSinglePattern(patterns) ) {
+ rules = singlePattern(patterns);
+ }
+ else {
+ extd.forEach( patterns, fnMerge );
+ rules = compositePattern(merged);
+ }
+ }
+ return rules;
+ }
+ //
+ if (isArray(options)) {
+ cb = conditions;
+ conditions = options;
+ options = {};
+ } else {
+ options = options || {};
+ }
+ scope = options.scope || {};
+ return processConditions(conditions, scope);
}
-
exports.createRule = createRule;
-
-
diff --git a/readme.md b/readme.md
index e5bc0d9..f76c6f9 100644
--- a/readme.md
+++ b/readme.md
@@ -38,10 +38,11 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi
* [Salience](#rule-salience)
* [Scope](#rule-scope)
* [Constraints](#constraints)
- * [Custom](#custom-contraints)
+ * [Custom](#custom-contraints)
* [Not](#not-constraint)
* [Or](#or-constraint)
* [From](#from-constraint)
+ * [Collect](#collect-modifer)
* [Exists](#exists-constraint)
* [Actions](#action)
* [Async Actions](#action-async)
@@ -80,7 +81,7 @@ var Message = function (message) {
var flow = nools.flow("Hello World", function (flow) {
//find any message that start with hello
- flow.rule("Hello", [Message, "m", "m.text =~ /^hello\\sworld$/"], function (facts) {
+ flow.rule("Hello", [Message, "m", "m.text =~ /^hello(\\s*world)?$/"], function (facts) {
facts.m.text = facts.m.text + " goodbye";
this.modify(facts.m);
});
@@ -97,7 +98,7 @@ In the above flow definition 2 rules were defined
* Hello
* Requires a Message
- * The messages's `text` must match the regular expression `/^hello\\sworld$/`
+ * The messages's `text` must match the regular expression `/^hello(\\s*world)?$/`
* When matched the message's `text` is modified and then we let the engine know that we modified the message.
* Goodbye
* Requires a Message
@@ -341,7 +342,6 @@ session.getFacts(Number); //[1, 2];
session.getFacts(String); //["A", "B"];
```
-
## Firing the rules
@@ -1003,8 +1003,6 @@ flow1
});
```
-
-
### Scope
@@ -1038,7 +1036,7 @@ function matches(str, regex){
rule Hello {
when {
- m : Message matches(m.text, /^hello\s*world)?$/);
+ m : Message matches(m.text, /^hello(\\s*world)?$/);
}
then {
modify(m, function(){
@@ -1061,7 +1059,7 @@ Or you can pass in a custom function using the scope option in compile.
```
rule Hello {
when {
- m : Message doesMatch(m.text, /^hello\sworld$/);
+ m : Message doesMatch(m.text, /^hello(\\s*world)?$/);
}
then {
modify(m, function(){
@@ -1194,7 +1192,6 @@ session.match().then(function(){
console.log("DONE");
});
```
-
#### Not Constraint
@@ -1406,6 +1403,29 @@ rule "my rule", {
}
```
+
+
+###Collect Modifier
+The 'collect' modifer results in a returned object, as such a pattern can specify collect as its 'from' source.
+The 'collect' modifer returns an array which allows cardinality reasoning (when there are more than 7 red buses).
+
+This example chains two 'from's together. It finds customers who have bought items all of which are priced over 10, where the items are a field and not asserted into the working memory:
+ ```javascript
+c : Customer
+items : Array items.length == c.items.length ) from collect( item : Item item.price > 10 from c.items);
+ ```
+ If the items where not a field, but instead asserted into the working memory, we could use a correlated 'collect' pattern:
+```javascript
+p : Person ;
+list: Array from collect( item: Item item.owner === p );
+items : Array items.length === list.length from collect( item: Item item.price > 10 from list );
+ ```
+ This blog post from Marc Proctor the team lead on Drools explains collect in more detail.
+ http://blog.athico.com/2007/06/chained-from-accumulate-collect.html
+
+ This paper was used to develop the collect node:
+ http://citeseer.ist.psu.edu/viewdoc/download?doi=10.1.1.25.1076&rep=rep1&type=pdf
+
###Exists Constraint
@@ -1690,7 +1710,7 @@ define Message {
rule Hello {
when {
- m : Message m.text =~ /^hello\sworld$/
+ m : Message m.text =~ /^hello(\\sworld)?$/
}
then {
modify(m, function(){
diff --git a/test/flow.dsl.test.js b/test/flow.dsl.test.js
index 01e3855..8fdfe27 100644
--- a/test/flow.dsl.test.js
+++ b/test/flow.dsl.test.js
@@ -593,4 +593,5 @@ it.describe("Flow dsl", function (it) {
});
});
});
+
});
\ No newline at end of file
diff --git a/test/flow/collect.nools b/test/flow/collect.nools
new file mode 100644
index 0000000..53251f1
--- /dev/null
+++ b/test/flow/collect.nools
@@ -0,0 +1,83 @@
+
+define System {
+ location : "",
+ status: 'normal',
+ constructor : function (loc) {
+ this.location = loc;
+ }
+}
+
+define Alarm {
+ system: undefined
+ ,status: 'normal'
+ ,purpose: undefined
+ ,constructor: function(system, status, purpose) {
+ this.system = system;
+ this.status = status;
+ this.purpose = purpose;
+ }
+}
+
+define Emergency {
+ system : undefined,
+ alarms: undefined,
+ constructor : function (system, alarms) {
+ this.system = system;
+ this.alarms = alarms;
+ }
+}
+
+
+/**
+ classic Drools alarm example
+*/
+rule "Raise priority if system has more than 3 pending alarms" {
+ when {
+ $system : System $system.status === 'normal';
+ $alarms : Array $alarms.length >= 3 from collect( $alarm : Alarm $alarm.system === $system and $alarm.status === 'pending' );
+ }
+ then {
+ emit('system-alarms', $system, $alarms);
+ modify($system, function() {this.status = 'alarms-pending';})
+ }
+}
+/**
+ if the system is alarmed respond by asserting as Emergency fact.
+*/
+rule "If System is alarmed respond by asserting Emergency" {
+ when {
+ $system : System $system.status === 'alarms-pending';
+ $alarms : Array from collect( $alarm : Alarm $alarm.system === $system and $alarm.status === 'pending' );
+ }
+ then {
+ emit('system-emergency', $system, $alarms);
+ modify($system, function() {this.status = 'emergency-declared';});
+ var e = new Emergency($system, $alarms);
+ assert(e);
+ }
+}
+/**
+ if there is an emergency turn of
+*/
+rule "If System is alarmed respond by asserting Emergency" {
+ when {
+ $emergency : Emergency;
+ }
+ then {
+ emit('emergency-response', $emergency);
+ }
+}
+
+/**
+ find customers who have bought items all of which are priced over 10
+ where the items are a file and not in working memory.
+*/
+rule TestCollect {
+ when {
+ c : Customer;
+ items: Array items.size === c.items.size from collect( item: Item item.price > 10 from c.items );
+ }
+ then {
+ emit('test-collect', c, items);
+ }
+}
diff --git a/test/flow/collect.test.js b/test/flow/collect.test.js
new file mode 100644
index 0000000..9fbe4a8
--- /dev/null
+++ b/test/flow/collect.test.js
@@ -0,0 +1,221 @@
+"use strict";
+var it = require("it"),
+ assert = require("assert"),
+ nools = require("../../");
+
+//
+function Customer(name) {
+ this.name = name;
+ this.items = [];
+}
+Customer.prototype = {
+ add: function(order) {
+ this.items.push(order);
+ }
+ ,getOrder: function(name) {
+ var item;
+ this.items.some(function(order) { if( order.type === name ) {item = order; return true;} });
+ return item;
+ }
+}
+//
+function Item(type, price) {
+ this.type = type;
+ this.price = price;
+}
+//
+var rule1Called = 0;
+var rule2Called = 0;
+var rule3Called = 0;
+
+it.describe("collect condition", function (it) {
+
+ it.describe("basic test of collection element", function (it) {
+debugger;
+ var flow = nools.flow("collect test 1",function (flow) {
+ flow.addDefined('Customer', Customer);
+ flow.addDefined('Item', Item);
+ flow.rule("rule 1" , {salience: 10, scope: { Customer: Customer, Item: Item }}, [
+ [Customer, 'c']
+ ,[Array, 'list', 'list.size === c.items.size', 'from collect( item : Item item.price > 10 from c.items )']
+ ], function(facts) {
+debugger;
+ rule1Called++;
+ });
+ });
+ //
+ it.should("rhs for collection called a single time and set avaialable in lhs", function () {
+debugger;
+ var session = flow.getSession();
+ //
+ var customer = new Customer('John');
+ var stroller = new Item('stroller', 50);
+ var bike = new Item('bike', 11);
+ var car = new Item('car', 2500);
+ session.assert(stroller);session.assert(bike);session.assert(car);
+ customer.add(stroller);customer.add(bike);customer.add(car);
+ session.assert(customer);
+ rule1Called = 0;
+debugger;
+ return session.match().then(function () {
+debugger;
+ assert.equal(rule1Called, 1);
+ });
+ });
+
+ //
+ var flow = nools.flow("collect test 2",function (flow) {
+ flow.addDefined('Customer', Customer);
+ flow.addDefined('Item', Item);
+ flow.rule("rule 1" , {salience: 10, scope: { Customer: Customer, Item: Item }}, [
+ [Customer, 'c']
+ ,[Array, 'list', 'list.size === c.items.size', 'from collect( item : Item item.price > 10 from c.items)']
+ ], function(facts) {
+ rule1Called++;
+ });
+ flow.rule("rule 2", {salience: 5, noLoop: true, scope: { Customer: Customer, Item: Item }}, [
+ [Item, 'item', "item.type == 'bike' && item.price !== 11"]
+ ], function (facts) {
+ rule2Called++;
+ facts.item.price = 11;
+ this.modify(facts.item);
+ });
+ });
+ //
+ it.should("use set in from expression with modified fact causing rhs to fire once", function () {
+ var session = flow.getSession();
+ //
+ var customer = new Customer('John');
+ var stroller = new Item('stroller', 50);
+ var bike = new Item('bike', 9);
+ var car = new Item('car', 2500);
+ session.assert(stroller);session.assert(bike);session.assert(car);
+ customer.add(stroller);customer.add(bike);customer.add(car);
+ session.assert(customer);
+ //
+ rule1Called = rule2Called = 0;
+ //
+ return session.match().then(function () {
+ assert.equal(rule1Called, 1);
+ assert.equal(rule2Called, 1);
+ });
+ });
+
+ });
+
+ it.describe("basic test of collect using DSL", function (it) {
+ var defines = { Customer: Customer, Item: Item },
+ flow = nools.compile(__dirname + '/collect.nools', { name: 'TestCollect', defines: defines} ),
+ rule1Called = 0;
+ it.should("alarm example initial states cause rule fire", function () {
+ //
+ var System = flow.getDefined('System'),
+ Alarm = flow.getDefined('Alarm'),
+ session = flow.getSession(),
+ system, alarmA, alarmB, alarmC;
+ //
+ session.on("system-alarms", function (system, alarms) {
+ rule1Called++;
+ assert.equal(alarms.length, 3);
+ assert.equal(system.location, 'kitchen');
+ assert.deepEqual(alarms[0], alarmA);
+ assert.deepEqual(alarms[1], alarmB);
+ assert.deepEqual(alarms[2], alarmC);
+ });
+ //
+ system = new System('kitchen');
+ session.assert(system);
+
+ alarmA = new Alarm(system, 'pending', 'door'); // create three alarms for the System
+ alarmB = new Alarm(system, 'pending', 'fire');
+ alarmC = new Alarm(system, 'pending', 'window');
+ //
+ session.assert(alarmA);
+ session.assert(alarmB);
+ session.assert(alarmC);
+ //
+ return session.match().then(function () {
+ assert.equal(rule1Called, 1);
+ });
+ });
+ it.should("modified alarm example alarm causes rule fire", function () {
+ //
+ var System = flow.getDefined('System'),
+ Alarm = flow.getDefined('Alarm'),
+ session = flow.getSession(),
+ system, alarmA, alarmB, alarmC;
+
+ rule1Called = 0;
+ rule2Called = 0;
+ rule3Called = 0;
+ //
+ session.on("system-alarms", function (system, alarms) {
+ rule1Called++;
+ assert.equal(alarms.length, 3);
+ assert.equal(system.location, 'kitchen');
+ assert.deepEqual(alarms[0], alarmA);
+ assert.deepEqual(alarms[1], alarmB);
+ assert.deepEqual(alarms[2], alarmC);
+ });
+ //
+ session.on("system-emergency", function (system, alarms) {
+ rule2Called++;
+ assert.equal(alarms.length, 3);
+ assert.equal(system.status, 'alarms-pending');
+ });
+ //
+ session.on("emergency-response", function (emergency) {
+ rule3Called++;
+ assert.equal(emergency.alarms.length, 3);
+ assert.deepEqual(emergency.system, system);
+ });
+
+
+ //
+ system = new System('kitchen');
+ session.assert(system);
+
+ alarmA = new Alarm(system, 'pending', 'door'); // create three alarms for the System
+ alarmB = new Alarm(system, 'pending', 'fire');
+ alarmC = new Alarm(system, 'normal', 'window');
+ //
+ session.assert(alarmA);
+ session.assert(alarmB);
+ session.assert(alarmC);
+ //
+ return session.match().then(function () {
+ assert.equal(rule1Called, 0);
+ alarmC.status = 'pending';
+ session.modify(alarmC);
+ return session.match().then(function() {
+ assert.equal(rule1Called, 1);
+ });
+ });
+ });
+
+ //
+ it.should("rhs for collection called a single time and set avaialable in lhs", function () {
+ var session = flow.getSession();
+ var rule1Called = 0;
+ //
+ session.on("test-collect", function (customer, items) {
+ rule1Called++;
+ assert.equal(items.length, 3);
+ assert(customer);
+ });
+
+ //
+ var customer = new Customer('John');
+ var stroller = new Item('stroller', 50);
+ var bike = new Item('bike', 11);
+ var car = new Item('car', 2500);
+ session.assert(stroller);session.assert(bike);session.assert(car);
+ customer.add(stroller);customer.add(bike);customer.add(car);
+ session.assert(customer);
+ //
+ return session.match().then(function () {
+ assert.equal(rule1Called, 1);
+ });
+ });
+ });
+});