From a4b0c82c66ae29433b3af9ec8a8f63e4116cc933 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Thu, 28 Jan 2016 16:21:14 -0800 Subject: [PATCH 1/5] refactored createRule to support collect / query features --- lib/rule.js | 209 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 138 insertions(+), 71 deletions(-) diff --git a/lib/rule.js b/lib/rule.js index 75a73d8..e69fe8f 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -130,7 +130,17 @@ var getParamType = function getParamType(type, scope) { return _getParamType(type); }; +// +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) { @@ -198,16 +208,16 @@ var parsePattern = extd } 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]} - ) - ]; + 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,7 +228,7 @@ var parsePattern = extd {scope: condition.scope, pattern: condition[2]} ) ]; - } + } }).switcher(); var Rule = declare({ @@ -227,6 +237,7 @@ var Rule = declare({ 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 +247,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 +260,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; - - From da03eb9020c44668073a465a9cf2c426ee464d1b Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Fri, 29 Jan 2016 10:50:16 -0800 Subject: [PATCH 2/5] exposed topLevelTokens for future feature work --- lib/parser/nools/nool.parser.js | 2 +- lib/parser/nools/tokens.js | 145 +++++++++++++++++--------------- 2 files changed, 80 insertions(+), 67 deletions(-) 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; From ab29ca18d50e82c4a31a9aa4d56e6f4c08ffdd30 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Fri, 29 Jan 2016 10:29:06 -0800 Subject: [PATCH 3/5] needed to expose the match class for feature work added findIndex to lib/extends.js --- lib/context.js | 77 ++++++++++--------------------------------------- lib/extended.js | 23 +++++++++++++++ lib/match.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 lib/match.js 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); + + + From 44da66150d48d3a5858d8e5c54e5f0afbf0d0535 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Mon, 8 Feb 2016 14:25:14 -0800 Subject: [PATCH 4/5] conditional elment collect inital checkin --- lib/compile/index.js | 9 +- lib/nodes/collectNode.js | 264 ++++++++++++++++++++++++++++++++++++++ lib/nodes/index.js | 11 +- lib/pattern.js | 39 ++++++ lib/rule.js | 100 ++++++++++++++- readme.md | 40 ++++-- test/flow.dsl.test.js | 1 + test/flow/collect.nools | 83 ++++++++++++ test/flow/collect.test.js | 221 +++++++++++++++++++++++++++++++ 9 files changed, 751 insertions(+), 17 deletions(-) create mode 100644 lib/nodes/collectNode.js create mode 100644 test/flow/collect.nools create mode 100644 test/flow/collect.test.js 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/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/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 e69fe8f..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 @@ -131,6 +134,8 @@ 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) { @@ -202,12 +207,102 @@ 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) { + 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), @@ -218,6 +313,7 @@ var parsePattern = extd {scope: condition.scope, pattern: condition[2]} ) ]; + } } else { return [ new ObjectPattern( @@ -232,7 +328,9 @@ var parsePattern = extd }).switcher(); var Rule = declare({ + instance: { + constructor: function (name, options, pattern, cb) { this.name = name; this.pattern = pattern; 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); + }); + }); + }); +}); From f5d456f51373ae40a365f9f52790e96ed91a8eb5 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Mon, 8 Feb 2016 17:19:33 -0800 Subject: [PATCH 5/5] initial checkin for query feature --- lib/compile/index.js | 18 +- lib/constraint.js | 25 +- lib/constraintMatcher.js | 229 +++++++++--- lib/context.js | 16 +- lib/flow.js | 39 ++- lib/flowContainer.js | 37 +- lib/nodes/ParamNode.js | 27 ++ lib/nodes/collectNode.js | 2 +- lib/nodes/equalityNode.js | 39 ++- lib/nodes/fromNode.js | 13 +- lib/nodes/index.js | 46 ++- lib/nodes/queryNode.js | 19 + lib/parser/function/grammar.js | 47 +++ lib/parser/function/parser.js | 617 +++++++++++++++++++++++++++++++++ lib/parser/nools/tokens.js | 46 ++- lib/pattern.js | 20 ++ lib/rule.js | 296 +++++++++++++--- lib/workingMemory.js | 16 +- readme.md | 52 ++- test/flow/from.test.js | 8 +- test/flow/query.nools | 103 ++++++ test/flow/query.test.js | 206 +++++++++++ 22 files changed, 1759 insertions(+), 162 deletions(-) create mode 100644 lib/nodes/ParamNode.js create mode 100644 lib/nodes/queryNode.js create mode 100644 lib/parser/function/grammar.js create mode 100644 lib/parser/function/parser.js create mode 100644 test/flow/query.nools create mode 100644 test/flow/query.test.js diff --git a/lib/compile/index.js b/lib/compile/index.js index 095488e..cbdb248 100644 --- a/lib/compile/index.js +++ b/lib/compile/index.js @@ -117,12 +117,19 @@ var createRuleFromObject = (function () { } return function (obj, defined, scope) { - var name = obj.name; + var name = obj.name, + options = obj.options || {}; + + options.scope = scope; + if (extd.isEmpty(obj)) { throw new Error("Rule is empty"); } - var options = obj.options || {}; - options.scope = scope; + if( obj.isQuery ) { + obj.options.arguments = obj.args; + options.scope = extd.merge(options.scope, defined || {}); + return rules.createQuery(obj.name, obj.options, obj.constraints); + } var constraints = obj.constraints || [], l = constraints.length; if (!l) { constraints = ["true"]; @@ -166,6 +173,8 @@ exports.compile = function (flowObj, options, cb, Container) { defined.Buffer = Buffer; } var scope = merge({console: console}, options.scope); + scope.queries = flow.__queries; + // //add the anything added to the scope as a property forEach(flowObj.scope, function (s) { scope[s.name] = true; @@ -190,7 +199,8 @@ exports.compile = function (flowObj, options, cb, Container) { var rules = flowObj.rules; if (rules.length) { forEach(rules, function (rule) { - flow.__rules = flow.__rules.concat(createRuleFromObject(rule, defined, scope)); + var instance = createRuleFromObject(rule, defined, scope); + flow.__addRule(instance); }); } if (cb) { diff --git a/lib/constraint.js b/lib/constraint.js index 9ff975e..a24eb0a 100644 --- a/lib/constraint.js +++ b/lib/constraint.js @@ -213,7 +213,7 @@ Constraint.extend({ } }).as(exports, "HashConstraint"); -Constraint.extend({ +var FromConstraint = Constraint.extend({ instance: { constructor: function (constraints, options) { this.type = "from"; @@ -225,8 +225,8 @@ Constraint.extend({ return instanceOf(constraint, this._static) && this.get("alias") === constraint.get("alias") && deepEqual(this.constraints, constraint.constraints); }, - "assert": function (fact, fh) { - return this.constraints(fact, fh); + "assert": function (fact, fh, session) { + return this.constraints.call(session, fact, fh); }, getters: { @@ -236,7 +236,24 @@ Constraint.extend({ } } -}).as(exports, "FromConstraint"); +}); +exports.FromConstraint = FromConstraint; + +var QueryConstraint = FromConstraint.extend({ + instance: { + constructor: function (constraints, options) { + this.type = "query"; + this.constraints = constraintMatcher.getSourceMatcher(constraints, (options || {}), true); + extd.bindAll(this, ["assert"]); + }, + + "assert": function (fact, fh, session) { + return this.constraints.call(session, fact, fh); + } + } +}); +exports.QueryConstraint = QueryConstraint; + Constraint.extend({ instance: { diff --git a/lib/constraintMatcher.js b/lib/constraintMatcher.js index c3202bc..79d5d89 100644 --- a/lib/constraintMatcher.js +++ b/lib/constraintMatcher.js @@ -169,7 +169,7 @@ var lang = { ret = ret.concat(this.getIdentifiers(propChain[1])); break; } else { - propChain = propChain[1]; + propChain = propChain[1]; } } } @@ -187,10 +187,58 @@ var lang = { return removeDups(ret); }, + getArguments: function (rule) { + var ret = [], + rule2 = rule[2]; + + if (rule2 === "identifier") { + return [ (rule[0] + identSuffix) ]; + } else if (rule2 === "function") { + ret = ret.concat(this.getArguments(rule[0])).concat(this.getArguments(rule[1])); + } else if ( + rule2 !== "string" && + rule2 !== "number" && + rule2 !== "boolean" && + rule2 !== "regexp" && + rule2 !== "unary") { + //its an expression so keep going + if (rule2 === "prop") { + ret = ret.concat(this.getArguments(rule[0])); + if (rule[1]) { + var propChain = rule[1]; + //go through the member variables and collect any identifiers that may be in functions + while (isArray(propChain)) { + if (propChain[2] === "function") { + ret = ret.concat(this.getArguments(propChain[1])); + break; + } else { + propChain = propChain[1]; + } + } + } + + } else { + if (rule[0]) { + ret = ret.concat(this.getArguments(rule[0])); + } + if (rule[1]) { + ret = ret.concat(this.getArguments(rule[1])); + } + } + } + else { + return [rule[0]]; + } + //remove dups and return + return removeDups(ret); + }, + toConstraints: function (rule, options) { var ret = [], alias = options.alias, - scope = options.scope || {}; + scope = options.scope || {}, + queries = scope.queries, + fnName; var rule2 = rule[2]; @@ -216,38 +264,42 @@ var lang = { rule2 === "propLookup" || rule2 === "function" || rule2 === "logicalNot") { - var isReference = some(this.getIdentifiers(rule), function (i) { + var isReference = some(this.getIdentifiers(rule), function (i) { return i !== alias && !(i in definedFuncs) && !(i in scope); }); - switch (rule2) { - case "eq": - ret.push(new atoms[isReference ? "ReferenceEqualityConstraint" : "EqualityConstraint"](rule, options)); - break; - case "seq": - ret.push(new atoms[isReference ? "ReferenceEqualityConstraint" : "EqualityConstraint"](rule, options)); - break; - case "neq": - ret.push(new atoms[isReference ? "ReferenceInequalityConstraint" : "InequalityConstraint"](rule, options)); - break; - case "sneq": - ret.push(new atoms[isReference ? "ReferenceInequalityConstraint" : "InequalityConstraint"](rule, options)); - break; - case "gt": - ret.push(new atoms[isReference ? "ReferenceGTConstraint" : "ComparisonConstraint"](rule, options)); - break; - case "gte": - ret.push(new atoms[isReference ? "ReferenceGTEConstraint" : "ComparisonConstraint"](rule, options)); - break; - case "lt": - ret.push(new atoms[isReference ? "ReferenceLTConstraint" : "ComparisonConstraint"](rule, options)); - break; - case "lte": - ret.push(new atoms[isReference ? "ReferenceLTEConstraint" : "ComparisonConstraint"](rule, options)); - break; - default: - ret.push(new atoms[isReference ? "ReferenceConstraint" : "ComparisonConstraint"](rule, options)); - } - + if( rule2 === 'function' && queries && queries[rule[0][0]] ) { + ret.push(new atoms.QueryConstraint(rule, options)); + } + else { + switch (rule2) { + case "eq": + ret.push(new atoms[isReference ? "ReferenceEqualityConstraint" : "EqualityConstraint"](rule, options)); + break; + case "seq": + ret.push(new atoms[isReference ? "ReferenceEqualityConstraint" : "EqualityConstraint"](rule, options)); + break; + case "neq": + ret.push(new atoms[isReference ? "ReferenceInequalityConstraint" : "InequalityConstraint"](rule, options)); + break; + case "sneq": + ret.push(new atoms[isReference ? "ReferenceInequalityConstraint" : "InequalityConstraint"](rule, options)); + break; + case "gt": + ret.push(new atoms[isReference ? "ReferenceGTConstraint" : "ComparisonConstraint"](rule, options)); + break; + case "gte": + ret.push(new atoms[isReference ? "ReferenceGTEConstraint" : "ComparisonConstraint"](rule, options)); + break; + case "lt": + ret.push(new atoms[isReference ? "ReferenceLTConstraint" : "ComparisonConstraint"](rule, options)); + break; + case "lte": + ret.push(new atoms[isReference ? "ReferenceLTEConstraint" : "ComparisonConstraint"](rule, options)); + break; + default: + ret.push(new atoms[isReference ? "ReferenceConstraint" : "ComparisonConstraint"](rule, options)); + }; + } } return ret; }, @@ -411,16 +463,20 @@ var lang = { var matcherCount = 0; var toJs = exports.toJs = function (rule, scope, alias, equality, wrap) { /*jshint evil:true*/ - var js = lang.parse(rule); - scope = scope || {}; - var vars = lang.getIdentifiers(rule); - var closureVars = ["var indexOf = definedFuncs.indexOf; var hasOwnProperty = Object.prototype.hasOwnProperty;"], funcVars = []; - extd(vars).filter(function (v) { + var closureVars = ["var indexOf = definedFuncs.indexOf; var hasOwnProperty = Object.prototype.hasOwnProperty;"], + js = lang.parse(rule), + funcVars = [], + closureBody, f, fnSig, vars, closureVars; + // + scope = scope || {}; + vars = lang.getIdentifiers(rule); + // + extd(vars).filter(function (v) { var ret = ["var ", v, " = "]; if (definedFuncs.hasOwnProperty(v)) { ret.push("definedFuncs['", v, "']"); } else if (scope.hasOwnProperty(v)) { - ret.push("scope['", v, "']"); + ret.push("scope['", v, "']"); } else { return true; } @@ -429,7 +485,7 @@ var toJs = exports.toJs = function (rule, scope, alias, equality, wrap) { return false; }).forEach(function (v) { var ret = ["var ", v, " = "]; - if (equality || v !== alias) { + if (equality || v !== alias) { ret.push("fact." + v); } else if (v === alias) { ret.push("hash.", v, ""); @@ -437,9 +493,85 @@ var toJs = exports.toJs = function (rule, scope, alias, equality, wrap) { ret.push(";"); funcVars.push(ret.join("")); }); - var closureBody = closureVars.join("") + "return function matcher" + (matcherCount++) + (!equality ? "(fact, hash){" : "(fact){") + funcVars.join("") + " return " + (wrap ? wrap(js) : js) + ";}"; - var f = new Function("definedFuncs, scope", closureBody)(definedFuncs, scope); - //console.log(f.toString()); + // + closureBody = closureVars.join("") + "return function matcher" + (matcherCount++) + (!equality ? "(fact, hash){" : "(fact){") + funcVars.join("") + " return " + (wrap ? wrap(js) : js) + ";}"; + f = new Function("definedFuncs, scope", closureBody)(definedFuncs, scope); + return f; +}; + +var identSuffix = ':~ident~:'; + +var toJsFunc = function (rule, scope, alias, equality, wrap) { + /*jshint evil:true*/ + var closureVars = ["var indexOf = definedFuncs.indexOf; var hasOwnProperty = Object.prototype.hasOwnProperty;"], + js = lang.parse(rule), + funcVars = [], + queries, queryVar, closureBody, f, querySig, queryParams, fnSig, args, closureVars; + // + scope = scope || {}; + queries = scope.queries; + args = lang.getArguments(rule); + // + extd(args).filter(function (v) { + var idx = 'string' === typeof v ? ( v.indexOf(identSuffix) ) : undefined, + ret; + // + if( idx > 0 ) { // it's an identifier... + v = v.substr(0,idx); + ret = ["var ", v, " = "]; + if (definedFuncs.hasOwnProperty(v)) { + ret.push("definedFuncs['", v, "']"); + } else if (scope.hasOwnProperty(v)) { + ret.push("scope['", v, "']"); + } else { + return true; + } + } + else { // it's a literal... + return false; // skip it's not a closure var, scope or define + } + ret.push(";"); + closureVars.push(ret.join("")); + return false; + }).forEach(function (v) { + + var idx = v.indexOf(identSuffix) + ,ret; + v = v.substr(0,idx); + ret = ["var ", v, " = "]; + if( queries && queries[v] ) { + queryVar = v; + ret.push("this.getQuery('" + v + "')"); + } + else if (equality || v !== alias) { + ret.push("fact." + v); + } else if (v === alias) { + ret.push("hash.", v, ""); + } + ret.push(";"); + funcVars.push(ret.join("")); + }); + // + if( queryVar ) { // queryVar is always @ 0, with remaining params...-> + queryParams = []; + for(var i=1; i < args.length; i++) { + var arg = args[i], + idx = ('string' === typeof arg) ? arg.indexOf(identSuffix) : undefined; + // + arg = idx ? (arg.substr(0,idx)) : arg; + queryParams.push(arg); + } + querySig = [queryVar,'(', queryParams.join(','), ');']; + closureBody = ['return function(fact,hash){ ', funcVars.join('\n'), ('return ' + querySig.join('')), ' }' ]; + fnSig = closureBody.join('\n'); + f = new Function('definedFuncs', 'scope', fnSig); + f = f(definedFuncs, scope); + return f; + } + else { + closureBody = closureVars.join("") + "return function matcher" + (matcherCount++) + (!equality ? "(fact, hash){" : "(fact){") + funcVars.join("") + " return " + (wrap ? wrap(js) : js) + ";}"; + f = new Function("definedFuncs, scope", closureBody)(definedFuncs, scope); + } return f; }; @@ -452,9 +584,16 @@ exports.getMatcher = function (rule, options, equality) { exports.getSourceMatcher = function (rule, options, equality) { options = options || {}; - return toJs(rule, options.scope, options.alias, equality, function (src) { - return src; - }); + if( rule[2] === 'function' ) { + return toJsFunc(rule, options.scope, options.alias, equality, function (src) { + return src; + }); + } + else { + return toJs(rule, options.scope, options.alias, equality, function (src) { + return src; + }); + } }; exports.toConstraints = function (constraint, options) { diff --git a/lib/context.js b/lib/context.js index 28b8ac9..de448ae 100644 --- a/lib/context.js +++ b/lib/context.js @@ -18,7 +18,7 @@ function createContextHash(paths, hashCode) { return ret; } -function createPathstHash(paths, hashCode) { +function createPathsHash(paths, hashCode) { var ret = "", i = -1, l = paths.length; @@ -33,13 +33,13 @@ function createPathstHash(paths, hashCode) { 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/flow.js b/lib/flow.js index c636305..4ba6a12 100644 --- a/lib/flow.js +++ b/lib/flow.js @@ -1,4 +1,4 @@ -"use strict"; + "use strict"; var extd = require("./extended"), bind = extd.bind, declare = extd.declare, @@ -18,9 +18,11 @@ module.exports = declare(EventEmitter, { executionStrategy: null, constructor: function (name, conflictResolutionStrategy) { - this.env = null; - this.name = name; - this.__rules = {}; + this.env = null; + this.name = name; + this.__rules = {}; + this.__queries = {}; + this.queryResults = {}; this.conflictResolutionStrategy = conflictResolutionStrategy; this.workingMemory = new WorkingMemory(); this.agenda = new AgendaTree(this, conflictResolutionStrategy); @@ -102,7 +104,17 @@ module.exports = declare(EventEmitter, { }, rule: function (rule) { - this.rootNode.assertRule(rule); + var me = this, rhsFn; + // + if(rule.pattern.queryName) { + this.queryResults[rule.pattern.queryName] = {}; // the hashCode is by parameter fact id(s) + // supply the query entry point + this.__queries[rule.pattern.queryName] = function() { + var ret = rule.pattern.queryFn.apply(me, arguments); // add the sessions context + return ret; + } + } + this.rootNode.assertRule(rule, me); }, matchUntilHalt: function (cb) { @@ -111,7 +123,22 @@ module.exports = declare(EventEmitter, { match: function (cb) { return (this.executionStrategy = new ExecutionStragegy(this)).execute().classic(cb).promise(); - } + }, + getQuery: function(name) { + return this.__queries[name]; + }, + + getParamNode: function(pattern) { + var paramNodes = this.rootNode.paramNodes, + target; + paramNodes.some(function(node) { + if( node.pattern === pattern) { + target = node; + return true; + } + }); + return target; + } } }); \ No newline at end of file diff --git a/lib/flowContainer.js b/lib/flowContainer.js index 3bb7f2e..6174518 100644 --- a/lib/flowContainer.js +++ b/lib/flowContainer.js @@ -19,6 +19,7 @@ var FlowContainer = declare({ this.cb = cb; this.__rules = []; this.__defined = {}; + this.__queries = {}; this.conflictResolutionStrategy = conflictResolution; if (cb) { cb.call(this, this); @@ -42,20 +43,37 @@ var FlowContainer = declare({ } return ret; }, - addDefined: function (name, cls) { //normalize this.__defined[name.toLowerCase()] = cls; return cls; }, - rule: function () { - this.__rules = this.__rules.concat(rule.createRule.apply(rule, arguments)); + rule: function (name, options, conditions, cb) { + var instance; + if(extd.isArray(options)) { + cb = conditions; + conditions = options; + options = {}; + } + options.scope = options.scope || {}; + options.scope.queries = this.__queries; + instance = rule.createRule(name, options, conditions, cb); + this.__addRule(instance); return this; }, + query: function(name, options, conditions) { + var instance, pattern, scope; + instance = rule.createQuery(name, options,conditions); + this.__addRule(instance); + return this; + }, + getSession: function () { - var flow = new Flow(this.name, this.conflictResolutionStrategy); + var flow = new Flow(this.name, this.conflictResolutionStrategy), + initialFact; + forEach(this.__rules, function (rule) { flow.rule(rule); }); @@ -70,8 +88,17 @@ var FlowContainer = declare({ return extd.some(this.__rules, function (rule) { return rule.name === name; }); - } + }, + __addRule: function(ruleInstance) { + if( ruleInstance instanceof rule.Query ) { + this.__queries[ruleInstance.name] = ruleInstance; + this.__rules.push(ruleInstance); + } + else { + this.__rules = this.__rules.concat(ruleInstance); + } + } }, "static": { diff --git a/lib/nodes/ParamNode.js b/lib/nodes/ParamNode.js new file mode 100644 index 0000000..abbd9f3 --- /dev/null +++ b/lib/nodes/ParamNode.js @@ -0,0 +1,27 @@ +var FromNode = require("./fromNode"), + extd = require("../extended"), + Context = require("../context"); + +FromNode.extend({ + instance: { + nodeType: "ParamNode", + + assertLeft: function (context) { + this.leftContext = context; + }, + /** + assumption: this is called left-right query parameters + */ + assertParam: function(param) { + var createdFact, leftContext, createdFactId; + if (this.type(param)) { + createdFact = this.workingMemory.getFactHandle(param); + rc = new Context(createdFact, null, null); // dont' need pattern arg, since from nodes arent' shared! + rc.set(this.alias, param); + rc = rc.clone(createdFact, null, this.leftContext.match.merge(rc.match)); + this.__propagate("assert", rc); + } + return createdFact ? createdFact.id : undefined; + } + } +}).as(module); \ No newline at end of file diff --git a/lib/nodes/collectNode.js b/lib/nodes/collectNode.js index 258dddb..834c901 100644 --- a/lib/nodes/collectNode.js +++ b/lib/nodes/collectNode.js @@ -18,7 +18,7 @@ FromNode.extend({ // constructor: function (pattern, wm) { this._super([pattern, wm]); - this.setAliases = pattern.setAliases; + this.setAliases = pattern.setAliases || pattern.setOrientedAliases; this.collectionHash = {}; this.isCollectionObject = this.type({}) ? true : false this.fnCollectionSrc = pattern.fnCollectionSrc; diff --git a/lib/nodes/equalityNode.js b/lib/nodes/equalityNode.js index 16d8113..dd35e58 100644 --- a/lib/nodes/equalityNode.js +++ b/lib/nodes/equalityNode.js @@ -1,4 +1,8 @@ -var AlphaNode = require("./alphaNode"); +var AlphaNode = require("./alphaNode"), + Context = require('../context'), + extd = require("../extended"), + intersection = extd.intersection, + InitialFact = require('../pattern').InitialFact; AlphaNode.extend({ instance: { @@ -11,7 +15,38 @@ AlphaNode.extend({ assert: function (context) { if ((this.memory[context.pathsHash] = this.constraintAssert(context.factHash))) { - this.__propagate("assert", context); + if( this.constraint.alias === "__i__" && context.fact.object instanceof InitialFact ) { + var entrySet = this.__entrySet, i = entrySet.length, entry, outNode, paths, continuingPaths; + var queries, rules = []; + + entrySet.forEach(function(entry) { + outNode = entry.key; + paths = entry.value; + // + + if(paths[0].queryInitialFact) { + queries = queries || []; + queries.push(entry); + } + else { + rules.push(entry); + } + }); + entrySet = queries ? ( rules.concat(queries) ) : rules; + while (--i > -1) { + entry = entrySet[i]; + outNode = entry.key; + paths = entry.value; + + if ((continuingPaths = intersection(paths, context.paths)).length) { + outNode['assert'](new Context(context.fact, continuingPaths, context.match)); + } + } + + } + else { + this.__propagate("assert", context); + } } }, diff --git a/lib/nodes/fromNode.js b/lib/nodes/fromNode.js index 3b2135c..4e27625 100644 --- a/lib/nodes/fromNode.js +++ b/lib/nodes/fromNode.js @@ -42,11 +42,16 @@ JoinNode.extend({ }, __createMatches: function (context) { - var fh = context.factHash, o = this.from(fh); + var fh = context.factHash, o = this.from(fh, fh, this.session); if (isArray(o)) { - for (var i = 0, l = o.length; i < l; i++) { - this.__checkMatch(context, o[i], true); - } + if( this.type(o) ) { + this.__checkMatch(context, o, true); + } + else { + for (var i = 0, l = o.length; i < l; i++) { + this.__checkMatch(context, o[i], true); + } + } } else if (isDefined(o)) { this.__checkMatch(context, o, true); } diff --git a/lib/nodes/index.js b/lib/nodes/index.js index 0a23294..ad6d939 100644 --- a/lib/nodes/index.js +++ b/lib/nodes/index.js @@ -11,8 +11,9 @@ var extd = require("../extended"), FromExistsPattern = pattern.FromExistsPattern, NotPattern = pattern.NotPattern, CompositePattern = pattern.CompositePattern, - InitialFactPattern = pattern.InitialFactPattern, - CollectPattern = pattern.CollectPattern, + InitialFactPattern = pattern.InitialFactPattern, + CollectPattern = pattern.CollectPattern, + ParamPattern = pattern.ParamPattern, constraints = require("../constraint"), HashConstraint = constraints.HashConstraint, ReferenceConstraint = constraints.ReferenceConstraint, @@ -25,13 +26,14 @@ var extd = require("../extended"), FromNotNode = require("./fromNotNode"), ExistsNode = require("./existsNode"), ExistsFromNode = require("./existsFromNode"), + CollectNode = require('./collectNode'), + ParamNode = require('./ParamNode'), LeftAdapterNode = require("./leftAdapterNode"), RightAdapterNode = require("./rightAdapterNode"), TypeNode = require("./typeNode"), TerminalNode = require("./terminalNode"), - PropertyNode = require("./propertyNode"), - CollectNode = require("./collectNode"); - + QueryNode = require("./queryNode"), + PropertyNode = require("./propertyNode"); function hasRefernceConstraints(pattern) { return some(pattern.constraints || [], function (c) { @@ -44,6 +46,8 @@ declare({ constructor: function (wm, agendaTree) { this.terminalNodes = []; this.joinNodes = []; + this.fromNodes = []; + this.paramNodes = []; this.nodes = []; this.constraints = []; this.typeNodes = []; @@ -56,10 +60,17 @@ declare({ this.workingMemory = wm; }, - assertRule: function (rule) { - var terminalNode = new TerminalNode(this.bucket, this.__ruleCount++, rule, this.agendaTree); + assertRule: function (rule, session) { + var terminalNode; + if(rule.pattern.queryName) { + terminalNode = new QueryNode(this.bucket, this.__ruleCount++, rule, this.agendaTree); + } + else { + terminalNode = new TerminalNode(this.bucket, this.__ruleCount++, rule, this.agendaTree); + } this.__addToNetwork(rule, rule.pattern, terminalNode); this.__mergeJoinNodes(); + this.__linkFromNodes(session); this.terminalNodes.push(terminalNode); }, @@ -116,7 +127,11 @@ declare({ } } }, - + + __linkFromNodes: function(session) { + var me = this; + me.fromNodes.forEach(function(node) { node.session = session; } ); + }, __checkEqual: function (node) { var constraints = this.constraints, i = constraints.length - 1; for (; i >= 0; i--) { @@ -170,8 +185,15 @@ declare({ 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 CollectPattern ) { + joinNode = new CollectNode(pattern.rightPattern, this.workingMemory); + } else if (pattern.rightPattern instanceof ParamPattern ) { + joinNode = new ParamNode(pattern.rightPattern, this.workingMemory); + this.paramNodes.push(joinNode); // remember so query fn can inject parameter(s) initiating prop + joinNode.pattern = pattern.rightPattern; // must have pattern instance so node can be located } else if (pattern.rightPattern instanceof FromPattern) { joinNode = new FromNode(pattern.rightPattern, this.workingMemory); + this.fromNodes.push(joinNode); } else if (pattern instanceof CompositePattern && !hasRefernceConstraints(pattern.leftPattern) && !hasRefernceConstraints(pattern.rightPattern)) { joinNode = new BetaNode(); this.joinNodes.push(joinNode); @@ -191,9 +213,11 @@ declare({ }, __addToNetwork: function (rule, pattern, outNode, side) { + var initialPattern; if (pattern instanceof ObjectPattern) { if (!(pattern instanceof InitialFactPattern) && (!side || side === "left")) { - this.__createBetaNode(rule, new CompositePattern(new InitialFactPattern(), pattern), outNode, side); + initialPattern = new CompositePattern(new InitialFactPattern(), pattern); + this.__createBetaNode(rule, initialPattern, outNode, side); } else { this.__createAlphaNode(rule, pattern, outNode, side); } @@ -214,7 +238,9 @@ declare({ __createAlphaNode: function (rule, pattern, outNode, side) { var typeNode, parentNode; if (!(pattern instanceof FromPattern)) { - + if( rule.pattern.queryName && pattern instanceof InitialFactPattern ) { + pattern.queryInitialFact = true; // used to ensure initial fact propagates before queries are used in other rule LHS + } var constraints = pattern.get("constraints"); typeNode = this.__createTypeNode(rule, pattern); var aliasNode = this.__createAliasNode(rule, pattern); diff --git a/lib/nodes/queryNode.js b/lib/nodes/queryNode.js new file mode 100644 index 0000000..c5c632c --- /dev/null +++ b/lib/nodes/queryNode.js @@ -0,0 +1,19 @@ +var TerminalNode = require("./terminalNode"), + extd = require("../extended"), + bind = extd.bind; + +TerminalNode.extend({ + instance: { + + __assertModify: function (context) { + var match = context.match; + if (match.isMatch) { + var rule = this.rule, bucket = this.bucket; + rule.fire(this.agenda.flow, match); + } + }, + toString: function () { + return "QueryNode " + this.rule.name; + } + } +}).as(module); \ No newline at end of file diff --git a/lib/parser/function/grammar.js b/lib/parser/function/grammar.js new file mode 100644 index 0000000..a804e78 --- /dev/null +++ b/lib/parser/function/grammar.js @@ -0,0 +1,47 @@ +var Parser = require("jison").Parser, fs = require("fs"); + +var grammar = { + "lex": { + "rules": [ + ["\\s+", "/* skip whitespace */"], + ["([a-zA-Z_$][0-9a-zA-Z_$]*)", "return 'IDENTIFIER';"], + [",", "return ',';"], + ["\\(", "return '(';"], + ["\\)", "return ')';"], + [":", "return ':';"], + ["$", "return 'EOF';"] + ] + }, + + "bnf": { + "expressions": [ + [ "OBJECT_EXPRESSION EOF", "return $1;" ] + ], + + "DECL_LIST": [ + ["ARG_DECL", "$$ = [$1, 'arguments']"], + ["DECL_LIST , ARG_DECL", "$$ = [[$1[0][0],$1[0][1], 'arg-decl'], $3, 'arguments']"] + ], + + "ARG_DECL" : [ + ["IDENTIFIER_EXPRESSION ARG_IDENT", "$$ = [$1, $2, 'arg-decl']"] + ], + + "ARG_IDENT" : [ + ["IDENTIFIER_EXPRESSION", "$$ = [$1, 'arg-ident']"] + ], + + "IDENTIFIER_EXPRESSION": [ + [ "IDENTIFIER", "$$ = [String(yytext), null, 'identifier'];" ] + ], + + "OBJECT_EXPRESSION": [ + "IDENTIFIER_EXPRESSION", + [ "OBJECT_EXPRESSION ( )", "$$ = [$1, ['arguments'], 'function']"], + [ "OBJECT_EXPRESSION ( DECL_LIST )", "$$ = [$1, $3, 'function']"] + ] + } +}; + +var parser = new Parser(grammar); +fs.writeFileSync(__dirname + '/parser.js', parser.generate()); diff --git a/lib/parser/function/parser.js b/lib/parser/function/parser.js new file mode 100644 index 0000000..34cfada --- /dev/null +++ b/lib/parser/function/parser.js @@ -0,0 +1,617 @@ +/* parser generated by jison 0.4.13 */ +/* + Returns a Parser object of the following structure: + + Parser: { + yy: {} + } + + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + + + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + + + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var parser = (function(){ +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"expressions":3,"OBJECT_EXPRESSION":4,"EOF":5,"DECL_LIST":6,"ARG_DECL":7,",":8,"IDENTIFIER_EXPRESSION":9,"ARG_IDENT":10,"IDENTIFIER":11,"(":12,")":13,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",8:",",11:"IDENTIFIER",12:"(",13:")"}, +productions_: [0,[3,2],[6,1],[6,3],[7,2],[10,1],[9,1],[4,1],[4,3],[4,4]], +performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */ +/**/) { +/* this == yyval */ + +var $0 = $$.length - 1; +switch (yystate) { +case 1:return $$[$0-1]; +break; +case 2:this.$ = [$$[$0], 'arguments'] +break; +case 3:this.$ = [[$$[$0-2][0][0],$$[$0-2][0][1], 'arg-decl'], $$[$0], 'arguments'] +break; +case 4:this.$ = [$$[$0-1], $$[$0], 'arg-decl'] +break; +case 5:this.$ = [$$[$0], 'arg-ident'] +break; +case 6:this.$ = [String(yytext), null, 'identifier']; +break; +case 8:this.$ = [$$[$0-2], ['arguments'], 'function'] +break; +case 9:this.$ = [$$[$0-3], $$[$0-1], 'function'] +break; +} +}, +table: [{3:1,4:2,9:3,11:[1,4]},{1:[3]},{5:[1,5],12:[1,6]},{5:[2,7],12:[2,7]},{5:[2,6],8:[2,6],11:[2,6],12:[2,6],13:[2,6]},{1:[2,1]},{6:8,7:9,9:10,11:[1,4],13:[1,7]},{5:[2,8],12:[2,8]},{8:[1,12],13:[1,11]},{8:[2,2],13:[2,2]},{9:14,10:13,11:[1,4]},{5:[2,9],12:[2,9]},{7:15,9:10,11:[1,4]},{8:[2,4],13:[2,4]},{8:[2,5],13:[2,5]},{8:[2,3],13:[2,3]}], +defaultActions: {5:[2,1]}, +parseError: function parseError(str, hash) { + if (hash.recoverable) { + this.trace(str); + } else { + throw new Error(str); + } +}, +parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + var args = lstack.slice.call(arguments, 1); + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == 'undefined') { + this.lexer.yylloc = {}; + } + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === 'function') { + this.parseError = this.yy.parseError; + } else { + this.parseError = Object.getPrototypeOf(this).parseError; + } + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || EOF; + if (typeof token !== 'number') { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == 'undefined') { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === 'undefined' || !action.length || !action[0]) { + var errStr = ''; + expected = []; + for (p in table[state]) { + if (this.terminals_[p] && p > TERROR) { + expected.push('\'' + this.terminals_[p] + '\''); + } + } + if (this.lexer.showPosition) { + errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + this.lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; + } else { + errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); + } + this.parseError(errStr, { + text: this.lexer.match, + token: this.terminals_[symbol] || symbol, + line: this.lexer.yylineno, + loc: yyloc, + expected: expected + }); + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) { + recovering--; + } + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }; + if (ranges) { + yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]; + } + r = this.performAction.apply(yyval, [ + yytext, + yyleng, + yylineno, + this.yy, + action[1], + vstack, + lstack + ].concat(args)); + if (typeof r !== 'undefined') { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +}}; +/* generated by jison-lex 0.2.1 */ +var lexer = (function(){ +var lexer = { + +EOF:1, + +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, + +// resets the lexer, sets new input +setInput:function (input) { + this._input = input; + this._more = this._backtrack = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }; + if (this.options.ranges) { + this.yylloc.range = [0,0]; + } + this.offset = 0; + return this; + }, + +// consumes and returns one char from the input +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) { + this.yylloc.range[1]++; + } + + this._input = this._input.slice(1); + return ch; + }, + +// unshifts one char (or a string) into the input +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length - 1); + this.matched = this.matched.substr(0, this.matched.length - 1); + + if (lines.length - 1) { + this.yylineno -= lines.length - 1; + } + var r = this.yylloc.range; + + this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - lines[0].length : + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + this.yyleng = this.yytext.length; + return this; + }, + +// When called from action, caches matched text and appends it on next action +more:function () { + this._more = true; + return this; + }, + +// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. +reject:function () { + if (this.options.backtrack_lexer) { + this._backtrack = true; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + + } + return this; + }, + +// retain first n characters of the match +less:function (n) { + this.unput(this.match.slice(n)); + }, + +// displays already matched input, i.e. for error messages +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, + +// displays upcoming input, i.e. for error messages +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); + }, + +// displays the character position where the lexing error occurred, i.e. for error messages +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c + "^"; + }, + +// test the lexed token: return FALSE when not a match, otherwise return token +test_match:function (match, indexed_rule) { + var token, + lines, + backup; + + if (this.options.backtrack_lexer) { + // save context + backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }; + if (this.options.ranges) { + backup.yylloc.range = this.yylloc.range.slice(0); + } + } + + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno += lines.length; + } + this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines ? + lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : + this.yylloc.last_column + match[0].length + }; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._backtrack = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); + if (this.done && this._input) { + this.done = false; + } + if (token) { + return token; + } else if (this._backtrack) { + // recover context + for (var k in backup) { + this[k] = backup[k]; + } + return false; // rule action called reject() implying the next rule should be tested instead. + } + return false; + }, + +// return next match in input +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) { + this.done = true; + } + + var token, + match, + tempMatch, + index; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i = 0; i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (this.options.backtrack_lexer) { + token = this.test_match(tempMatch, rules[i]); + if (token !== false) { + return token; + } else if (this._backtrack) { + match = false; + continue; // rule action called reject() implying a rule MISmatch. + } else { + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + } else if (!this.options.flex) { + break; + } + } + } + if (match) { + token = this.test_match(match, rules[index]); + if (token !== false) { + return token; + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return false; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { + text: "", + token: null, + line: this.yylineno + }); + } + }, + +// return next match that has a token +lex:function lex() { + var r = this.next(); + if (r) { + return r; + } else { + return this.lex(); + } + }, + +// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) +begin:function begin(condition) { + this.conditionStack.push(condition); + }, + +// pop the previously active lexer condition state off the condition stack +popState:function popState() { + var n = this.conditionStack.length - 1; + if (n > 0) { + return this.conditionStack.pop(); + } else { + return this.conditionStack[0]; + } + }, + +// produce the lexer rule set which is active for the currently active lexer condition state +_currentRules:function _currentRules() { + if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { + return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; + } else { + return this.conditions["INITIAL"].rules; + } + }, + +// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available +topState:function topState(n) { + n = this.conditionStack.length - 1 - Math.abs(n || 0); + if (n >= 0) { + return this.conditionStack[n]; + } else { + return "INITIAL"; + } + }, + +// alias for begin(condition) +pushState:function pushState(condition) { + this.begin(condition); + }, + +// return the number of states currently on the stack +stateStackSize:function stateStackSize() { + return this.conditionStack.length; + }, +options: {}, +performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START +/**/) { + +var YYSTATE=YY_START; +switch($avoiding_name_collisions) { +case 0:/* skip whitespace */ +break; +case 1:return 11; +break; +case 2:return 8; +break; +case 3:return 12; +break; +case 4:return 13; +break; +case 5:return ':'; +break; +case 6:return 5; +break; +} +}, +rules: [/^(?:\s+)/,/^(?:([a-zA-Z_$][0-9a-zA-Z_$]*))/,/^(?:,)/,/^(?:\()/,/^(?:\))/,/^(?::)/,/^(?:$)/], +conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6],"inclusive":true}} +}; +return lexer; +})(); +parser.lexer = lexer; +function Parser () { + this.yy = {}; +} +Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); + + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = parser; +exports.Parser = parser.Parser; +exports.parse = function () { return parser.parse.apply(parser, arguments); }; +exports.main = function commonjsMain(args) { + if (!args[1]) { + console.log('Usage: '+args[0]+' FILE'); + process.exit(1); + } + var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); + return exports.parser.parse(source); +}; +if (typeof module !== 'undefined' && require.main === module) { + exports.main(process.argv.slice(1)); +} +} \ No newline at end of file diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 2ead85f..6c27df1 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -5,9 +5,11 @@ 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"); + predicateBeginExp = new RegExp(" *(" + predicates.join("|") + ") *\\(", "g"), + parser = require('../constraint/parser.js'), + fnSigParser = require('../function/parser.js'); var isWhiteSpace = function (str) { return str.replace(/[\s|\n|\r|\t]/g, "").length === 0; @@ -167,7 +169,7 @@ var ruleTokens = { } }; })(), - + "agenda-group": function () { return this.agendaGroup.apply(this, arguments); }, @@ -220,6 +222,7 @@ var ruleTokens = { }; })() }; +var queryTopRegExp = /^([$A-Z_][0-9A-Z_$]*)\s*(\(.*\))\s*$/i; var topLevelTokens = { "/": function (orig) { @@ -338,7 +341,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) { @@ -357,7 +360,40 @@ var topLevelTokens = { } else { throw new Error("missing name"); } - } + }, + "query": function (orig, context, parse) { + var src = orig.replace(/^query\s*/, "") + ,idxBracket = src.indexOf('{') + ,fnSig = (idxBracket > -1) ? src.substr(0, idxBracket) : undefined + ,queryFn,parsed, name, args; + if( fnSig ) { + fnSig = fnSig.match(queryTopRegExp); + if( fnSig ) { + fnSig = fnSig[0]; + parsed = fnSigParser.parse(fnSig); + src = src.replace(fnSig, "").replace(/^\s*|\s*$/g, ""); + if (utils.findNextToken(src) === "{") { + name = parsed[0][0]; + args = parsed[1]; + var rule = { name: name, options: {arguments:args}, constraints: null, action: null }; + var body = utils.getTokensBetween(src, "{", "}", true).join(""); + src = src.replace(body, ""); + rule.isQuery = true; + rule.args = args; + ruleTokens.when(body, rule); + context.rules.push(rule); + // + return src; + } else { + throw new Error("unexpected token : expected : '{' found : '" + utils.findNextToken(src) + "'"); + } + } + } + else { + throw new Error("invalid query signature"); + } + } + }; exports.topLevelTokens = topLevelTokens; diff --git a/lib/pattern.js b/lib/pattern.js index d18f238..adf4614 100644 --- a/lib/pattern.js +++ b/lib/pattern.js @@ -138,6 +138,26 @@ var CollectPattern = FromPattern.extend({ } }).as(exports, "CollectPattern"); +var ParamPattern = FromPattern.extend({ + instance: { + constructor: function (type, alias, scope) { + ObjectPattern.prototype.constructor.call(this, type, alias, [true, null, 'identifier'], {}, {scope:scope, pattern: 'true'}); + this.from = { + assert: function() { + return this.parameter; + } + } + } + // + ,setParam: function(session, p) { + var reteNode = session.getParamNode(this) + ,factId = reteNode.assertParam(p); + return factId; + } + } +}).as(exports, "ParamPattern"); + + Pattern.extend({ instance: { diff --git a/lib/rule.js b/lib/rule.js index 72dba38..4190fbc 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -16,7 +16,8 @@ var extd = require("./extended"), FromNotPattern = pattern.FromNotPattern, FromExistsPattern = pattern.FromExistsPattern, CompositePattern = pattern.CompositePattern, - CollectPattern = pattern.CollectPattern; + ParamPattern = pattern.ParamPattern, + CollectPattern = pattern.CollectPattern; // var parseConstraint = function (constraint) { @@ -51,7 +52,7 @@ var normailizeConstraint = extd }) //handle case where c[2] is a hash rather than a constraint string .isLength(3, function (c) { - if (isString(c[2]) && /^from +/.test(c[2])) { + if ( isString(c[2]) && /^from +/.test(c[2]) ) { var extra = c[2]; c.splice(2, 0, "true"); c[3] = null; @@ -145,7 +146,6 @@ function mergePatterns(patterns, merged, setAliases) { return merged; } -// var parsePattern = extd .switcher() .containsAt("or", 0, function (condition) { @@ -163,9 +163,9 @@ var parsePattern = extd new FromNotPattern( getParamType(condition[0], condition.scope), condition[1] || "m", - parseConstraint(condition[2] || "true"), + parseConstraint(condition[2] || "true", condition.scope), condition[3] || {}, - parseConstraint(condition[4].from), + parseConstraint(condition[4].from, condition.scope), {scope: condition.scope, pattern: condition[2]} ) ]; @@ -174,7 +174,7 @@ var parsePattern = extd new NotPattern( getParamType(condition[0], condition.scope), condition[1] || "m", - parseConstraint(condition[2] || "true"), + parseConstraint(condition[2] || "true", condition.scope), condition[3] || {}, {scope: condition.scope, pattern: condition[2]} ) @@ -189,9 +189,9 @@ var parsePattern = extd new FromExistsPattern( getParamType(condition[0], condition.scope), condition[1] || "m", - parseConstraint(condition[2] || "true"), + parseConstraint(condition[2] || "true", condition.scope), condition[3] || {}, - parseConstraint(condition[4].from), + parseConstraint(condition[4].from, condition.scope), {scope: condition.scope, pattern: condition[2]} ) ]; @@ -200,12 +200,134 @@ var parsePattern = extd new ExistsPattern( getParamType(condition[0], condition.scope), condition[1] || "m", - parseConstraint(condition[2] || "true"), + parseConstraint(condition[2] || "true", condition.scope), condition[3] || {}, {scope: condition.scope, pattern: condition[2]} ) ]; } + }) + .containsAt("query", 0, function(condition) { + var params = condition[1] // params.name, params.arguments, params.conditions + ,scope = condition.scope + ,paramPattern, compPat, collectPattern, setOrientedAliases = [], patterns = [], queryFn, rhsFn, fnCollectionSrc, collection, tmp, bottom; + /* + The query function is call-able as a condition in an rule's LHS. + Query condition is converted to a from queryFn(params); + // + ex: list : Array [ expresson ] from MsgFilter($varA, $varB,...);// the from modifier can be omitted for brevity + */ + function buildQuery(name, paramPatterns, scope, fnCollectionSrc) { + var wrapper = ['return ', undefined] // function QueryWrapper(paramPatterns) { } + ,fnSig = ['function ', name, '(', undefined, ')'] + ,fnQuery = [undefined, '{', 'var session = this, factIds = [], queryHash, hashCode, queryResult, collFact;', + undefined, + ' hashCode = factIds.join(":");', + ' queryHash = session.queryResults["' + name + '"]', + ' queryResult = queryHash[hashCode];', + ' if(!queryResult) { collFact = fnCollectionSrc(session.workingMemory); queryResult = queryHash[hashCode] = collFact.object; }', + ' return queryResult;', + ' }' + ] + ,paramExpr = ['factIds.push(paramPatterns["', undefined, '"].setParam(session,', undefined, '));'] + ,tmp, wrapperFn, queryFn; + // + // build the signature + tmp = []; + paramPatterns.forEach(function(param) { + tmp.push(param.alias); + }); + fnSig[3] = tmp.join(','); + fnQuery[0] = fnSig = fnSig.join(''); + // + // build the query fn body + tmp = []; + paramPatterns.forEach(function(param) { + var a = ['paramPatterns["', param.alias, '"].setParam(', param.alias, ');']; + paramExpr[1] = param.alias; paramExpr[3] = param.alias; + tmp.push(paramExpr.join('')); + }); + fnQuery[3] = tmp.join('\n'); + fnQuery = fnQuery.join('\n'); + wrapper[1] = fnQuery; + wrapper = wrapper.join(''); + // + wrapperFn = new Function(['paramPatterns', 'fnCollectionSrc'], wrapper); + // we need a hash of param patterns not an array + tmp = {}; + paramPatterns.forEach(function(pattern) { tmp[pattern.alias] = pattern; }); + queryFn = wrapperFn(tmp, fnCollectionSrc); + return queryFn; + } + /** + collection src + */ + function factoryCollectionSrc() { + var theFact, theCollection; + return function(wm) { + if(!theFact) { + theCollection = []; + theFact = wm.getFactHandle(theCollection); + } + return theFact; + } + } + /** + rhs function + */ + function queryRHS(match) { + var nFacts = match.factIds.length, + collection = match.facts[nFacts-1].object; + // purely for inspection... + } + // + // ParamPattern extends the from pattern, it is an alternate source (instead of working memory) of facts/arguments + if( params.arguments ) { + params.arguments.forEach(function(arg) { + var nCond = normailizeConstraint(arg); // arg as simple condition; e.g. 'msg' : String , ParamPattern extends from; + patterns.push( new ParamPattern(getParamType(nCond[0], scope),nCond[1], scope) ); + }); + } + else { // there is no parameter produce a fake, the InitialParam ( like the initial fact ) since params are query; trigger/blocker + patterns.push( new ParamPattern(Object, 'auto', scope) ); + } + // + // now that we have the parameters we can build the Query Function + // the query fn is kept in the flow like scope so it can be referenced in rule lhs + fnCollectionSrc = factoryCollectionSrc(); + queryFn = buildQuery(params.name, patterns, scope, fnCollectionSrc); + //rhsFn = buildRHS(params.name, patterns, scope); + rhsFn = queryRHS; + // + // parse the (non parameter) query conditions + params.conditions.forEach(function(condition) { + condition.scope = scope; + var pattern = parsePattern(condition)[0]; // pattern from query condition + setOrientedAliases.push(pattern.alias); // this alias is a set which the (bottom) collect node will accumulate + patterns.push( pattern ); + }); + // + // build from bottom;e.g. closest to terminal node; all bound vars are sets (except for params) which exist as conditions preceding query constraints + collectPattern = new CollectPattern(Array, 'list', 'true', {}, {scope: scope, pattern: 'true'}); + collectPattern.setOrientedAliases = setOrientedAliases; // used to distinguish condtions from argument(s) + collectPattern.fnCollectionSrc = fnCollectionSrc; + // + tmp = []; + patterns.push(collectPattern); + for (var i = 0; i < patterns.length; i++) { + if (!compPat) { // left, right + compPat = new CompositePattern(patterns[i], patterns[++i]); + } else { + compPat = new CompositePattern(compPat, patterns[i]); + } + tmp.push(compPat); + } + // + bottom = tmp[tmp.length-1]; + bottom.queryFn = queryFn; + bottom.rhsFn = rhsFn; + bottom.queryName = params.name; + return [bottom]; }) .containsAt('collect', 0, function(condition) { var scope = condition.scope, @@ -303,29 +425,31 @@ var parsePattern = extd } } 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]} - ) - ]; + return [ + new FromPattern( + getParamType(condition[0], condition.scope), + condition[1] || "m", + parseConstraint(condition[2] || "true", condition.scope), + condition[3] || {}, + parseConstraint(condition[4].from, condition.scope), + {scope: condition.scope, pattern: condition[2]} + ) + ]; } } else { return [ new ObjectPattern( getParamType(condition[0], condition.scope), condition[1] || "m", - parseConstraint(condition[2] || "true"), + parseConstraint(condition[2] || "true", condition.scope), condition[3] || {}, {scope: condition.scope, pattern: condition[2]} ) ]; - } + } }).switcher(); +// +exports.parsePattern = parsePattern; var Rule = declare({ @@ -345,9 +469,9 @@ 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); + ret = cb.call(flow, match.factHash, flow, ret.resolve); } else { ret = cb.call(flow, match.factHash, flow); } @@ -360,6 +484,30 @@ var Rule = declare({ }); exports.Rule = Rule; + +var Query = Rule.extend({ + instance: { + constructor: function (name, options, pattern) { + this._super([name, options, pattern,pattern.rhsFn]); + }, + + fire: function (flow, match) { + var ret = new Promise(), cb = this.cb; + try { + if (cb.length === 3) { + cb.call(flow, match, flow, ret.resolve); + } else { + ret = cb.call(flow, match, flow); + } + } catch (e) { + ret.errback(e); + } + return ret; + } + } +}); +exports.Query = Query; + // function _mergePatterns (patterns) { // @@ -373,21 +521,21 @@ function _mergePatterns (patterns) { } 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); - }); - } - }; + } + } + 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); + }); + } + }; } // // @@ -418,31 +566,41 @@ function createRule(name, options, conditions, cb) { // function patternFromCondition(condition, scope) { var patterns; - condition.scope = scope; + 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); - }); + 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]); + } + } + if(compPat.queryName) { + return new Query(name, options, compPat); + } + else { + return new Rule(name, options, compPat, cb); + } + }); } // function singlePattern(pattern) { return extd.map(patterns, function (cond) { - return new Rule(name, options, cond, cb); - }); - } + if(cond.queryName) { + return new Query(name, options, cond); + } + else { + return new Rule(name, options, cond, cb); + } + }); + } // if( isRules ) { for (var i = 0; i < l; i++) { @@ -463,8 +621,8 @@ function createRule(name, options, conditions, cb) { rules = compositePattern(merged); } } - return rules; - } + return rules; + } // if (isArray(options)) { cb = conditions; @@ -478,3 +636,31 @@ function createRule(name, options, conditions, cb) { } exports.createRule = createRule; + +function createQuery(name, options, conditions) { + var wrapper = {name: name, arguments: undefined, conditions: conditions}, args, rule, tmp; + if( extd.isArray(options) ) { + conditions = options; + } + options = options || {}; + if( extd.isArray(options.arguments) ) { // from the dsl parser; (type ident, type ident,...) + tmp = options.arguments.slice(0, options.arguments.length-1); + args = []; + tmp.forEach(function(arg) { + var type = arg[0], ident = arg[1]; + args.push([type[0], ident[0][0]]); + }); + } + else if( extd.isObject(options.arguments) ) { // args are specified inside of options along side scope as follows: {arguments: { ident : type, ...}} in order + args = []; + extd(options.arguments).forEach(function(type, ident) { + args.push([type, ident]); + }); + } + wrapper.arguments = args; + conditions = ['query', wrapper]; + return createRule(name, options, conditions)[0]; +} + +exports.createQuery = createQuery; + diff --git a/lib/workingMemory.js b/lib/workingMemory.js index 0e7df66..2f288b8 100644 --- a/lib/workingMemory.js +++ b/lib/workingMemory.js @@ -58,14 +58,16 @@ declare({ return ret; }, - getFactHandle: function (o) { + getFactHandle: function (o, useExisting) { var head = {next: this.facts.head}, ret; - while ((head = head.next)) { - var existingFact = head.data; - if (existingFact.equals(o)) { - return existingFact; - } - } + if(useExisting) { + while ((head = head.next)) { + var existingFact = head.data; + if (existingFact.equals(o)) { + return existingFact; + } + } + } if (!ret) { ret = new Fact(o); ret.recency = this.recency++; diff --git a/readme.md b/readme.md index f76c6f9..a002e32 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,6 @@ +# test + + [![Build Status](https://travis-ci.org/C2FO/nools.png)](https://travis-ci.org/C2FO/nools) [![browser support](https://ci.testling.com/C2FO/nools.png)](https://ci.testling.com/C2FO/nools) @@ -37,8 +40,8 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi * [Structure](#rule-structure) * [Salience](#rule-salience) * [Scope](#rule-scope) + * [Query](#query) * [Constraints](#constraints) - * [Custom](#custom-contraints) * [Not](#not-constraint) * [Or](#or-constraint) * [From](#from-constraint) @@ -342,6 +345,7 @@ session.getFacts(Number); //[1, 2]; session.getFacts(String); //["A", "B"]; ``` + ## Firing the rules @@ -1086,6 +1090,48 @@ function matches(str, regex) { var flow = nools.compile(__dirname + "/rules/provided-scope.nools", {scope: {doesMatch: matches}}); ``` + +### Query + +A query is a simple way to search the working memory for facts that match the stated conditions. Therefore, it contains only the structure of the LHS of a rule, so that you specify neither "when" nor "then". A query has an optional set of parameters, each of which is typed. Query names are global so be careful to avoid name collisions in a given flow. +When a query is processed by the engine a function is created that can be accessed from the session by name; e.g. session.getQuery(). Then that function may be called passing arguments if neccessary depending upon the query signature. +A query may be called from another rule or query using the from modifer since queries introduce a collection object that is not in working memory. Note in the examples below unlike a javascript function the type is part of the query signature. +```javascript +query MsgFilter( RegExp filter ) { + m : Message m.text =~ filter; +} + +rule TestQuery { + when { + list : Array from MsgFilter(/world/i); + } + then { + } +} +// you may call queries manually from a session + +var queryFn = session.getQuery('MsgFilter'); +var list = queryFn(new RegExp(/hello/i)); + +// to define a query from a Flow... + +flow.query('MsgFilter', options, [ + [Message, 'm', "m.text =~ filter"] +]); + +// note: query arguments must be defined this is done by providing an arguments hash in a similiar fashion to scope. +var options = { + arguments: { + filter: RegExp + } + ,scope: { + Message: Message + } + }; +``` +Queries are reactive meaning the contents of the returned Array changes as the contents of working memory change over time. Another point to be aware of is a query does not have a RHS(then) and is not dependent upon the session match state. Queries simply +retrieve facts from working memory according to the conditions defined by the query. + ### Constraints @@ -1192,6 +1238,7 @@ session.match().then(function(){ console.log("DONE"); }); ``` + #### Not Constraint @@ -1710,7 +1757,7 @@ define Message { rule Hello { when { - m : Message m.text =~ /^hello(\\sworld)?$/ + m : Message m.text =~ /^hello(\\s*world)?$/ } then { modify(m, function(){ @@ -1738,6 +1785,7 @@ rule Goodbye { session = flow.getSession(); //assert your different messages session.assert(new Message("goodbye")); + session.assert(new Message("hello")); session.assert(new Message("hello world")); session.match(); } diff --git a/test/flow/from.test.js b/test/flow/from.test.js index 355945a..2714abd 100644 --- a/test/flow/from.test.js +++ b/test/flow/from.test.js @@ -494,8 +494,8 @@ it.describe("from condition", function (it) { }); return session.match().then(function () { - assert.equal(called1, 0); - assert.equal(called2, 5); + assert.equal(called1, 1); + assert.equal(called2, 4); called1 = called2 = 0; session.modify(persons[0], function () { this.lastName = "yukon"; @@ -528,8 +528,8 @@ it.describe("from condition", function (it) { this.lastName = "y"; }); return session.match().then(function () { - assert.equal(called1, 0); - assert.equal(called2, 5); + assert.equal(called1, 1); + assert.equal(called2, 4); }); }); }); diff --git a/test/flow/query.nools b/test/flow/query.nools new file mode 100644 index 0000000..68a59e3 --- /dev/null +++ b/test/flow/query.nools @@ -0,0 +1,103 @@ +define Message { + text : "", + constructor : function (message) { + this.text = message; + this.length = this.text ? this.text.length : 0; + } +} + +define Person { + age: undefined + ,location: undefined + ,constructor: function(age, location) { + this.age = age; + this.location = location; + } +} +/** + * Query people over the age of 30 +*/ +query people_over_the_age_of_30( ) { + person : Person person.age > 30; +} + +/** + * Query people over the age of x +*/ +query people_over_the_age_of_x( Number ageLimit ) { + person : Person person.age > ageLimit; +} + +/** + * Simple query with multiple parameters... + */ +query MsgFilter(Number maxLen, RegExp filter) { + msg: Message msg.text.length < maxLen and msg.text =~ filter; +} + +/** + call query with facts as parameters +*/ +rule TestMessageQuery { + when { + $maxLen : Number; + $filter : RegExp; + list : Array from MsgFilter($maxLen, $filter); + } + then { + emit("query-message", list); + } +} +/** + call query with mixed literal, facts as parameters +*/ +rule TestMessageQueryMixed { + when { + filter : RegExp; + list : Array from MsgFilter(14, filter); + } + then { + emit("query-message-mixed", list); + } +} + + +/** + call query with no arguments +*/ +rule TestPersonQueryNoParams { + when { + list : Array from people_over_the_age_of_30(); + } + then { + emit("query-person-no-params", list); + } +} + +/** + call query with literal argument value + // age: Number; +*/ +rule TestPersonQueryLiteralParams { + when { + + list : Array from people_over_the_age_of_x(40); + + } + then { + emit("query-person-literal-param", list); + } +} + +/** + call query with no arguments +*/ +rule TestSameQueryCalledFromMultipleRules { + when { + age : Number; + list : Array from people_over_the_age_of_x(age); + } + then { + emit("rule-calling-same-query", list); + } +} diff --git a/test/flow/query.test.js b/test/flow/query.test.js new file mode 100644 index 0000000..0e1c3a5 --- /dev/null +++ b/test/flow/query.test.js @@ -0,0 +1,206 @@ +"use strict"; +var it = require("it"), + assert = require("assert"), + nools = require("../../"); + +it.describe("query", function (it) { + + var Message = function (message) { + this.text = message; + this.length = this.text ? this.text.length : 0; + }; + it.describe("basic test of query", function (it) { + // + var options = { + arguments: { + maxLen: Number + ,filter: RegExp + } + ,scope: { + Message: Message + } + }; + // + var rule1Called = 0, listLen = 0, msg; + var flow = nools.flow("query test 1",function (flow) { + // + flow.query('MsgFilter', options, [ + [Message, 'm', "m.text.length < maxLen && m.text =~ filter"] + ]); + // + flow.rule('test', [ + [Number, '$maxLen'], + [RegExp, '$filter'], + [Array, 'list', 'from MsgFilter($maxLen, $filter)'] + ],function(facts) { + rule1Called++; + listLen = facts.list.length; + msg = facts.list[0]; + }); + }); + // + it.should("rule using query called once and list avaialable in lhs", function () { + var session = flow.getSession() + ,maxLen = 14; + // + rule1Called = 0; + listLen = 0; + msg = null; + // + session.assert( new Message('hello world')); + session.assert( new Message('this message is too long')); + session.assert( new Message('bad message') ); + + + session.assert( maxLen ); + session.assert( new RegExp(/world/i)); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(msg.text, 'hello world'); + }); + }); + // + it.should("be called manually from session", function () { + rule1Called = 0; + // + var session = flow.getSession(); + session.assert( new Message('hello world')); + session.assert( new Message('this message is too long')); + session.assert( new Message('bad message') ); + // + // call the query manually + var list = session.getQuery('MsgFilter')(14, /hello/i); + assert.equal(list.length, 1); + assert.equal(list[0].text, 'hello world'); + }); + }); + it.describe("basic test of query using DSL", function (it) { + // + var flow = nools.compile(__dirname + '/query.nools', { name: 'TestQuery', define: {Message: Message} } ) + ,maxLen = 14 + ,filter = new RegExp(/world/i) + ,Message = flow.getDefined('Message') + ,Person = flow.getDefined('Person'); + // + it.should("call rule once and query list avaialable in lhs", function () { + var rule1Called = 0 + ,session = flow.getSession() + ,msg, listLen; + // + session.on("query-message", function (list) { + rule1Called++; + listLen = list.length; + msg = list[0]; + }); + // + session.assert( new Message('hello world') ); + session.assert( new Message('this message is too long') ); + session.assert( new Message('bad message') ); + // + session.assert( maxLen ); + session.assert( filter ); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(msg.text, 'hello world'); + }); + }); + // + it.should("call query with mixed arguments", function () { + var rule1Called = 0 + ,session = flow.getSession() + ,msg, listLen; + // + session.on("query-message-mixed", function (list) { + rule1Called++; + listLen = list.length; + msg = list[0]; + }); + // + var Message = flow.getDefined('Message'); + // + session.assert( new Message('hello world') ); + session.assert( new Message('this message is too long') ); + session.assert( new Message('bad message') ); + + session.assert( filter ); // the other fact is a literal in the nools file + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(msg.text, 'hello world'); + }); + }); + // + it.should("call query with no arguments", function () { + var rule1Called = 0 + ,session = flow.getSession() + ,person, listLen; + // + session.on("query-person-no-params", function (list) { + rule1Called++; + listLen = list.length; + person = list[0]; + }); + // + session.assert( new Person(45, 'office')); + session.assert( new Person(25, 'kitchen')); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(person.age, 45); + }); + }); + // + it.should("call query with literal argument", function () { + var rule1Called = 0 + ,session = flow.getSession() + ,person, listLen; + // + session.on("query-person-literal-param", function (list) { + rule1Called++; + listLen = list.length; + person = list[0]; + }); + // + session.assert( new Person(45, 'office')); + session.assert( new Person(25, 'kitchen')); + session.assert(40); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(person.age, 45); + }); + }); + // + it.should("call the same query from multiple rules", function () { + var rule1Called = 0 + ,session = flow.getSession() + ,age = 40 + ,person, listLen; + // + session.on("rule-calling-same-query", function (list) { + rule1Called++; + listLen = list.length; + person = list[0]; + }); + + // + session.assert( new Person(45, 'office')); + session.assert( new Person(25, 'kitchen')); + // + session.assert(age); + // + return session.match().then(function () { + assert.equal(rule1Called, 1); + assert.equal(listLen, 1); + assert.equal(person.age, 45); + }); + }); + }); +});