From a2429b3f355448eb91763828fb85df57fba76c18 Mon Sep 17 00:00:00 2001 From: Mark Bjerke Date: Tue, 15 Dec 2015 11:30:50 -0800 Subject: [PATCH] # This is a combination of 2 commits. # The first commit's message is: Implement no-loop rule attribute Fixes issue with using grave marks for dsl strings in test file: noloop.test.js Implements extends functionality in the dsl These files address #162 , the extends keyword, which allows you to reference other types in either external or other defines as a base type. Revert "Implements extends functionality in the dsl" This reverts commit 1259c3d5c4779e9e425dfb9db219c700bf12e92f. removed gitattributs/gitignore from branch # This is the 2nd commit message: Fixes issue with using grave marks for dsl strings in test file: noloop.test.js --- .gitignore | 4 --- lib/agenda.js | 10 +++++- lib/parser/nools/tokens.js | 18 ++++++++++ lib/rule.js | 1 + readme.md | 28 ++++++++++++++++ test/flow/noLoop.test.js | 67 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 5 deletions(-) delete mode 100644 .gitignore create mode 100644 test/flow/noLoop.test.js diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c9bb555..0000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -nools.iml -.idea -atlassian-ide-plugin.xml diff --git a/lib/agenda.js b/lib/agenda.js index 99fd57e..cd585bc 100644 --- a/lib/agenda.js +++ b/lib/agenda.js @@ -84,7 +84,7 @@ module.exports = declare(EventEmitter, { register: function (node) { var agendaGroup = node.rule.agendaGroup; - this.rules[node.name] = {tree: new AVLTree({compare: this.comparator}), factTable: new FactHash()}; + this.rules[node.name] = { tree: new AVLTree({ compare: this.comparator }), factTable: new FactHash(), noLoop: {} }; if (agendaGroup) { this.addAgendaGroup(agendaGroup); } @@ -162,6 +162,14 @@ module.exports = declare(EventEmitter, { insert: function (node, insert) { var rule = this.rules[node.name], nodeRule = node.rule, agendaGroup = nodeRule.agendaGroup; + if (nodeRule.noLoop) { + if (rule.noLoop[insert.hashCode]) { + return; + } + else { + rule.noLoop[insert.hashCode] = true; + } + } rule.tree.insert(insert); this.getAgendaGroup(agendaGroup).insert(insert); if (agendaGroup) { diff --git a/lib/parser/nools/tokens.js b/lib/parser/nools/tokens.js index 3ac7080..23391e2 100644 --- a/lib/parser/nools/tokens.js +++ b/lib/parser/nools/tokens.js @@ -94,6 +94,20 @@ var ruleTokens = { }; })(), + noLoop: (function () { + var noLoopRegexp = /^(noLoop|no-loop)\s*:\s*(-?true|false)\s*[,;]?/; + return function (src, context) { + if (noLoopRegexp.test(src)) { + var parts = src.match(noLoopRegexp); + // + context.options.noLoop = Boolean(parts[2]) + return src.replace(parts[0], ""); + } else { + throw new Error("invalid format"); + } + }; + })(), + "agenda-group": function () { return this.agendaGroup.apply(this, arguments); }, @@ -102,6 +116,10 @@ var ruleTokens = { return this.autoFocus.apply(this, arguments); }, + "no-loop": function () { + return this.noLoop.apply(this, arguments); + }, + priority: function () { return this.salience.apply(this, arguments); }, diff --git a/lib/rule.js b/lib/rule.js index fe44bc6..cc5a7dc 100644 --- a/lib/rule.js +++ b/lib/rule.js @@ -208,6 +208,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; diff --git a/readme.md b/readme.md index e8b2405..eb471cf 100644 --- a/readme.md +++ b/readme.md @@ -37,6 +37,7 @@ Or [download the source](https://raw.github.com/C2FO/nools/master/nools.js) ([mi * [Structure](#rule-structure) * [Salience](#rule-salience) * [Scope](#rule-scope) + * [no-loop](#rule-no-loop) * [Constraints](#constraints) * [Not](#not-constraint) * [Or](#or-constraint) @@ -1001,6 +1002,33 @@ flow1 }); ``` + +### No-Loop + +When a rule's action modifies a fact it may cause the rule to activate again, causing an infinite loop. Setting no-loop to true will skip the creation of another Activation for the rule with the current set of facts. + +```javascript +this.rule("Hello", {noLoop: true}, [Message, "m", "m.text like /hello/"], function (facts) { + var m = facts.m; + m.text = 'hello world'; + this.modify(m) +}); + +``` +Or using the DSL + +```javascript +rule Hello { + no-loop: true; + when { + m: Message m.name like /hello/; + } + then { modify(m, function() { + m.text = 'hello world' + }); +} +``` + diff --git a/test/flow/noLoop.test.js b/test/flow/noLoop.test.js new file mode 100644 index 0000000..bcebe58 --- /dev/null +++ b/test/flow/noLoop.test.js @@ -0,0 +1,67 @@ +"use strict"; +var it = require("it"), + assert = require("assert"), + nools = require("../../"); + +it.describe("no-loop", function (it) { + /*jshint indent*/ + function Message(name) { + this.name = name; + } + var cnt = 0; + + var flow1 = nools.flow("noLoop1", function () { + + this.rule("Hello2", { noLoop: true }, [Message, "m", "m.name =~ /Hello/"], function (facts) { + var m = facts.m; + m.name = 'Hello World'; + this.modify(m); + }); + }), + + flow2 = nools.flow("noLoop2", function () { + + this.rule("Hello1", [Message, "m", "m.name =~ /Hello/"], function (facts) { + var m = facts.m; + if (cnt++ < 2) { + m.name = 'Hello World'; + this.modify(m); + } + }); + }); + + var noolsSource = "rule 'Hello3' { no-loop: true; when {m: Message m.name =~/Hello/;}then {modify(m, function () { this.name = 'Hello World'; });}}"; + + var flow3 = nools.compile(noolsSource, { + name: 'testDsl' + ,define: { + Message: Message + } + }); + + it.should("not loop with option on and loop otherwise", function () { + var fired1 = [], fired2 = [], fired3 = []; + var session1 = flow1.getSession(new Message("Hello")).on("fire", function (name) { + fired1.push(name); + }), + session2 = flow2.getSession(new Message("Hello")).on("fire", function (name) { + fired2.push(name); + }), + session3 = flow3.getSession(new Message("Hello")).on("fire", function (name) { + fired3.push(name); + }); + return session1.match() + .then(function () { + return session2.match().then(function () { + return session3.match().then(function () { + }) + }) + }) + .then(function () { + assert.deepEqual(fired1, ["Hello2"]); + assert.deepEqual(fired2, ["Hello1", "Hello1", "Hello1"]); + assert.deepEqual(fired3, ["Hello3"]); + }); + }); + +});