diff --git a/README.md b/README.md index 7ba46e7e..6cbd71ec 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ -# durable_rules +# durable_rules #### for real time analytics (a Ruby, Python and Node.js Rules Engine) [![Build Status](https://travis-ci.org/jruizgit/rules.svg?branch=master)](https://travis-ci.org/jruizgit/rules) [![Gem Version](https://badge.fury.io/rb/durable_rules.svg)](https://badge.fury.io/rb/durable_rules) [![npm version](https://badge.fury.io/js/durable.svg)](https://badge.fury.io/js/durable) -[![PyPI version](https://badge.fury.io/py/durable_rules.svg)](https://badge.fury.io/py/durable_rules) +[![PyPI version](https://badge.fury.io/py/durable_rules.svg)](https://badge.fury.io/py/durable_rules) durable_rules is a polyglot micro-framework for real-time, consistent and scalable coordination of events. With durable_rules you can track and analyze information about things that happen (events) by combining data from multiple sources to infer more complicated circumstances. -A full forward chaining implementation (Rete) is used to evaluate facts and events in real time. A simple meta-linguistic abstraction lets you define simple and complex rulesets as well as control flow structures such as flowcharts, statecharts, nested statecharts and time driven flows. +A full forward chaining implementation (Rete) is used to evaluate facts and events in real time. A simple meta-linguistic abstraction lets you define simple and complex rulesets as well as control flow structures such as flowcharts, statecharts, nested statecharts and time driven flows. -The durable_rules core engine is implemented in C, which enables fast rule evaluation as well as muti-language support. +The durable_rules core engine is implemented in C, which enables fast rule evaluation as well as multi-language support. -durable_rules can be scaled out by offloading state to a data store out of process such as Redis. State offloading is extensible, so you can integrate the data store of your choice. +durable_rules can be scaled out by offloading state to a data store out of process such as Redis. State offloading is extensible, so you can integrate the data store of your choice. *In durable_rules V2, less is more: The Rete tree is fully evaluated in C. Thus, the framework is 5x to 10x faster (depending on the scenario) and does not require Redis. The programming model for posting events, asserting and retracting facts is synchronous and does not prescribe any web framework.* -## Getting Started +## Getting Started -Using your scripting language of choice, simply describe the event to match (antecedent) and the action to take (consequent). +Using your scripting language of choice, simply describe the event to match (antecedent) and the action to take (consequent). ### Node.js @@ -35,7 +35,8 @@ d.ruleset('test', function() { }); d.post('test', {subject: 'World'}); -``` +``` + ### Python To install the framework do: `pip install durable_rules` @@ -51,7 +52,8 @@ with ruleset('test'): print ('Hello {0}'.format(c.m.subject)) post('test', { 'subject': 'World' }) -``` +``` + ### Ruby To install the framework do: `gem install durable_rules` @@ -68,29 +70,31 @@ Durable.ruleset :test do end Durable.post :test, { :subject => "World" } -``` -## Forward Inference +``` + +## Forward Inference -durable_rules super-power is the foward-chaining evaluation of rules. In other words, the repeated application of logical [modus ponens](https://en.wikipedia.org/wiki/Modus_ponens) to a set of facts or observed events to derive a conclusion. The example below shows a set of rules applied to a small knowledge base (set of facts). +durable_rules super-power is the forward-chaining evaluation of rules. In other words, the repeated application of logical [modus ponens](https://en.wikipedia.org/wiki/Modus_ponens) to a set of facts or observed events to derive a conclusion. The example below shows a set of rules applied to a small knowledge base (set of facts). ### Node.js + ```javascript var d = require('durable'); d.ruleset('animal', function() { whenAll: { - first = m.predicate == 'eats' && m.object == 'flies' + first = m.predicate == 'eats' && m.object == 'flies' m.predicate == 'lives' && m.object == 'water' && m.subject == first.subject } run: assert({ subject: first.subject, predicate: 'is', object: 'frog' }) whenAll: { - first = m.predicate == 'eats' && m.object == 'flies' + first = m.predicate == 'eats' && m.object == 'flies' m.predicate == 'lives' && m.object == 'land' && m.subject == first.subject } run: assert({ subject: first.subject, predicate: 'is', object: 'chameleon' }) - whenAll: m.predicate == 'eats' && m.object == 'worms' + whenAll: m.predicate == 'eats' && m.object == 'worms' run: assert({ subject: m.subject, predicate: 'is', object: 'bird' }) whenAll: m.predicate == 'is' && m.object == 'frog' @@ -99,7 +103,7 @@ d.ruleset('animal', function() { whenAll: m.predicate == 'is' && m.object == 'chameleon' run: assert({ subject: m.subject, predicate: 'is', object: 'green' }) - whenAll: m.predicate == 'is' && m.object == 'bird' + whenAll: m.predicate == 'is' && m.object == 'bird' run: assert({ subject: m.subject, predicate: 'is', object: 'black' }) whenAll: +m.subject @@ -112,7 +116,9 @@ d.assert('animal', { subject: 'Greedy', predicate: 'eats', object: 'flies'}); d.assert('animal', { subject: 'Greedy', predicate: 'lives', object: 'land'}); d.assert('animal', { subject: 'Tweety', predicate: 'eats', object: 'worms'}); ``` + ### Python + ```python from durable.lang import * @@ -152,19 +158,21 @@ assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'eats', 'object': 'fli assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'lives', 'object': 'water' }) assert_fact('animal', { 'subject': 'Greedy', 'predicate': 'eats', 'object': 'flies' }) assert_fact('animal', { 'subject': 'Greedy', 'predicate': 'lives', 'object': 'land' }) -assert_fact('animal', { 'subject': 'Tweety', 'predicate': 'eats', 'object': 'worms' }) +assert_fact('animal', { 'subject': 'Tweety', 'predicate': 'eats', 'object': 'worms' }) ``` + ### Ruby + ```ruby require "durable" Durable.ruleset :animal do - when_all c.first = (m.predicate == "eats") & (m.object == "flies"), + when_all c.first = (m.predicate == "eats") & (m.object == "flies"), (m.predicate == "lives") & (m.object == "water") & (m.subject == first.subject) do assert :subject => first.subject, :predicate => "is", :object => "frog" end - when_all c.first = (m.predicate == "eats") & (m.object == "flies"), + when_all c.first = (m.predicate == "eats") & (m.object == "flies"), (m.predicate == "lives") & (m.object == "land") & (m.subject == first.subject) do assert :subject => first.subject, :predicate => "is", :object => "chameleon" end @@ -172,11 +180,11 @@ Durable.ruleset :animal do when_all (m.predicate == "eats") & (m.object == "worms") do assert :subject => m.subject, :predicate => "is", :object => "bird" end - + when_all (m.predicate == "is") & (m.object == "frog") do assert :subject => m.subject, :predicate => "is", :object => "green" end - + when_all (m.predicate == "is") & (m.object == "chameleon") do assert :subject => m.subject, :predicate => "is", :object => "green" end @@ -184,7 +192,7 @@ Durable.ruleset :animal do when_all (m.predicate == "is") & (m.object == "bird") do assert :subject => m.subject, :predicate => "is", :object => "black" end - + when_all +m.subject do puts "fact: #{m.subject} #{m.predicate} #{m.object}" end @@ -196,21 +204,23 @@ Durable.assert :animal1, { :subject => "Greedy", :predicate => "eats", :object = Durable.assert :animal1, { :subject => "Greedy", :predicate => "lives", :object => "land" } Durable.assert :animal1, { :subject => "Tweety", :predicate => "eats", :object => "worms" } ``` + ## Pattern Matching durable_rules provides string pattern matching. Expressions are compiled down to a DFA, guaranteeing linear execution time in the order of single digit nano seconds per character (note: backtracking expressions are not supported). ### Node.js + ```javascript var d = require('durable'); d.ruleset('test', function() { whenAll: m.subject.matches('3[47][0-9]{13}') run: console.log('Amex detected in ' + m.subject) - + whenAll: m.subject.matches('4[0-9]{12}([0-9]{3})?') run: console.log('Visa detected in ' + m.subject) - + whenAll: m.subject.matches('(5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|2720)[0-9]{12}') run: console.log('Mastercard detected in ' + m.subject) }); @@ -219,7 +229,9 @@ d.assert('test', { subject: '375678956789765' }); d.assert('test', { subject: '4345634566789888' }); d.assert('test', { subject: '2228345634567898' }); ``` + ### Python + ```python from durable.lang import * @@ -240,18 +252,20 @@ assert_fact('test', { 'subject': '375678956789765' }) assert_fact('test', { 'subject': '4345634566789888' }) assert_fact('test', { 'subject': '2228345634567898' }) ``` + ### Ruby + ```ruby require "durable" Durable.ruleset :test do when_all m.subject.matches('3[47][0-9]{13}') do puts "Amex detected in #{m.subject}" end - + when_all m.subject.matches('4[0-9]{12}([0-9]{3})?') do puts "Visa detected in #{m.subject}" end - + when_all m.subject.matches('(5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|2720)[0-9]{12}') do puts "Mastercard detected in #{m.subject}" end @@ -261,42 +275,43 @@ Durable.assert :test, { :subject => "375678956789765" } Durable.assert :test, { :subject => "4345634566789888" } Durable.assert :test, { :subject => "2228345634567898" } ``` -## Business Rules and Miss Manners -durable_rules can also be used to solve traditional Production Business Rules problems. This example is an industry benchmark. Miss Manners has decided to throw a party. She wants to seat her guests such that adjacent people are of opposite sex and share at least one hobby. +## Business Rules and Miss Manners + +durable_rules can also be used to solve traditional Production Business Rules problems. This example is an industry benchmark. Miss Manners has decided to throw a party. She wants to seat her guests such that adjacent people are of opposite sex and share at least one hobby. -Note how the benchmark flow structure is defined using a statechart to improve code readability without sacrificing performance nor altering the combinatorics required by the benchmark. For 128 guests, 438 facts, the execution time is 450 ms in node.js and 600 ms in Python and Ruby. +Note how the benchmark flow structure is defined using a statechart to improve code readability without sacrificing performance nor altering the combinatorics required by the benchmark. For 128 guests, 438 facts, the execution time is 450 ms in node.js and 600 ms in Python and Ruby. -
+
-_IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive_ +_IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive_ -* [Ruby](https://github.com/jruizgit/rules/blob/testrb/manners.rb) -* [Python](https://github.com/jruizgit/rules/blob/testpy/manners.py) +* [Ruby](https://github.com/jruizgit/rules/blob/testrb/manners.rb) +* [Python](https://github.com/jruizgit/rules/blob/testpy/manners.py) * [Node.js](https://github.com/jruizgit/rules/blob/testjs/manners.js) ## Image recognition and Waltzdb -Waltzdb is a constraint propagation problem for image recognition: given a set of lines in a 2D space, the system needs to interpret the 3D depth of the image. The first part of the algorithm consists of identifying four types of junctions, then labeling the junctions following Huffman-Clowes notation. Pairs of adjacent junctions constraint each other’s edge labeling. So, after choosing the labeling for an initial junction, the second part of the algorithm iterates through the graph, propagating the labeling constraints by removing inconsistent labels. +Waltzdb is a constraint propagation problem for image recognition: given a set of lines in a 2D space, the system needs to interpret the 3D depth of the image. The first part of the algorithm consists of identifying four types of junctions, then labeling the junctions following Huffman-Clowes notation. Pairs of adjacent junctions constraint each other’s edge labeling. So, after choosing the labeling for an initial junction, the second part of the algorithm iterates through the graph, propagating the labeling constraints by removing inconsistent labels. In this case too, the benchmark flow structure is defined using a statechart to improve code readability. The benchmark requirements are not altered. The execution time, for the case of 4 regions, is 430 ms in node.js, 654 ms in Python and 552 ms in Ruby. -
+
-_IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive_ +_IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive_ -* [Node.js](https://github.com/jruizgit/rules/blob/master/testjs/waltzdb.js) -* [Ruby](https://github.com/jruizgit/rules/blob/master/testrb/waltzdb.rb) -* [Python](https://github.com/jruizgit/rules/blob/master/testpy/waltzdb.py) +* [Node.js](https://github.com/jruizgit/rules/blob/master/testjs/waltzdb.js) +* [Ruby](https://github.com/jruizgit/rules/blob/master/testrb/waltzdb.rb) +* [Python](https://github.com/jruizgit/rules/blob/master/testpy/waltzdb.py) +## To Learn More -## To Learn More -Reference Manual: -### [Ruby](https://github.com/jruizgit/rules/blob/master/docs/rb/reference.md) +Reference Manual: -### [Python](https://github.com/jruizgit/rules/blob/master/docs/py/reference.md) +### [Ruby](https://github.com/jruizgit/rules/blob/master/docs/rb/reference.md) -### [Node.js](https://github.com/jruizgit/rules/blob/master/docs/js/reference.md) +### [Python](https://github.com/jruizgit/rules/blob/master/docs/py/reference.md) -### [JSON](https://github.com/jruizgit/rules/blob/master/docs/json/reference.md) +### [Node.js](https://github.com/jruizgit/rules/blob/master/docs/js/reference.md) +### [JSON](https://github.com/jruizgit/rules/blob/master/docs/json/reference.md) \ No newline at end of file diff --git a/docs/js/reference.md b/docs/js/reference.md index cd695a46..f0a9d35a 100644 --- a/docs/js/reference.md +++ b/docs/js/reference.md @@ -1,6 +1,8 @@ Reference Manual ===== + ## Table of contents + * [Setup](reference.md#setup) * [Basics](reference.md#basics) * [Rules](reference.md#rules) @@ -19,12 +21,12 @@ Reference Manual * [Nested Objects](reference.md#nested-objects) * [Arrays](reference.md#arrays) * [Facts and Events as rvalues](reference.md#facts-and-events-as-rvalues) -* [Consequents](reference.md#consequents) +* [Consequents](reference.md#consequents) * [Conflict Resolution](reference.md#conflict-resolution) * [Action Batches](reference.md#action-batches) * [Async Actions](reference.md#async-actions) * [Unhandled Exceptions](reference.md#unhandled-exceptions) -* [Flow Structures](reference.md#flow-structures) +* [Flow Structures](reference.md#flow-structures) * [Statechart](reference.md#statechart) * [Nested States](reference.md#nested-states) * [Flowchart](reference.md#flowchart) @@ -33,16 +35,16 @@ Reference Manual ## Setup ### First App -Let's write a simple rule: +Let's write a simple rule: -1. Start a terminal -2. Create a directory for your app: `mkdir firstapp` `cd firstapp` -3. In the new directory `npm install durable` (this will download durable.js and its dependencies) -4. In that same directory create a test.js file using your favorite editor +1. Start a terminal +2. Create a directory for your app: `mkdir firstapp` `cd firstapp` +3. In the new directory `npm install durable` (this will download durable.js and its dependencies) +4. In that same directory create a test.js file using your favorite editor 5. Copy/Paste and save the following code: ```javascript var d = require('durable'); - + d.ruleset('a0', function() { whenAll: m.amount < 100 run: console.log('a0 approved') @@ -50,20 +52,20 @@ Let's write a simple rule: d.post('a0', { amount: 10 }); ``` -7. In the terminal type `node test.js` -8. You should see the message: `a0 approved` +7. In the terminal type `node test.js` +8. You should see the message: `a0 approved` -Note: If you are running in Windows, you will need VS2013 express edition and Python 2.7 for the package to build during npm install. Make sure both the VS build tools and the python directory are in your path. +Note: If you are running in Windows, you will need VS2013 express edition and Python 2.7 for the package to build during npm install. Make sure both the VS build tools and the python directory are in your path. -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Basics ### Rules A rule is the basic building block of the framework. The rule antecendent defines the conditions that need to be satisfied to execute the rule consequent (action). By convention `m` represents the data to be evaluated by a given rule. * `whenAll` and `whenAny` label the antecendent definition of a rule -* `run` and `runAsync` label the consequent definition of a rule - +* `run` and `runAsync` label the consequent definition of a rule + ```javascript var d = require('durable'); @@ -84,17 +86,17 @@ var d = require('durable'); d.ruleset('animal', function() { // will be triggered by 'Kermit eats flies' - whenAll: m.predicate == 'eats' && m.object == 'flies' + whenAll: m.predicate == 'eats' && m.object == 'flies' run: assert({ subject: m.subject, predicate: 'is', object: 'frog' }) - whenAll: m.predicate == 'eats' && m.object == 'worms' + whenAll: m.predicate == 'eats' && m.object == 'worms' run: assert({ subject: m.subject, predicate: 'is', object: 'bird' }) // will be chained after asserting 'Kermit is frog' - whenAll: m.predicate == 'is' && m.object == 'frog' + whenAll: m.predicate == 'is' && m.object == 'frog' run: assert({ subject: m.subject, predicate: 'is', object: 'green'}) - whenAll: m.predicate == 'is' && m.object == 'bird' + whenAll: m.predicate == 'is' && m.object == 'bird' run: assert({ subject: m.subject, predicate: 'is', object: 'black'}) whenAll: +m.subject @@ -104,10 +106,10 @@ d.ruleset('animal', function() { d.assert('animal', { subject: 'Kermit', predicate: 'eats', object: 'flies' }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Events -Events can be posted to and evaluated by rules. An event is an ephemeral fact, that is, a fact retracted right before executing a consequent. Thus, events can only be observed once. Events are stored until they are observed. +Events can be posted to and evaluated by rules. An event is an ephemeral fact, that is, a fact retracted right before executing a consequent. Thus, events can only be observed once. Events are stored until they are observed. ```javascript var d = require('durable'); @@ -126,20 +128,20 @@ d.post('risk', { t: 'purchase', location: 'US' }); d.post('risk', { t: 'purchase', location: 'CA' }); ``` -**Note from the autor:** +**Note from the author:** -*Using facts in the example above will produce the following output:* +*Using facts in the example above will produce the following output:* -`Fraud detected -> US, CA` -`Fraud detected -> CA, US` +`Fraud detected -> US, CA` +`Fraud detected -> CA, US` -*The reason is because both facts satisfy the first condition m.t == 'purchase' and each fact satisfies the second condition m.location != c.first.location in relation to the facts which satisfied the first.* +*The reason is because both facts satisfy the first condition m.t == 'purchase' and each fact satisfies the second condition m.location != c.first.location in relation to the facts which satisfied the first.* -*Events are ephemeral facts, they are retracted before they are dispatched. When using post in the example above, by the time the second pair is calculated the events have already been retracted.* +*Events are ephemeral facts, they are retracted before they are dispatched. When using post in the example above, by the time the second pair is calculated the events have already been retracted.* -*Retracting events before dispatch reduces the number of combinations to be calculated during action execution.* +*Retracting events before dispatch reduces the number of combinations to be calculated during action execution.* -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### State Context state is available when a consequent is executed. The same context state is passed across rule execution. Context state is stored until it is deleted. Context state changes can be evaluated by rules. By convention `s` represents the state to be evaluated by a rule. @@ -174,10 +176,10 @@ d.ruleset('flow', function() { d.updateState('flow', { status: 'start' }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Identity -Facts and events with the same property names and values are considered equal. +Facts and events with the same property names and values are considered equal. ```javascript var d = require('durable'); @@ -188,7 +190,7 @@ d.ruleset('bookstore', function() { run: console.log('bookstore reference ' + m.reference + ' status ' + m.status) whenAll: +m.name - run: { + run: { console.log('bookstore added: ' + m.name); } @@ -197,7 +199,7 @@ d.ruleset('bookstore', function() { run: console.log('bookstore no books'); }); -// will not throw because the fact assert was successful +// will not throw because the fact assert was successful d.assert('bookstore', { name: 'The new book', seller: 'bookstore', @@ -206,7 +208,7 @@ d.assert('bookstore', { }); -// will throw MessageObservedError because the fact has already been asserted +// will throw MessageObservedError because the fact has already been asserted try { d.assert('bookstore', { reference: '75323', @@ -215,7 +217,7 @@ try { seller: 'bookstore' }); } catch (err) { - console.log('bookstore: ' + err.message); + console.log('bookstore: ' + err.message); } // will not throw because a new event is being posted @@ -246,16 +248,16 @@ When asserting a fact, retracting a fact, posting an event or updating state con * MessageObservedError: The fact has already been asserted or the event has already been posted. * MessageNotHandledError: The event or fact was not captured because it did not match any rule. -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Antecendents ### Simple Filter -A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. By convention events or facts are represented with the `m` name. Context state are represented with the `s` name. +A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. By convention events or facts are represented with the `m` name. Context state are represented with the `s` name. -Logical operators: -* Unary: ~ (does not exist), + (exists) -* Logical operators: &&, || -* Relational operators: < , >, <=, >=, ==, != +Logical operators: +* Unary: ~ (does not exist), + (exists) +* Logical operators: &&, || +* Relational operators: < , >, <=, >=, ==, != ```javascript var d = require('durable'); @@ -266,39 +268,39 @@ d.ruleset('expense', function() { }); d.post('expense', { subject: 'approve' }); -``` +``` [top](reference.md#table-of-contents) ### Pattern Matching -durable_rules implements a simple pattern matching dialect. It uses % to escape, which vastly simplifies writing expressions. Expressions are compiled down into a deterministic state machine, thus backtracking is not supported. Event processing is O(n) guaranteed (n being the size of the event). - -**Repetition** -\+ 1 or more repetitions -\* 0 or more repetitions -? optional (0 or 1 occurrence) - -**Special** -() group -| disjunct -[] range -{} repeat - -**Character classes** -. all characters -%a letters -%c control characters -%d digits -%l lower case letters -%p punctuation characters -%s space characters -%u upper case letters -%w alphanumeric characters -%x hexadecimal digits +durable_rules implements a simple pattern matching dialect. It uses % to escape, which vastly simplifies writing expressions. Expressions are compiled down into a deterministic state machine, thus backtracking is not supported. Event processing is O(n) guaranteed (n being the size of the event). + +**Repetition** +\+ 1 or more repetitions +\* 0 or more repetitions +? optional (0 or 1 occurrence) + +**Special** +() group +| disjunct +[] range +{} repeat + +**Character classes** +. all characters +%a letters +%c control characters +%d digits +%l lower case letters +%p punctuation characters +%s space characters +%u upper case letters +%w alphanumeric characters +%x hexadecimal digits ```javascript var d = require('durable'); d.ruleset('match', function() { - whenAll: m.url.matches('(https?://)?([%da-z.-]+)%.[a-z]{2,6}(/[%w_.-]+/?)*') + whenAll: m.url.matches('(https?://)?([%da-z.-]+)%.[a-z]{2,6}(/[%w_.-]+/?)*') run: console.log('match url ' + m.url) }); @@ -307,9 +309,9 @@ d.post('match', {url: 'http://github.com/jruizgit/rul!es'}, function(err, state) d.post('match', {url: 'https://github.com/jruizgit/rules/reference.md'}); d.post('match', {url: '//rules'}, function(err, state){ console.log('match: ' + err.message) }); d.post('match', {url: 'https://github.c/jruizgit/rules'}, function(err, state){ console.log('match: ' + err.message) }); -``` -[top](reference.md#table-of-contents) -### String Operations +``` +[top](reference.md#table-of-contents) +### String Operations The pattern matching dialect can be used for common string operations. The `imatches` function enables case insensitive pattern matching. ```javascript @@ -331,15 +333,15 @@ d.assert('strings', { subject: 'world hello' }); d.assert('strings', { subject: 'hello hi' }); d.assert('strings', { subject: 'has Hello string' }); d.assert('strings', { subject: 'does not match' }, function(err, state){ console.log('strings: ' + err.message) }); -``` +``` [top](reference.md#table-of-contents) ### Correlated Sequence -Rules can be used to efficiently evaluate sequences of correlated events or facts. The fraud detection rule in the example below shows a pattern of three events: the second event amount being more than 200% the first event amount and the third event amount greater than the average of the other two. +Rules can be used to efficiently evaluate sequences of correlated events or facts. The fraud detection rule in the example below shows a pattern of three events: the second event amount being more than 200% the first event amount and the third event amount greater than the average of the other two. -By default a correlated sequences capture distinct messages. In the example below the second event satisfies the second and the third condition, however the event will be captured only for the second condition. Use the `distinct` attribute to disable distinct event or fact correlation. +By default a correlated sequences capture distinct messages. In the example below the second event satisfies the second and the third condition, however the event will be captured only for the second condition. Use the `distinct` attribute to disable distinct event or fact correlation. -The `whenAll` label expresses a sequence of events or facts. The assignment operator is used to name events or facts, which can be referenced in subsequent expressions. When referencing events or facts, all properties are available. Complex patterns can be expressed using arithmetic operators. +The `whenAll` label expresses a sequence of events or facts. The assignment operator is used to name events or facts, which can be referenced in subsequent expressions. When referencing events or facts, all properties are available. Complex patterns can be expressed using arithmetic operators. Arithmetic operators: +, -, *, / ```javascript @@ -363,14 +365,14 @@ d.post('risk', { amount: 50 }); d.post('risk', { amount: 200 }); d.post('risk', { amount: 251 }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Choice of Sequences durable_rules allows expressing and efficiently evaluating richer events sequences. In the example below any of the two event\fact sequences will trigger an action. -The following two labels can be used and combined to define richer event sequences: -* whenAll: a set of event or fact patterns. All of them are required to match to trigger an action. -* whenAny: a set of event or fact patterns. Any one match will trigger an action. +The following two labels can be used and combined to define richer event sequences: +* whenAll: a set of event or fact patterns. All of them are required to match to trigger an action. +* whenAny: a set of event or fact patterns. Any one match will trigger an action. ```javascript var d = require('durable'); @@ -381,16 +383,16 @@ d.ruleset('expense', function() { first = m.subject == 'approve' second = m.amount == 1000 } - whenAll: { + whenAll: { third = m.subject == 'jumbo' fourth = m.amount == 10000 } } run: { if (first) { - console.log('Approved ' + first.subject + ' ' + second.amount); + console.log('Approved ' + first.subject + ' ' + second.amount); } else { - console.log('Approved ' + third.subject + ' ' + fourth.amount); + console.log('Approved ' + third.subject + ' ' + fourth.amount); } } }); @@ -400,7 +402,7 @@ d.post('expense', { amount: 1000 }); d.post('expense', { subject: 'jumbo' }); d.post('expense', { amount: 10000 }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Lack of Information In some cases lack of information is meaningful. The `none` function can be used in rules with correlated sequences to evaluate the lack of information. @@ -436,10 +438,10 @@ d.assert('risk', {sid: 2, t: 'chargeback'}); d.assert('risk', {sid: 2, t: 'balance'}); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested Objects -Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. +Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. ```javascript var d = require('durable'); @@ -457,19 +459,19 @@ d.ruleset('expense4', function() { }); // one level of nesting -d.post('expense4', {t: 'bill', invoice: {amount: 100}}); +d.post('expense4', {t: 'bill', invoice: {amount: 100}}); // two levels of nesting -d.post('expense4', {t: 'account', payment: {invoice: {amount: 100}}}); +d.post('expense4', {t: 'account', payment: {invoice: {amount: 100}}}); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Arrays ```javascript var d = require('durable'); d.ruleset('risk', function() { - + // matching primitive array whenAll: { m.payments.allItems(item > 100) @@ -481,7 +483,7 @@ d.ruleset('risk', function() { m.payments.allItems(item.amount < 250 || item.amount >= 300) } run: console.log('fraud 2 detected ' + JSON.stringify(m.payments)) - + // pattern matching string array whenAll: { m.cards.anyItem(item.matches('three.*')) @@ -498,24 +500,24 @@ d.ruleset('risk', function() { d.post('risk', { payments: [ 150, 350, 450 ] }); d.post('risk', { payments: [ { amount: 200 }, { amount: 300 }, { amount: 400 } ] }); d.post('risk', { cards: [ 'one card', 'two cards', 'three cards' ] }); -d.post('risk', { payments: [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}); +d.post('risk', { payments: [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Facts and Events as rvalues -Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. +Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. ```javascript var d = require('durable'); d.ruleset('risk', function() { - + // compares properties in the same event whenAll: { m.debit > 2 * m.credit } run: console.log('debit ' + m.debit + ' more than twice the credit ' + m.credit) - + // compares two correlated events whenAll: { first = m.amount > 100 @@ -533,7 +535,7 @@ d.post('risk', {amount: 200}); d.post('risk', {amount: 500}); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Consequents ### Conflict Resolution @@ -545,13 +547,13 @@ var d = require('durable'); d.ruleset('attributes', function() { whenAll: m.amount < 300 - pri: 3 + pri: 3 run: console.log('attributes P3 ->' + m.amount); - + whenAll: m.amount < 200 pri: 2 - run: console.log('attributes P2 ->' + m.amount); - + run: console.log('attributes P2 ->' + m.amount); + whenAll: m.amount < 100 pri: 1 run: console.log('attributes P1 ->' + m.amount); @@ -561,12 +563,12 @@ d.assert('attributes', { amount: 50 }); d.assert('attributes', { amount: 150 }); d.assert('attributes', { amount: 250 }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Action Batches When a high number of events or facts satisfy a consequent, the consequent results can be delivered in batches. -* count: defines the exact number of times the rule needs to be satisfied before scheduling the action. -* cap: defines the maximum number of times the rule needs to be satisfied before scheduling the action. +* count: defines the exact number of times the rule needs to be satisfied before scheduling the action. +* cap: defines the maximum number of times the rule needs to be satisfied before scheduling the action. This example batches exactly three approvals and caps the number of rejects to two: ```javascript @@ -595,9 +597,9 @@ d.postBatch('expense', [{ amount: 10 }, { amount: 400 }]); d.assert('expense', { review: true }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) -### Async Actions +### Async Actions The consequent action can be asynchronous. When the action is finished, the `complete` function has to be called. By default an action is considered abandoned after 5 seconds. This value can be changed by returning a different number in the action function or extended by calling `renewActionLease`. ```javascript @@ -610,7 +612,7 @@ d.ruleset('flow', function() { setTimeout(function() { s.state = 'second'; console.log('first completed'); - + // completes the async action after 3 seconds complete(); }, 3000); @@ -621,12 +623,12 @@ d.ruleset('flow', function() { setTimeout(function() { s.state = 'third'; console.log('second completed'); - + // completes the async action after 6 seconds // use the first argument to signal an error complete('error detected'); }, 6000); - + // overrides the 5 second default abandon timeout return 10; } @@ -634,8 +636,8 @@ d.ruleset('flow', function() { d.updateState('flow', { state: 'first' }); ``` -[top](reference.md#table-of-contents) -### Unhandled Exceptions +[top](reference.md#table-of-contents) +### Unhandled Exceptions When exceptions are not handled by actions, they are stored in the context state. This enables writing exception handling rules. ```javascript @@ -649,28 +651,28 @@ d.ruleset('flow', function() { whenAll: +s.exception run: { console.log(s.exception); - delete(s.exception); + delete(s.exception); } }); d.post('flow', { action: 'start' }); ``` -[top](reference.md#table-of-contents) - +[top](reference.md#table-of-contents) + ## Flow Structures ### Statechart -Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. - -Statechart rules: -* A statechart can have one or more states. -* A statechart requires an initial state. -* An initial state is defined as a vertex without incoming edges. -* A state can have zero or more triggers. -* A state can have zero or more states (see [nested states](reference.md#nested-states)). -* A trigger has a destination state. -* A trigger can have a rule (absence means state enter). -* A trigger can have an action. +Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. + +Statechart rules: +* A statechart can have one or more states. +* A statechart requires an initial state. +* An initial state is defined as a vertex without incoming edges. +* A state can have zero or more triggers. +* A state can have zero or more states (see [nested states](reference.md#nested-states)). +* A trigger has a destination state. +* A trigger can have a rule (absence means state enter). +* A trigger can have an action. ```javascript var d = require('durable'); @@ -699,7 +701,7 @@ d.statechart('expense', function() { whenAll: m.subject == 'denied' run: console.log('Expense denied') } - + // 'denied' and 'approved' are final states denied: {} approved: {} @@ -715,7 +717,7 @@ d.post('expense', { sid: 1, subject: 'denied' }); // events directed to statechart instance with id '2' d.post('expense', { sid: 2, subject: 'approve', amount: 10000 }); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested States Nested states allow for writing compact statecharts. If a context is in the nested state, it also (implicitly) is in the surrounding state. The statechart will attempt to handle any event in the context of the sub-state. If the sub-state does not handle an event, the event is automatically handled at the context of the super-state. @@ -737,7 +739,7 @@ d.statechart('worker', function() { whenAll: m.subject == 'continue' run: console.log('continue processing') } - + // the super-state trigger will be evaluated for all sub-state triggers to: 'canceled' whenAll: m.subject == 'cancel' @@ -749,7 +751,7 @@ d.statechart('worker', function() { // will move the statechart to the 'work.process' sub-state d.post('worker', { subject: 'enter' }); - + // will keep the statechart to the 'work.process' sub-state d.post('worker', { subject: 'continue' }); d.post('worker', { subject: 'continue' }); @@ -759,15 +761,15 @@ d.post('worker', { subject: 'cancel' }); ``` [top](reference.md#table-of-contents) ### Flowchart -A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state), when applied to the context state, it results in a transition to another stage. +A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state), when applied to the context state, it results in a transition to another stage. -Flowchart rules: -* A flowchart can have one or more stages. -* A flowchart requires an initial stage. -* An initial stage is defined as a vertex without incoming edges. -* A stage can have an action. -* A stage can have zero or more conditions. -* A condition has a rule and a destination stage. +Flowchart rules: +* A flowchart can have one or more stages. +* A flowchart requires an initial stage. +* An initial stage is defined as a vertex without incoming edges. +* A stage can have an action. +* A stage can have zero or more conditions. +* A condition has a rule and a destination stage. ```javascript var d = require('durable'); @@ -775,7 +777,7 @@ var d = require('durable'); d.flowchart('expense', function() { // initial stage 'input' has two conditions input: { - request: m.subject == 'approve' && m.amount <= 1000 + request: m.subject == 'approve' && m.amount <= 1000 deny: m.subject == 'approve' && m.amount > 1000 } @@ -809,7 +811,7 @@ d.post('expense', {sid: 1, subject: 'denied'}); // event for the flowchart instance '2' immediately denied d.post('expense', {sid: 2, subject: 'approve', amount: 10000}); ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Timers Events can be scheduled with timers. A timeout condition can be included in the rule antecedent. By default a timeout is triggered as an event (observed only once). Timeouts can also be triggered as facts by 'manual reset' timers, the timers can be reset during action execution (see last example). @@ -822,15 +824,15 @@ Events can be scheduled with timers. A timeout condition can be included in the var d = require('durable'); d.ruleset('timer', function() { - + whenAll: m.subject == 'start' run: startTimer('MyTimer', 5); whenAll: { - timeout('MyTimer') + timeout('MyTimer') } run: { - console.log('timer timeout'); + console.log('timer timeout'); } }); @@ -865,13 +867,13 @@ d.statechart('risk', function() { }); // three events in a row will trigger the fraud rule -d.post('risk', { amount: 200 }); -d.post('risk', { amount: 300 }); -d.post('risk', { amount: 400 }); +d.post('risk', { amount: 200 }); +d.post('risk', { amount: 300 }); +d.post('risk', { amount: 400 }); // two events will exit after 5 seconds -d.post('risk', { sid: 1, amount: 500 }); -d.post('risk', { sid: 1, amount: 600 }); +d.post('risk', { sid: 1, amount: 500 }); +d.post('risk', { sid: 1, amount: 600 }); ``` In this example a manual reset timer is used for measuring velocity. @@ -888,7 +890,7 @@ d.statechart('risk', function() { meter: { to: 'meter' - whenAll: { + whenAll: { message = m.amount > 100 timeout('VelocityTimer') } @@ -897,7 +899,7 @@ d.statechart('risk', function() { console.log('risk velocity: ' + m.length + ' events in 5 seconds'); // resets and restarts the manual reset timer startTimer('VelocityTimer', 5, true); - } + } to: 'meter' whenAll: { @@ -910,12 +912,12 @@ d.statechart('risk', function() { } }); -d.post('risk', { amount: 200 }); -d.post('risk', { amount: 300 }); -d.post('risk', { amount: 500 }); -d.post('risk', { amount: 600 }); +d.post('risk', { amount: 200 }); +d.post('risk', { amount: 300 }); +d.post('risk', { amount: 500 }); +d.post('risk', { amount: 600 }); ``` -[top](reference.md#table-of-contents) - +[top](reference.md#table-of-contents) + diff --git a/docs/json/reference.md b/docs/json/reference.md index d4f765fd..7ffc6ae2 100644 --- a/docs/json/reference.md +++ b/docs/json/reference.md @@ -1,6 +1,8 @@ Reference Manual ===== + ## Table of contents + * [Basics](reference.md#basics) * [Rules](reference.md#rules) * [Facts](reference.md#facts) @@ -17,10 +19,10 @@ Reference Manual * [Nested Objects](reference.md#nested-objects) * [Arrays](reference.md#arrays) * [Facts and Events as rvalues](reference.md#facts-and-events-as-rvalues) -* [Consequents](reference.md#consequents) +* [Consequents](reference.md#consequents) * [Conflict Resolution](reference.md#conflict-resolution) * [Unhandled Exceptions](reference.md#unhandled-exceptions) -* [Flow Structures](reference.md#flow-structures) +* [Flow Structures](reference.md#flow-structures) * [Statechart](reference.md#statechart) * [Nested States](reference.md#nested-states) * [Flowchart](reference.md#flowchart) @@ -31,8 +33,8 @@ Reference Manual ### Rules * `all` and `any` label the antecendent definition of a rule -* `run` label the consequent definition of a rule - +* `run` label the consequent definition of a rule + ```javascript { "test": { @@ -138,11 +140,11 @@ Reference Manual } ``` -Facts can be asserted using the http API. For the example above, run the following command: +Facts can be asserted using the http API. For the example above, run the following command: `curl -H "content-type: application/json" -X POST -d '{"subject": "Tweety", "predicate": "eats", "object": "worms"}' http://localhost:5000/animal/facts` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Events @@ -172,19 +174,19 @@ Facts can be asserted using the http API. For the example above, run the follow } ``` -Events can be posted using the http API. When the example above is listening, run the following commands: +Events can be posted using the http API. When the example above is listening, run the following commands: -`curl -H "content-type: application/json" -X POST -d '{"t": "purchase", "location": "BR"}' http://localhost:5000/risk/events` -`curl -H "content-type: application/json" -X POST -d '{"t": "purchase", "location": "JP"}' http://localhost:5000/risk/events` +`curl -H "content-type: application/json" -X POST -d '{"t": "purchase", "location": "BR"}' http://localhost:5000/risk/events` +`curl -H "content-type: application/json" -X POST -d '{"t": "purchase", "location": "JP"}' http://localhost:5000/risk/events` -**Note from the autor:** +**Note from the author:** -*Using facts in the example above will produce the following output:* +*Using facts in the example above will produce the following output:* -`Fraud detected -> US, CA` -`Fraud detected -> CA, US` +`Fraud detected -> US, CA` +`Fraud detected -> CA, US` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### State Context state changes can be evaluated by rules. By convention, context state change events have the `"$s": 1` attribute. @@ -245,10 +247,10 @@ Context state changes can be evaluated by rules. By convention, context state ch } } ``` -State can also be retrieved and modified using the http API. When the example above is running, try the following commands: -`curl -H "content-type: application/json" -X POST -d '{"status": "next"}' http://localhost:5000/flow/state` +State can also be retrieved and modified using the http API. When the example above is running, try the following commands: +`curl -H "content-type: application/json" -X POST -d '{"status": "next"}' http://localhost:5000/flow/state` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Identity @@ -297,12 +299,12 @@ State can also be retrieved and modified using the http API. When the example ab ## Antecendents ### Simple Filter -A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. +A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. -Logical operators: -* Unary: "$nex" (does not exist), "$ex" (exists) -* Logical operators: "$and", "$or" -* Relational operators: "$lt", "$gt", "$lte", "$gte", "$neq", attribute relation tests equality +Logical operators: +* Unary: "$nex" (does not exist), "$ex" (exists) +* Logical operators: "$and", "$or" +* Relational operators: "$lt", "$gt", "$lte", "$gte", "$neq", attribute relation tests equality ```javascript { @@ -326,33 +328,33 @@ Logical operators: } } } -``` +``` [top](reference.md#table-of-contents) ### Pattern Matching -durable_rules implements a simple pattern matching dialect. Use `"$mt"` to define the rule match pattern. - -**Repetition** -\+ 1 or more repetitions -\* 0 or more repetitions -? optional (0 or 1 occurrence) - -**Special** -() group -| disjunct -[] range -{} repeat - -**Character classes** -. all characters -%a letters -%c control characters -%d digits -%l lower case letters -%p punctuation characters -%s space characters -%u upper case letters -%w alphanumeric characters -%x hexadecimal digits +durable_rules implements a simple pattern matching dialect. Use `"$mt"` to define the rule match pattern. + +**Repetition** +\+ 1 or more repetitions +\* 0 or more repetitions +? optional (0 or 1 occurrence) + +**Special** +() group +| disjunct +[] range +{} repeat + +**Character classes** +. all characters +%a letters +%c control characters +%d digits +%l lower case letters +%p punctuation characters +%s space characters +%u upper case letters +%w alphanumeric characters +%x hexadecimal digits ```javascript { @@ -371,9 +373,9 @@ durable_rules implements a simple pattern matching dialect. Use `"$mt"` to defin } } } -``` -[top](reference.md#table-of-contents) -### String Operations +``` +[top](reference.md#table-of-contents) +### String Operations The pattern matching dialect can be used for common string operations. Use `"$imt"` fto define case insensitive rule match pattern. ```javascript @@ -417,13 +419,13 @@ The pattern matching dialect can be used for common string operations. Use `"$im } } } -``` +``` [top](reference.md#table-of-contents) ### Correlated Sequence The `"all"` object expresses a sequence of events or facts. The object names are used to name events or facts, which can be referenced in subsequent expressions. When referencing events or facts, all properties are available. Complex patterns can be expressed using arithmetic operators. Arithmetic operators have left `"$l"` and right `"$r"` subexpressions. -Arithmetic operators: `"$add"`, `"$sub"`, `"$mul"`, `"$div"` +Arithmetic operators: `"$add"`, `"$sub"`, `"$mul"`, `"$div"` ```javascript { "risk": { @@ -472,12 +474,12 @@ Arithmetic operators: `"$add"`, `"$sub"`, `"$mul"`, `"$div"` } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Choice of Sequences -The following two labels can be used and combined to define richer event sequences: -* `"all"`: a set of event or fact patterns. All of them are required to match to trigger an action. -* `"any"`: a set of event or fact patterns. Any one match will trigger an action. +The following two labels can be used and combined to define richer event sequences: +* `"all"`: a set of event or fact patterns. All of them are required to match to trigger an action. +* `"any"`: a set of event or fact patterns. Any one match will trigger an action. ```javascript { @@ -518,7 +520,7 @@ The following two labels can be used and combined to define richer event sequenc } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Lack of Information The `$not` modifier can be used in rules with correlated sequences to evaluate the lack of information. @@ -555,10 +557,10 @@ The `$not` modifier can be used in rules with correlated sequences to evaluate t } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested Objects -Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. +Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. ```javascript { @@ -599,11 +601,11 @@ Queries on nested events or facts are also supported. The `.` notation is used f } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Arrays -Use `"$iall"` to match all items in an array, `"$iany"` to match any element in an array, `"$i"` to specify an item in the array. +Use `"$iall"` to match all items in an array, `"$iany"` to match any element in an array, `"$i"` to specify an item in the array. ```javascript { @@ -727,10 +729,10 @@ Use `"$iall"` to match all items in an array, `"$iany"` to match any element in } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Facts and Events as rvalues -Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. Use `"$m"` to reference the event or fact. +Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. Use `"$m"` to reference the event or fact. ```javascript { @@ -791,11 +793,11 @@ Aside from scalars (strings, number and boolean values), it is possible to use t } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Consequents ### Conflict Resolution -Event and fact evaluation can lead to multiple consequents. The triggering order can be controlled by using the `pri` (salience) attribute. +Event and fact evaluation can lead to multiple consequents. The triggering order can be controlled by using the `pri` (salience) attribute. ```javascript { "attributes": { @@ -841,10 +843,10 @@ Event and fact evaluation can lead to multiple consequents. The triggering order } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) -### Unhandled Exceptions -When exceptions are not handled by actions, they are stored in the context state. Exceptions are stored as an object with `"$ex"` name. +### Unhandled Exceptions +When exceptions are not handled by actions, they are stored in the context state. Exceptions are stored as an object with `"$ex"` name. ```javascript { @@ -881,22 +883,22 @@ When exceptions are not handled by actions, they are stored in the context state } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Flow Structures ### Statechart -Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. - -Statechart rules: -* A statechart can have one or more states. -* A statechart requires an initial state. -* An initial state is defined as a vertex without incoming edges. -* A state can have zero or more triggers. -* A state can have zero or more states (see [nested states](reference.md#nested-states)). -* A trigger has a destination state. -* A trigger can have a rule (absence means state enter). -* A trigger can have an action. +Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. + +Statechart rules: +* A statechart can have one or more states. +* A statechart requires an initial state. +* An initial state is defined as a vertex without incoming edges. +* A state can have zero or more triggers. +* A state can have zero or more states (see [nested states](reference.md#nested-states)). +* A trigger has a destination state. +* A trigger can have a rule (absence means state enter). +* A trigger can have an action. ```javascript { @@ -973,7 +975,7 @@ Statechart rules: } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested States Nested states allow for writing compact statecharts. If a context is in the nested state, it also (implicitly) is in the surrounding state. The statechart will attempt to handle any event in the context of the sub-state. If the sub-state does not handle an event, the event is automatically handled at the context of the super-state. Nested states are defined as an object with the name `"$chart"`. @@ -1029,15 +1031,15 @@ Nested states allow for writing compact statecharts. If a context is in the nest ``` [top](reference.md#table-of-contents) ### Flowchart -A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state), when applied to the context state, it results in a transition to another stage. +A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state), when applied to the context state, it results in a transition to another stage. -Flowchart rules: -* A flowchart can have one or more stages. -* A flowchart requires an initial stage. -* An initial stage is defined as a vertex without incoming edges. -* A stage can have an action. -* A stage can have zero or more conditions. -* A condition has a rule and a destination stage. +Flowchart rules: +* A flowchart can have one or more stages. +* A flowchart requires an initial stage. +* An initial stage is defined as a vertex without incoming edges. +* A stage can have an action. +* A stage can have zero or more conditions. +* A condition has a rule and a destination stage. ```javascript { @@ -1125,7 +1127,7 @@ Flowchart rules: } } ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Timers Events can be scheduled with timers. A timeout condition can be included in the rule antecedent. Use `"$t"` to specify the timer name to be observed. @@ -1234,11 +1236,11 @@ The example below uses a timer to detect higher event rate (use `"count"` to mat } ``` -In this example a manual reset timer is used for measuring velocity (use `"cap"` to limit the number of events matched). +In this example a manual reset timer is used for measuring velocity (use `"cap"` to limit the number of events matched). Try issuing the command below multiple times. -`curl -H "content-type: application/json" -X POST -d '{"amount": 200}' http://localhost:5000/risk/events` +`curl -H "content-type: application/json" -X POST -d '{"amount": 200}' http://localhost:5000/risk/events` ```javascript { @@ -1286,6 +1288,6 @@ Try issuing the command below multiple times. ``` -[top](reference.md#table-of-contents) - +[top](reference.md#table-of-contents) + diff --git a/docs/py/README.txt b/docs/py/README.txt index 76d48d71..1b7fa785 100644 --- a/docs/py/README.txt +++ b/docs/py/README.txt @@ -1,22 +1,22 @@ ============= durable_rules -============= +============= durable_rules is a polyglot micro-framework for real-time, consistent and scalable coordination of events. With durable_rules you can track and analyze information about things that happen (events) by combining data from multiple sources to infer more complicated circumstances. -A full forward chaining implementation (A.K.A. Rete) is used to evaluate facts and events in real time. A simple meta-linguistic abstraction lets you define simple and complex rulesets as well as control flow structures such as flowcharts, statecharts, nested statecharts and time driven flows. +A full forward chaining implementation (A.K.A. Rete) is used to evaluate facts and events in real time. A simple meta-linguistic abstraction lets you define simple and complex rulesets as well as control flow structures such as flowcharts, statecharts, nested statecharts and time driven flows. -The durable_rules core engine is implemented in C, which enables fast rule evaluation as well as muti-language support. +The durable_rules core engine is implemented in C, which enables fast rule evaluation as well as multi-language support. -durable_rules can be scaled out by offloading state to a data store out of process such as Redis. State offloading is extensible, so you can integrate the data store of your choice. +durable_rules can be scaled out by offloading state to a data store out of process such as Redis. State offloading is extensible, so you can integrate the data store of your choice. In durable_rules V2, less is more: The Rete tree is fully evaluated in C. Thus, the framework is 5x to 10x faster (depending on the scenario) and does not require Redis. The programming model for posting events, asserting and retracting facts is synchronous and does not prescribe any web framework. **Getting Started** -durable_rules is simple: to define a rule, all you need to do is describe the event or fact pattern to match (antecedent) and the action to take (consequent). +durable_rules is simple: to define a rule, all you need to do is describe the event or fact pattern to match (antecedent) and the action to take (consequent). -To install the framework do: `pip install durable_rules` +To install the framework do: `pip install durable_rules` :: @@ -33,7 +33,7 @@ To install the framework do: `pip install durable_rules` **Forward Inference** -durable_rules super-power is the foward-chaining evaluation of rules. In other words, the repeated application of logical modus ponens(https://en.wikipedia.org/wiki/Modus_ponens) to a set of facts or observed events to derive a conclusion. The example below shows a set of rules applied to a small knowledge base (set of facts). +durable_rules super-power is the forward-chaining evaluation of rules. In other words, the repeated application of logical modus ponens(https://en.wikipedia.org/wiki/Modus_ponens) to a set of facts or observed events to derive a conclusion. The example below shows a set of rules applied to a small knowledge base (set of facts). :: @@ -75,11 +75,11 @@ durable_rules super-power is the foward-chaining evaluation of rules. In other w assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'lives', 'object': 'water' }) assert_fact('animal', { 'subject': 'Greedy', 'predicate': 'eats', 'object': 'flies' }) assert_fact('animal', { 'subject': 'Greedy', 'predicate': 'lives', 'object': 'land' }) - assert_fact('animal', { 'subject': 'Tweety', 'predicate': 'eats', 'object': 'worms' }) + assert_fact('animal', { 'subject': 'Tweety', 'predicate': 'eats', 'object': 'worms' }) **Pattern Matching** -durable_rules provides string pattern matching. Expressions are compiled down to a DFA, guaranteeing linear execution time in the order of single digit nano seconds per character (note: backtracking expressions are not supported). +durable_rules provides string pattern matching. Expressions are compiled down to a DFA, guaranteeing linear execution time in the order of single digit nano seconds per character (note: backtracking expressions are not supported). :: @@ -104,27 +104,27 @@ durable_rules provides string pattern matching. Expressions are compiled down to **Business Rules and Miss Manners** -durable_rules can also be used to solve traditional Production Business Rules problems. This example is an industry benchmark. Miss Manners has decided to throw a party. She wants to seat her guests such that adjacent people are of opposite sex and share at least one hobby. +durable_rules can also be used to solve traditional Production Business Rules problems. This example is an industry benchmark. Miss Manners has decided to throw a party. She wants to seat her guests such that adjacent people are of opposite sex and share at least one hobby. -Note how the benchmark flow structure is defined using a statechart to improve code readability without sacrificing performance nor altering the combinatorics required by the benchmark. For 128 guests, 438 facts, the execution time is 600 ms. +Note how the benchmark flow structure is defined using a statechart to improve code readability without sacrificing performance nor altering the combinatorics required by the benchmark. For 128 guests, 438 facts, the execution time is 600 ms. -https://github.com/jruizgit/rules/blob/master/testpy/manners.py +https://github.com/jruizgit/rules/blob/master/testpy/manners.py -IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive +IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive **Image recognition and Waltzdb** -Waltzdb is a constraint propagation problem for image recognition: given a set of lines in a 2D space, the system needs to interpret the 3D depth of the image. The first part of the algorithm consists of identifying four types of junctions, then labeling the junctions following Huffman-Clowes notation. Pairs of adjacent junctions constraint each other’s edge labeling. So, after choosing the labeling for an initial junction, the second part of the algorithm iterates through the graph, propagating the labeling constraints by removing inconsistent labels. +Waltzdb is a constraint propagation problem for image recognition: given a set of lines in a 2D space, the system needs to interpret the 3D depth of the image. The first part of the algorithm consists of identifying four types of junctions, then labeling the junctions following Huffman-Clowes notation. Pairs of adjacent junctions constraint each other’s edge labeling. So, after choosing the labeling for an initial junction, the second part of the algorithm iterates through the graph, propagating the labeling constraints by removing inconsistent labels. -In this case too, the benchmark flow structure is defined using a statechart to improve code readability. The benchmark requirements are not altered. The execution time, for the case of 4 regions 654 ms. +In this case too, the benchmark flow structure is defined using a statechart to improve code readability. The benchmark requirements are not altered. The execution time, for the case of 4 regions 654 ms. https://github.com/jruizgit/rules/blob/master/testpy/waltzdb.py -IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive +IMac, 4GHz i7, 32GB 1600MHz DDR3, 1.12 TB Fusion Drive **Reference Manual:** -https://github.com/jruizgit/rules/blob/master/docs/py/reference.md +https://github.com/jruizgit/rules/blob/master/docs/py/reference.md diff --git a/docs/py/reference.md b/docs/py/reference.md index 9d976635..5bb8107d 100644 --- a/docs/py/reference.md +++ b/docs/py/reference.md @@ -1,6 +1,8 @@ Reference Manual ===== + ## Table of contents + * [Setup](reference.md#setup) * [Basics](reference.md#basics) * [Rules](reference.md#rules) @@ -19,12 +21,12 @@ Reference Manual * [Nested Objects](reference.md#nested-objects) * [Arrays](reference.md#arrays) * [Facts and Events as rvalues](reference.md#facts-and-events-as-rvalues) -* [Consequents](reference.md#consequents) +* [Consequents](reference.md#consequents) * [Conflict Resolution](reference.md#conflict-resolution) * [Action Batches](reference.md#action-batches) * [Async Actions](reference.md#async-actions) * [Unhandled Exceptions](reference.md#unhandled-exceptions) -* [Flow Structures](reference.md#flow-structures) +* [Flow Structures](reference.md#flow-structures) * [Statechart](reference.md#statechart) * [Nested States](reference.md#nested-states) * [Flowchart](reference.md#flowchart) @@ -33,12 +35,12 @@ Reference Manual ## Setup ### First App -Let's write a simple rule: +Let's write a simple rule: -1. Start a terminal -2. Create a directory for your app: `mkdir firstapp` `cd firstapp` -3. In the new directory `pip install durable_rules` (this will download durable_rules and its dependencies) -4. In that same directory create a test.py file using your favorite editor +1. Start a terminal +2. Create a directory for your app: `mkdir firstapp` `cd firstapp` +3. In the new directory `pip install durable_rules` (this will download durable_rules and its dependencies) +4. In that same directory create a test.py file using your favorite editor 5. Copy/Paste and save the following code: ```python from durable.lang import * @@ -49,16 +51,16 @@ Let's write a simple rule: post('test', { 'subject': 'World' }) ``` -7. In the terminal type `python test.py` -8. You should see the message: `Hello World` +7. In the terminal type `python test.py` +8. You should see the message: `Hello World` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Basics ### Rules A rule is the basic building block of the framework. The rule antecendent defines the conditions that need to be satisfied to execute the rule consequent (action). By convention `m` represents the data to be evaluated by a given rule. * `when_all` and `when_any` annotate the antecendent definition of a rule - + ```python from durable.lang import * @@ -103,10 +105,10 @@ with ruleset('animal'): assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'eats', 'object': 'flies' }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Events -Events can be posted to and evaluated by rules. An event is an ephemeral fact, that is, a fact retracted right before executing a consequent. Thus, events can only be observed once. Events are stored until they are observed. +Events can be posted to and evaluated by rules. An event is an ephemeral fact, that is, a fact retracted right before executing a consequent. Thus, events can only be observed once. Events are stored until they are observed. ```python from durable.lang import * @@ -117,26 +119,26 @@ with ruleset('risk'): # the event pair will only be observed once def fraud(c): print('Fraud detected -> {0}, {1}'.format(c.first.location, c.second.location)) - + post('risk', {'t': 'purchase', 'location': 'US'}) post('risk', {'t': 'purchase', 'location': 'CA'}) ``` -**Note:** +**Note:** -*Using facts in the example above will produce the following output:* +*Using facts in the example above will produce the following output:* -`Fraud detected -> US, CA` -`Fraud detected -> CA, US` +`Fraud detected -> US, CA` +`Fraud detected -> CA, US` -*In the example both facts satisfy the first condition m.t == 'purchase' and each fact satisfies the second condition m.location != c.first.location in relation to the facts which satisfied the first.* +*In the example both facts satisfy the first condition m.t == 'purchase' and each fact satisfies the second condition m.location != c.first.location in relation to the facts which satisfied the first.* -*An event ia an ephemeral fact. As soon as a fact is scheduled to be dispatched, it is retracted. When using post in the example above, by the time the second pair is calculated the events have already been retracted.* +*An event ia an ephemeral fact. As soon as a fact is scheduled to be dispatched, it is retracted. When using post in the example above, by the time the second pair is calculated the events have already been retracted.* -*Retracting events before dispatch reduces the number of combinations to be calculated during action execution.* +*Retracting events before dispatch reduces the number of combinations to be calculated during action execution.* -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### State Context state is available when a consequent is executed. The same context state is passed across rule execution. Context state is stored until it is deleted. Context state changes can be evaluated by rules. By convention `s` represents the state to be evaluated by a rule. @@ -149,17 +151,17 @@ with ruleset('flow'): @when_all(s.status == 'start') def start(c): # state update on 's' - c.s.status = 'next' + c.s.status = 'next' print('start') @when_all(s.status == 'next') def next(c): - c.s.status = 'last' + c.s.status = 'last' print('next') @when_all(s.status == 'last') def last(c): - c.s.status = 'end' + c.s.status = 'end' print('last') # deletes state at the end c.delete_state() @@ -167,9 +169,9 @@ with ruleset('flow'): update_state('flow', { 'status': 'start' }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Identity -Facts with the same property names and values are considered equal when asserted or retracted. Events with the same property names and values are considered different when posted because the posting time matters. +Facts with the same property names and values are considered equal when asserted or retracted. Events with the same property names and values are considered different when posted because the posting time matters. ```python from durable.lang import * @@ -183,13 +185,13 @@ with ruleset('bookstore'): @when_all(+m.name) def fact(c): print('bookstore-> Added {0}'.format(c.m.name)) - + # this rule will be triggered when the fact is retracted @when_all(none(+m.name)) def empty(c): print('bookstore-> No books') -# will not throw because the fact assert was successful +# will not throw because the fact assert was successful assert_fact('bookstore', { 'name': 'The new book', 'seller': 'bookstore', @@ -197,7 +199,7 @@ assert_fact('bookstore', { 'price': 500 }) -# will throw MessageObservedError because the fact has already been asserted +# will throw MessageObservedError because the fact has already been asserted try: assert_fact('bookstore', { 'reference': '75323', @@ -228,7 +230,7 @@ retract_fact('bookstore', { }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Error Codes When asserting a fact, retracting a fact, posting an event or updating state context, the following exceptions can be thrown: @@ -236,16 +238,16 @@ When asserting a fact, retracting a fact, posting an event or updating state con * MessageObservedException: The fact has already been asserted or the event has already been posted. * MessageNotHandledException: The event or fact was not captured because it did not match any rule. -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Antecendents ### Simple Filter -A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. By convention events or facts are represented with the `m` name. Context state are represented with the `s` name. +A rule antecedent is an expression. The left side of the expression represents an event or fact property. The right side defines a pattern to be matched. By convention events or facts are represented with the `m` name. Context state are represented with the `s` name. -Logical operators: -* Unary: - (does not exist), + (exists) -* Logical operators: &, | -* Relational operators: < , >, <=, >=, ==, != +Logical operators: +* Unary: - (does not exist), + (exists) +* Logical operators: &, | +* Relational operators: < , >, <=, >=, ==, != ```python from durable.lang import * @@ -254,36 +256,36 @@ with ruleset('expense'): @when_all((m.subject == 'approve') | (m.subject == 'ok')) def approved(c): print ('Approved subject: {0}'.format(c.m.subject)) - + post('expense', { 'subject': 'approve'}) -``` -[top](reference.md#table-of-contents) +``` +[top](reference.md#table-of-contents) ### Pattern Matching -durable_rules implements a simple pattern matching dialect. It uses % to escape, which vastly simplifies writing expressions. Expressions are compiled down into a deterministic state machine, thus backtracking is not supported. Event processing is O(n) guaranteed (n being the size of the event). - -**Repetition** -\+ 1 or more repetitions -\* 0 or more repetitions -? optional (0 or 1 occurrence) - -**Special** -() group -| disjunct -[] range -{} repeat - -**Character classes** -. all characters -%a letters -%c control characters -%d digits -%l lower case letters -%p punctuation characters -%s space characters -%u upper case letters -%w alphanumeric characters -%x hexadecimal digits +durable_rules implements a simple pattern matching dialect. It uses % to escape, which vastly simplifies writing expressions. Expressions are compiled down into a deterministic state machine, thus backtracking is not supported. Event processing is O(n) guaranteed (n being the size of the event). + +**Repetition** +\+ 1 or more repetitions +\* 0 or more repetitions +? optional (0 or 1 occurrence) + +**Special** +() group +| disjunct +[] range +{} repeat + +**Character classes** +. all characters +%a letters +%c control characters +%d digits +%l lower case letters +%p punctuation characters +%s space characters +%u upper case letters +%w alphanumeric characters +%x hexadecimal digits ```python from durable.lang import * @@ -301,11 +303,11 @@ post('match', { 'url': 'http://github.com/jruizgit/rul!es' }, match_complete_cal post('match', { 'url': 'https://github.com/jruizgit/rules/reference.md' }) post('match', { 'url': '//rules'}, match_complete_callback) post('match', { 'url': 'https://github.c/jruizgit/rules' }, match_complete_callback) -``` +``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) -### String Operations +### String Operations The pattern matching dialect can be used for common string operations. The `imatches` function enables case insensitive pattern matching. ```python @@ -323,22 +325,22 @@ with ruleset('strings'): @when_all(m.subject.imatches('.*hello.*')) def contains(c): print ('string contains hello (case insensitive) -> {0}'.format(c.m.subject)) - + assert_fact('strings', { 'subject': 'HELLO world' }) assert_fact('strings', { 'subject': 'world hello' }) assert_fact('strings', { 'subject': 'hello hi' }) assert_fact('strings', { 'subject': 'has Hello string' }) assert_fact('strings', { 'subject': 'does not match' }) -``` +``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Correlated Sequence -Rules can be used to efficiently evaluate sequences of correlated events or facts. The fraud detection rule in the example below shows a pattern of three events: the second event amount being more than 200% the first event amount and the third event amount greater than the average of the other two. +Rules can be used to efficiently evaluate sequences of correlated events or facts. The fraud detection rule in the example below shows a pattern of three events: the second event amount being more than 200% the first event amount and the third event amount greater than the average of the other two. By default a correlated sequence captures distinct messages. In the example below the second event satisfies the second and the third condition, however the event will be captured only for the second condition. Use the `distinct` attribute to disable distinct event or fact correlation. -The `when_all` annotation expresses a sequence of events or facts. The `<<` operator is used to name events or facts, which can be referenced in subsequent expressions. When referencing events or facts, all properties are available. Complex patterns can be expressed using arithmetic operators. +The `when_all` annotation expresses a sequence of events or facts. The `<<` operator is used to name events or facts, which can be referenced in subsequent expressions. When referencing events or facts, all properties are available. Complex patterns can be expressed using arithmetic operators. Arithmetic operators: +, -, *, / ```python @@ -353,45 +355,45 @@ with ruleset('risk'): print('fraud detected -> {0}'.format(c.first.amount)) print(' -> {0}'.format(c.second.amount)) print(' -> {0}'.format(c.third.amount)) - + post('risk', { 'amount': 50 }) post('risk', { 'amount': 200 }) post('risk', { 'amount': 251 }) -``` +``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Choice of Sequences durable_rules allows expressing and efficiently evaluating richer event sequences. In the example below each of the two event\fact sequences will trigger an action. -The following two functions can be used and combined to define richer event sequences: -* all: a set of event or fact patterns. All of them are required to match to trigger an action. -* any: a set of event or fact patterns. Any one match will trigger an action. +The following two functions can be used and combined to define richer event sequences: +* all: a set of event or fact patterns. All of them are required to match to trigger an action. +* any: a set of event or fact patterns. Any one match will trigger an action. ```python from durable.lang import * with ruleset('expense'): - @when_any(all(c.first << m.subject == 'approve', - c.second << m.amount == 1000), - all(c.third << m.subject == 'jumbo', + @when_any(all(c.first << m.subject == 'approve', + c.second << m.amount == 1000), + all(c.third << m.subject == 'jumbo', c.fourth << m.amount == 10000)) def action(c): if c.first: print ('Approved {0} {1}'.format(c.first.subject, c.second.amount)) else: print ('Approved {0} {1}'.format(c.third.subject, c.fourth.amount)) - + post('expense', { 'subject': 'approve' }) post('expense', { 'amount': 1000 }) post('expense', { 'subject': 'jumbo' }) post('expense', { 'amount': 10000 }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Lack of Information -In some cases lack of information is meaningful. The `none` function can be used in rules with correlated sequences to evaluate the lack of information. +In some cases lack of information is meaningful. The `none` function can be used in rules with correlated sequences to evaluate the lack of information. *Note: the `none` function requires information to reason about a lack of information. That is, it will not trigger any actions if no events or facts have been registered in the corresponding rule.* @@ -405,7 +407,7 @@ with ruleset('risk'): c.fourth << m.t == 'chargeback') def detected(c): print('fraud detected {0} {1} {2}'.format(c.first.t, c.third.t, c.fourth.t)) - + assert_fact('risk', { 't': 'deposit' }) assert_fact('risk', { 't': 'withdrawal' }) assert_fact('risk', { 't': 'chargeback' }) @@ -418,10 +420,10 @@ retract_fact('risk', { 'sid': 1, 't': 'balance' }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested Objects -Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. +Queries on nested events or facts are also supported. The `.` notation is used for defining conditions on properties in nested objects. ```python from durable.lang import * @@ -433,14 +435,14 @@ with ruleset('expense'): def approved(c): print ('bill amount ->{0}'.format(c.bill.invoice.amount)) print ('account payment amount ->{0}'.format(c.account.payment.invoice.amount)) - + # one level of nesting post('expense', {'t': 'bill', 'invoice': {'amount': 100}}) - + #two levels of nesting post('expense', {'t': 'account', 'payment': {'invoice': {'amount': 100}}}) -``` -[top](reference.md#table-of-contents) +``` +[top](reference.md#table-of-contents) ### Arrays @@ -467,23 +469,23 @@ with ruleset('risk'): @when_all(m.payments.anyItem(item.allItems(item < 100))) def rule4(c): print('fraud 4 detected {0}'.format(c.m.payments)) - + post('risk', {'payments': [ 150, 300, 450 ]}) post('risk', {'payments': [ { 'amount' : 200 }, { 'amount' : 300 }, { 'amount' : 450 } ]}) post('risk', {'cards': [ 'one card', 'two cards', 'three cards' ]}) -post('risk', {'payments': [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}) -``` -[top](reference.md#table-of-contents) +post('risk', {'payments': [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}) +``` +[top](reference.md#table-of-contents) ### Facts and Events as rvalues -Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. +Aside from scalars (strings, number and boolean values), it is possible to use the fact or event observed on the right side of an expression. ```python from durable.lang import * with ruleset('risk'): - # compares properties in the same event, this expression is evaluated in the client + # compares properties in the same event, this expression is evaluated in the client @when_all(m.debit > m.credit * 2) def fraud_1(c): print('debit {0} more than twice the credit {1}'.format(c.m.debit, c.m.credit)) @@ -494,15 +496,15 @@ with ruleset('risk'): def fraud_2(c): print('fraud detected ->{0}'.format(c.first.amount)) print('fraud detected ->{0}'.format(c.second.amount)) - + post('risk', { 'debit': 220, 'credit': 100 }) post('risk', { 'debit': 150, 'credit': 100 }) post('risk', { 'amount': 200 }) post('risk', { 'amount': 500 }) ``` -[top](reference.md#table-of-contents) - +[top](reference.md#table-of-contents) + ## Consequents ### Conflict Resolution Event and fact evaluation can lead to multiple consequents. The triggering order can be controlled by using the `pri` (salience) function. Actions with lower value are executed first. The default value for all actions is 0. @@ -515,25 +517,25 @@ with ruleset('attributes'): @when_all(pri(3), m.amount < 300) def first_detect(c): print('attributes P3 ->{0}'.format(c.m.amount)) - + @when_all(pri(2), m.amount < 200) def second_detect(c): print('attributes P2 ->{0}'.format(c.m.amount)) - + @when_all(pri(1), m.amount < 100) def third_detect(c): print('attributes P1 ->{0}'.format(c.m.amount)) - + assert_fact('attributes', { 'amount': 50 }) assert_fact('attributes', { 'amount': 150 }) assert_fact('attributes', { 'amount': 250 }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Action Batches When a high number of events or facts satisfy a consequent, the consequent results can be delivered in batches. -* count: defines the exact number of times the rule needs to be satisfied before scheduling the action. -* cap: defines the maximum number of times the rule needs to be satisfied before scheduling the action. +* count: defines the exact number of times the rule needs to be satisfied before scheduling the action. +* cap: defines the maximum number of times the rule needs to be satisfied before scheduling the action. This example batches exactly three approvals and caps the number of rejects to two: ```python @@ -545,7 +547,7 @@ with ruleset('expense'): def approve(c): print('approved {0}'.format(c.m)) - # this rule will be triggered when 'expense' is asserted batching at most two results + # this rule will be triggered when 'expense' is asserted batching at most two results @when_all(cap(2), c.expense << m.amount >= 100, c.approval << m.review == True) @@ -560,8 +562,8 @@ post_batch('expense', [{ 'amount': 10 }, { 'amount': 400 }]) assert_fact('expense', { 'review': True }) ``` -[top](reference.md#table-of-contents) -### Async Actions +[top](reference.md#table-of-contents) +### Async Actions The consequent action can be asynchronous. When the action is finished, the `complete` function has to be called. By default an action is considered abandoned after 5 seconds. This value can be changed by returning a different number in the action function or extended by calling `renew_action_lease`. ```python @@ -573,21 +575,21 @@ with ruleset('flow'): def start_timer(time, callback): timer = threading.Timer(time, callback) - timer.daemon = True + timer.daemon = True timer.start() @when_all(s.state == 'first') # async actions take a callback argument to signal completion def first(c, complete): def end_first(): - c.s.state = 'second' + c.s.state = 'second' print('first completed') # completes the action after 3 seconds complete(None) - + start_timer(3, end_first) - + @when_all(s.state == 'second') def second(c, complete): def end_second(): @@ -602,18 +604,18 @@ with ruleset('flow'): # overrides the 5 second default abandon timeout return 10 - + update_state('flow', { 'state': 'first' }) ``` -[top](reference.md#table-of-contents) -### Unhandled Exceptions +[top](reference.md#table-of-contents) +### Unhandled Exceptions When exceptions are not handled by actions, they are stored in the context state. This enables writing exception-handling rules. ```python from durable.lang import * with ruleset('flow'): - + @when_all(m.action == 'start') def first(c): raise Exception('Unhandled Exception!') @@ -623,24 +625,24 @@ with ruleset('flow'): def second(c): print(c.s.exception) c.s.exception = None - + post('flow', { 'action': 'start' }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ## Flow Structures ### Statechart -Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. - -Statechart rules: -* A statechart can have one or more states. -* A statechart requires an initial state. -* An initial state is defined as a vertex without incoming edges. -* A state can have zero or more triggers. -* A state can have zero or more states (see [nested states](reference.md#nested-states)). -* A trigger has a destination state. -* A trigger can have a rule (absence means state enter). -* A trigger can have an action. +Rules can be organized using statecharts. A statechart is a deterministic finite automaton (DFA). The state context is in one of a number of possible states with conditional transitions between these states. + +Statechart rules: +* A statechart can have one or more states. +* A statechart requires an initial state. +* An initial state is defined as a vertex without incoming edges. +* A state can have zero or more triggers. +* A state can have zero or more states (see [nested states](reference.md#nested-states)). +* A trigger has a destination state. +* A trigger can have a rule (absence means state enter). +* A trigger can have an action. ```python from durable.lang import * @@ -654,28 +656,28 @@ with statechart('expense'): # action executed before state change def denied(c): print ('denied amount {0}'.format(c.m.amount)) - - @to('pending') + + @to('pending') @when_all((m.subject == 'approve') & (m.amount <= 1000)) def request(c): print ('requesting approve amount {0}'.format(c.m.amount)) - + # intermediate state 'pending' with two triggers with state('pending'): @to('approved') @when_all(m.subject == 'approved') def approved(c): print ('expense approved') - + @to('denied') @when_all(m.subject == 'denied') def denied(c): print ('expense denied') - - # 'denied' and 'approved' are final states + + # 'denied' and 'approved' are final states state('denied') state('approved') - + # events directed to default statechart instance post('expense', { 'subject': 'approve', 'amount': 100 }) post('expense', { 'subject': 'approved' }) @@ -687,7 +689,7 @@ post('expense', { 'sid': 1, 'subject': 'denied' }) # events directed to statechart instance with id '2' post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000 }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Nested States Nested states allow for writing compact statecharts. If a context is in the nested state, it also (implicitly) is in the surrounding state. The statechart will attempt to handle any event in the context of the sub-state. If the sub-state does not handle an event, the event is automatically handled at the context of the super-state. @@ -703,7 +705,7 @@ with statechart('worker'): @when_all(m.subject == 'enter') def continue_process(c): print('start process') - + with state('process'): @to('process') @when_all(m.subject == 'continue') @@ -730,38 +732,38 @@ post('worker', { 'subject': 'cancel' }) ``` [top](reference.md#table-of-contents) ### Flowchart -A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state) when applied to the context state it results in a transition to another stage. +A flowchart is another way of organizing a ruleset flow. In a flowchart each stage represents an action to be executed. So (unlike the statechart state) when applied to the context state it results in a transition to another stage. -Flowchart rules: -* A flowchart can have one or more stages. -* A flowchart requires an initial stage. -* An initial stage is defined as a vertex without incoming edges. -* A stage can have an action. -* A stage can have zero or more conditions. -* A condition has a rule and a destination stage. +Flowchart rules: +* A flowchart can have one or more stages. +* A flowchart requires an initial stage. +* An initial stage is defined as a vertex without incoming edges. +* A stage can have an action. +* A stage can have zero or more conditions. +* A condition has a rule and a destination stage. ```python from durable.lang import * with flowchart('expense'): # initial stage 'input' has two conditions - with stage('input'): + with stage('input'): to('request').when_all((m.subject == 'approve') & (m.amount <= 1000)) to('deny').when_all((m.subject == 'approve') & (m.amount > 1000)) - + # intermediate stage 'request' has an action and three conditions with stage('request'): @run def request(c): print('requesting approve') - + to('approve').when_all(m.subject == 'approved') to('deny').when_all(m.subject == 'denied') # reflexive condition: if met, returns to the same stage to('request').when_all(m.subject == 'retry') - + with stage('approve'): - @run + @run def approved(c): print('expense approved') @@ -782,7 +784,7 @@ post('expense', { 'sid': 1, 'subject': 'denied'}) # event for the flowchart instance '2' immediately denied post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000}) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) ### Timers Events can be scheduled with timers. A timeout condition can be included in the rule antecedent. By default a timeout is triggered as an event (observed only once). Timeouts can also be triggered as facts by 'manual reset' timers and the timers can be reset during action execution (see last example). @@ -795,11 +797,11 @@ Events can be scheduled with timers. A timeout condition can be included in the from durable.lang import * with ruleset('timer'): - + @when_all(m.subject == 'start') def start(c): c.start_timer('MyTimer', 5) - + @when_all(timeout('MyTimer')) def timer(c): print('timer timeout') @@ -807,7 +809,7 @@ with ruleset('timer'): post('timer', { 'subject': 'start' }) ``` -The example below uses a timer to detect a higher event rate: +The example below uses a timer to detect a higher event rate: ```python from durable.lang import * @@ -823,7 +825,7 @@ with statechart('risk'): @when_all(count(3), c.message << m.amount > 100) def fraud(c): for e in c.m: - print(e.message) + print(e.message) @to('exit') @when_all(timeout('RiskTimer')) @@ -843,7 +845,7 @@ post('risk', { 'sid': 1, 'amount': 500 }) post('risk', { 'sid': 1, 'amount': 600 }) ``` -In this example a manual reset timer is used for measuring velocity. +In this example a manual reset timer is used for measuring velocity. ```python from durable.lang import * @@ -856,7 +858,7 @@ with statechart('risk'): with state('meter'): @to('meter') - @when_all(cap(5), + @when_all(cap(5), m.amount > 100, timeout('VelocityTimer')) def some_events(c): @@ -879,4 +881,4 @@ post('risk', { 'amount': 500 }) post('risk', { 'amount': 600 }) ``` -[top](reference.md#table-of-contents) +[top](reference.md#table-of-contents) diff --git a/libjs/durable.js b/libjs/durable.js index 1d20b3a9..b23641cb 100644 --- a/libjs/durable.js +++ b/libjs/durable.js @@ -523,7 +523,7 @@ exports = module.exports = durableEngine = function () { } else if (s.type === 'LabeledStatement') { return s; } else { - throw 'syntax error: statment type ' + s.type + ' unexpected'; + throw 'syntax error: statement type ' + s.type + ' unexpected'; } }, []); } else { diff --git a/libpy/durable/lang.py b/libpy/durable/lang.py index 2d32006d..b3d7c9b0 100644 --- a/libpy/durable/lang.py +++ b/libpy/durable/lang.py @@ -88,11 +88,11 @@ def define(self, parent_name = None): else: left_definition = {self._name: self._left} - righ_definition = self._right + right_definition = self._right if isinstance(self._right, avalue): - righ_definition = self._right.define() + right_definition = self._right.define() - return {self._op: {'$l': left_definition, '$r': righ_definition}} + return {self._op: {'$l': left_definition, '$r': right_definition}} class closure(object): diff --git a/librb/durable.rb b/librb/durable.rb index 20f26d9e..878bd08e 100644 --- a/librb/durable.rb +++ b/librb/durable.rb @@ -103,12 +103,12 @@ def definition left_definition = {@name => @left} end - righ_definition = @right + right_definition = @right if @right.kind_of? Arithmetic - righ_definition = @right.definition + right_definition = @right.definition end - return {@op => {:$l => left_definition, :$r => righ_definition}} + return {@op => {:$l => left_definition, :$r => right_definition}} end end @@ -224,18 +224,18 @@ def definition(parent_name=nil) if not @left raise ArgumentError, "Property for #{@__type} and #{@__op} not defined" end - righ_definition = @right + right_definition = @right if (@right.kind_of? Expression) || (@right.kind_of? Arithmetic) - righ_definition = @right.definition + right_definition = @right.definition end if @__op == :$eq - new_definition = {@left => righ_definition} + new_definition = {@left => right_definition} else if not @right new_definition = {@__type => @left} else - new_definition = {@__op => {@left => righ_definition}} + new_definition = {@__op => {@left => right_definition}} end end end diff --git a/testjs/test.js b/testjs/test.js index 6c8c0adc..706f5643 100644 --- a/testjs/test.js +++ b/testjs/test.js @@ -63,7 +63,7 @@ function (err, result) { var hash3 = result; client.script('load', 'for i = 1, 500000, 1 do\n' + - ' local resul = redis.call("lrange", "test2", 0, -1)\n' + + ' local result = redis.call("lrange", "test2", 0, -1)\n' + 'end\n', function (err, result) { if (err) {